Skip to content

Go 后端面试复盘:语言、并发与服务实践 · 2. 数组、切片与字符串 ​

返回分篇目录

前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。

2. 数组、切片与字符串 ​

2.1 数组和切片有什么区别? ​

对比项数组切片
类型长度属于类型一部分动态视图
赋值复制全部元素复制切片头
长度固定可变
能否比较元素可比较时可以不能,除与 nil 比较
常见用途固定大小数据、值语义日常集合处理

[3]int 和 [4]int 是不同类型。

2.2 slice 的底层结构是什么? ​

概念上,slice 由三个字段构成:

go
type sliceHeader struct {
	Data unsafe.Pointer
	Len  int
	Cap  int
}
  • Data:指向底层数组起始位置。
  • Len:当前可访问元素数。
  • Cap:从当前起始位置到底层数组末尾的容量。

不要在业务代码中直接操作 reflect.SliceHeader。它用于解释模型,不代表可以安全绕过类型系统。

2.3 len 和 cap 有什么区别? ​

  • len(s):当前切片中可直接访问的元素数量。
  • cap(s):从切片起点到其底层数组末端能容纳的元素数量。
go
a := []int{0, 1, 2, 3, 4}
s := a[1:3]
// s = [1,2], len=2, cap=4

完整切片表达式可以限制容量:

go
s := a[1:3:3] // len=2, cap=2

这样对 s 执行 append 时更容易触发新数组分配,避免覆盖原数组后续元素。

2.4 append 一定会分配新数组吗? ​

不一定。

  • 若剩余容量足够,append 复用原底层数组。
  • 若容量不足,运行时申请更大的数组,复制旧元素,并返回新的切片。

因此必须接收 append 返回值:

go
s = append(s, v)

容量增长是运行时实现细节。面试中可以说“小切片常见近似倍增,大切片增长更平缓,并受元素大小、内存对齐和 size class 影响”,不要把某个固定公式当语言规范。

2.5 为什么 append 会修改另一个切片? ​

因为两个切片可能共享底层数组:

go
a := []int{1, 2, 3, 4}
b := a[:2]
b = append(b, 99)
fmt.Println(a) // [1 2 99 4]

b 的容量仍覆盖 a[2],append 复用了原数组。

需要隔离时可以:

go
b := append([]int(nil), a[:2]...)

或:

go
b := slices.Clone(a[:2])

2.6 如何正确复制 slice? ​

go
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 有什么区别? ​

go
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 元素有哪些写法? ​

保持顺序:

go
s = append(s[:i], s[i+1:]...)

不保持顺序,O(1) 覆盖:

go
s[i] = s[len(s)-1]
s = s[:len(s)-1]

如果元素包含指针且切片生命周期很长,删除后应清零尾部引用,帮助 GC:

go
copy(s[i:], s[i+1:])
var zero T
s[len(s)-1] = zero
s = s[:len(s)-1]

2.9 什么是 slice 内存滞留? ​

一个很小的子切片仍可能引用一个很大的底层数组,导致大数组无法被 GC 回收:

go
func firstKB(big []byte) []byte {
	return big[:1024]
}

如果 big 很大且返回值长期存活,应复制所需内容:

go
func firstKB(big []byte) []byte {
	return append([]byte(nil), big[:1024]...)
}

2.10 字符串的底层结构是什么? ​

概念上字符串由数据指针和长度组成:

go
type stringHeader struct {
	Data unsafe.Pointer
	Len  int
}

Go 字符串是只读字节序列,不保证内容一定是合法 UTF-8。字符串不可变意味着不能直接修改某个字节。

2.11 len(s) 为什么不一定是字符数? ​

len(string) 返回字节数。

go
s := "你好"
fmt.Println(len(s))                    // 6
fmt.Println(utf8.RuneCountInString(s)) // 2

for range 遍历字符串时,索引是每个 rune 起始字节偏移,值是解码后的 rune:

go
for i, r := range "A你" {
	fmt.Printf("%d %c\n", i, r)
}
// 0 A
// 1 你

2.12 string 与 []byte 转换会发生什么? ​

常规转换一般需要分配并复制数据,因为:

  • string 不可变。
  • []byte 可变。

编译器在特定只读、短生命周期场景可能做优化,但业务代码不要依赖这个细节。

需要高效拼接字符串时,优先考虑:

  • strings.Builder
  • bytes.Buffer
  • 已知总长度时调用 Grow

避免循环中反复 s += part 产生大量临时对象。


继续阅读