主题
第05章 面试题精选 —— 集合类型
★ 基础题
Q1:数组和切片有什么区别?
| 数组 | 切片 | |
|---|---|---|
| 长度 | 类型一部分(不可变) | 可动态变化 |
| 值/引用 | 值类型(拷贝整个) | 引用语义(拷贝头) |
| 实战使用 | 极少 | 99% 场景 |
| 比较 | == 逐元素比较 | 不能用 ==,要用 slices.Equal |
Q2:make([]int, 5, 10) 的两个数字分别是什么?
- 第一个数字(5):长度
len - 第二个数字(10):容量
cap
go
s := make([]int, 5, 10)
// s = [0 0 0 0 0]
// len(s) = 5
// cap(s) = 10
// 还能 append 5 个元素而不触发扩容Q3:下面代码输出什么?
go
s := []int{1, 2, 3}
s2 := s[1:]
s2[0] = 99
fmt.Println(s)答:[1 99 3]
s2 与 s 共享底层数组,改 s2[0] 等于改 s[1]。
Q4:map 的零值是什么?为什么不能写?
map 的零值是 nil。
go
var m map[string]int
fmt.Println(m == nil) // true
m["a"] = 1 // panic原因:nil map 没有底层哈希表结构,写入需要找桶——根本没桶。
读 nil map 不会 panic,返回零值。
Q5:怎么判断 map 中是否存在某个 key?
go
v, ok := m["key"]
if ok { fmt.Println("存在", v) }
// 注意:不能仅靠 v == 0 判断,因为零值也可能是合法值★★ 进阶题
Q6:append 一定会修改原切片吗?
答:不一定。
go
s := []int{1, 2, 3} // len=3 cap=3
s2 := append(s, 4) // 触发扩容,s2 是新切片
s2[0] = 100
fmt.Println(s) // [1 2 3],未受影响但如果 cap 够:
go
s := make([]int, 3, 5) // [0 0 0] cap=5
s2 := append(s, 4) // 不扩容,s2[0..2] 与 s 共享
s2[0] = 100
fmt.Println(s) // [100 0 0]这是 append 最隐蔽的坑。
Q7:实现切片去重。
go
func dedup(s []int) []int {
seen := make(map[int]struct{})
result := make([]int, 0, len(s))
for _, v := range s {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
result = append(result, v)
}
}
return result
}考点:map 当 set 用,struct{} 占 0 字节最省内存。
Q8:为什么 map 的遍历顺序是随机的?
答:Go 故意在每次 range 时随机化起点,防止开发者依赖顺序。
如果允许有序,开发者可能习惯依赖某种隐式顺序,一旦底层实现改变就会出 bug。Go 团队选择"明确告诉你:不要依赖顺序"。
Q9:len("中国") 等于几?为什么?
答:6。
"中国" 在 UTF-8 中占 6 个字节(中、国 各 3 字节),len 返回字节数。
要得到字符数:
go
len([]rune("中国")) // 2
utf8.RuneCountInString("中国") // 2,更高效Q10:为什么大量字符串拼接要用 strings.Builder?
go
// ❌ 慢,每次都分配新内存
s := ""
for i := 0; i < 10000; i++ {
s += "x"
}
// ✅ 快,内部用动态扩容的 []byte
var b strings.Builder
for i := 0; i < 10000; i++ {
b.WriteString("x")
}
s := b.String()+= 拼接的复杂度是 O(N²),Builder 是 O(N),差异在 N=10000 时已是几百倍。
★★★ 深度题
Q11:切片头是多大?
在 64 位机器上:24 字节(3 个 8 字节字段:ptr + len + cap)。
字符串头是 16 字节(ptr + len)。
map 头是个指针(hmap 结构体),通常 8 字节。
这是为什么"切片传参"是廉价的——拷贝 24 字节而已。
Q12:range 一个 channel 时为什么不需要双值?
go
for v := range ch {
// ...
}答:channel 的 range 只产出值,没有"索引"。当 channel 被关闭且缓冲区清空时循环自动退出。
Q13:copy(dst, src) 拷贝多少个元素?
go
dst := make([]int, 3)
src := []int{1, 2, 3, 4, 5}
n := copy(dst, src) // n = 3答:n = min(len(dst), len(src)),不会扩容 dst。
Q14:map 是 map[K]struct{} 当作 set 用,为什么要用空结构体?
go
seen := make(map[int]struct{})
seen[42] = struct{}{}答:struct{} 占用 0 字节,不像 bool 占 1 字节。100 万个元素能省 100 万字节。
Q15:以下代码为什么 panic?
go
m := map[string][]int{}
m["a"] = append(m["a"], 1) // ✅
m["a"][0] = 100 // ✅
delete(m, "a")
fmt.Println(m["a"][0]) // ❌答:删除后 m["a"] 是 nil,nil[0] 越界 panic。
更典型的坑:
go
type S struct { A int }
m := map[string]S{"x": {1}}
m["x"].A = 100 // ❌ 编译错误:cannot assign to struct fieldmap 的 value 是 struct 时不可寻址,要么改成 map[string]*S,要么读出来 → 改 → 写回。
📌 速记口诀
"数组定长,切片活;map 必 make 才能写;切片共享要警惕;中文长度算字节"
🎯 自测练习
- 实现
func unique[T comparable](s []T) []T,泛型去重 - 写一个 LRU 缓存(hint:双向链表 + map)
- 实现
func ReverseString(s string) string,能正确处理中文 - 解释
s := s[:0]的作用 - 解释为什么
m := map[K]V{}比var m map[K]V更安全
完成后进入第06章 🚀