Go 后端面试复盘:语言、并发与服务实践 · 2. 数组、切片与字符串
前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。
2. 数组、切片与字符串
2.1 数组和切片有什么区别?
| 对比项 | 数组 | 切片 |
|---|---|---|
| 类型 | 长度属于类型一部分 | 动态视图 |
| 赋值 | 复制全部元素 | 复制切片头 |
| 长度 | 固定 | 可变 |
| 能否比较 | 元素可比较时可以 | 不能,除与 nil 比较 |
| 常见用途 | 固定大小数据、值语义 | 日常集合处理 |
[3]int 和 [4]int 是不同类型。
2.2 slice 的底层结构是什么?
概念上,slice 由三个字段构成:
type sliceHeader struct {
Data unsafe.Pointer
Len int
Cap int
}- Data:指向底层数组起始位置。
- Len:当前可访问元素数。
- Cap:从当前起始位置到底层数组末尾的容量。
不要在业务代码中直接操作 reflect.SliceHeader。它用于解释模型,不代表可以安全绕过类型系统。
2.3 len 和 cap 有什么区别?
len(s):当前切片中可直接访问的元素数量。cap(s):从切片起点到其底层数组末端能容纳的元素数量。
a := []int{0, 1, 2, 3, 4}
s := a[1:3]
// s = [1,2], len=2, cap=4完整切片表达式可以限制容量:
s := a[1:3:3] // len=2, cap=2这样对 s 执行 append 时更容易触发新数组分配,避免覆盖原数组后续元素。
2.4 append 一定会分配新数组吗?
不一定。
- 若剩余容量足够,append 复用原底层数组。
- 若容量不足,运行时申请更大的数组,复制旧元素,并返回新的切片。
因此必须接收 append 返回值:
s = append(s, v)容量增长是运行时实现细节。面试中可以说“小切片常见近似倍增,大切片增长更平缓,并受元素大小、内存对齐和 size class 影响”,不要把某个固定公式当语言规范。
2.5 为什么 append 会修改另一个切片?
因为两个切片可能共享底层数组:
a := []int{1, 2, 3, 4}
b := a[:2]
b = append(b, 99)
fmt.Println(a) // [1 2 99 4]b 的容量仍覆盖 a[2],append 复用了原数组。
需要隔离时可以:
b := append([]int(nil), a[:2]...)或:
b := slices.Clone(a[:2])2.6 如何正确复制 slice?
dst := make([]T, len(src))
copy(dst, src)copy 复制元素数量为 min(len(dst), len(src)),源和目标可以重叠。
append([]T(nil), src...) 也能浅拷贝。注意:如果元素本身是指针、map、slice 或包含引用字段,这只是浅拷贝。
2.7 nil slice 和空 slice 有什么区别?
var a []int // nil slice
b := []int{} // 非 nil 空 slice
c := make([]int, 0)共同点:
len和cap都可以是 0。- 都可以 append。
- range 都不会执行。
区别:
a == nil为 true,b == nil为 false。- 某些 JSON 编码结果可能不同:nil slice 常编码为
null,空 slice 常编码为[]。
对外 API 若要求稳定返回数组,应初始化为空切片或使用自定义编码逻辑。
2.8 删除 slice 元素有哪些写法?
保持顺序:
s = append(s[:i], s[i+1:]...)不保持顺序,O(1) 覆盖:
s[i] = s[len(s)-1]
s = s[:len(s)-1]如果元素包含指针且切片生命周期很长,删除后应清零尾部引用,帮助 GC:
copy(s[i:], s[i+1:])
var zero T
s[len(s)-1] = zero
s = s[:len(s)-1]2.9 什么是 slice 内存滞留?
一个很小的子切片仍可能引用一个很大的底层数组,导致大数组无法被 GC 回收:
func firstKB(big []byte) []byte {
return big[:1024]
}如果 big 很大且返回值长期存活,应复制所需内容:
func firstKB(big []byte) []byte {
return append([]byte(nil), big[:1024]...)
}2.10 字符串的底层结构是什么?
概念上字符串由数据指针和长度组成:
type stringHeader struct {
Data unsafe.Pointer
Len int
}Go 字符串是只读字节序列,不保证内容一定是合法 UTF-8。字符串不可变意味着不能直接修改某个字节。
2.11 len(s) 为什么不一定是字符数?
len(string) 返回字节数。
s := "你好"
fmt.Println(len(s)) // 6
fmt.Println(utf8.RuneCountInString(s)) // 2for range 遍历字符串时,索引是每个 rune 起始字节偏移,值是解码后的 rune:
for i, r := range "A你" {
fmt.Printf("%d %c\n", i, r)
}
// 0 A
// 1 你2.12 string 与 []byte 转换会发生什么?
常规转换一般需要分配并复制数据,因为:
- string 不可变。
[]byte可变。
编译器在特定只读、短生命周期场景可能做优化,但业务代码不要依赖这个细节。
需要高效拼接字符串时,优先考虑:
strings.Builderbytes.Buffer- 已知总长度时调用
Grow
避免循环中反复 s += part 产生大量临时对象。
继续阅读
- 排错
- 排错
- 排错