Skip to content

第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]

s2s 共享底层数组,改 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:mapmap[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"]nilnil[0] 越界 panic。

更典型的坑:

go
type S struct { A int }
m := map[string]S{"x": {1}}
m["x"].A = 100   // ❌ 编译错误:cannot assign to struct field

map 的 value 是 struct 时不可寻址,要么改成 map[string]*S,要么读出来 → 改 → 写回。


📌 速记口诀

"数组定长,切片活;map 必 make 才能写;切片共享要警惕;中文长度算字节"


🎯 自测练习

  1. 实现 func unique[T comparable](s []T) []T,泛型去重
  2. 写一个 LRU 缓存(hint:双向链表 + map)
  3. 实现 func ReverseString(s string) string,能正确处理中文
  4. 解释 s := s[:0] 的作用
  5. 解释为什么 m := map[K]V{}var m map[K]V 更安全

完成后进入第06章 🚀