Go Slice 和 Array 的区别
初学 Go 的人常把数组和切片当成一回事,直到遇到「改了一处,另一处跟着变」的问题。区别要从内存模型说起。
数组:长度是类型的一部分
var a [3]int // 类型是 [3]int
var b [4]int // 类型是 [4]int,和 [3]int 是两个不同类型
// b = a // 编译错误:不能使用 a (类型 [3]int) 作为 [4]int
数组是值类型:赋值和传参都会完整复制一份。
func modify(arr [3]int) {
arr[0] = 999
}
func main() {
a := [3]int{1, 2, 3}
modify(a)
fmt.Println(a) // [1 2 3],原数组没变
}
因为长度写在类型里,数组实际用得不多——绝大多数场景用切片。
切片:一个描述符
切片本身很小,它只是一个描述「底层数组的哪一段」的结构体,包含三个字段:指针、len(当前长度)、cap(容量)。
nums := []int{1, 2, 3}
fmt.Println(len(nums), cap(nums)) // 3 3
对已有切片再切片,得到的新切片共享同一个底层数组:
nums := []int{1, 2, 3, 4, 5}
part := nums[1:3] // [2 3],len=2,cap=4
part[0] = 99
fmt.Println(nums) // [1 99 3 4 5] 原切片被改了
这就是「改了一处、另一处跟着变」的来源。需要独立副本时用 copy:
independent := make([]int, len(part))
copy(independent, part)
append 与容量
append 在容量够时写入底层数组,容量不够时分配新数组并复制:
s := make([]int, 0, 2) // len=0,cap=2
s = append(s, 1, 2)
fmt.Println(len(s), cap(s)) // 2 2
s = append(s, 3) // 触发扩容
fmt.Println(len(s), cap(s)) // 3 4(容量通常翻倍)
因此切片增长后,与旧切片可能不再共享底层数组:
base := []int{1, 2, 3}
a := base[:2] // len=2, cap=3
b := append(a, 99) // 容量够,直接写入底层数组
fmt.Println(base) // [1 2 99] 原切片被覆盖
结论:永远不要假设 append 之后原切片不变。要独立结果就显式 copy,或者用 slices.Clone。
nil 切片
var s []int // nil 切片,len=0,cap=0
fmt.Println(s == nil, len(s)) // true 0
s = append(s, 1) // 合法,append 会分配底层数组
nil 切片可以直接 append 和 len,不需要先 make;只有在要按索引写入时才需要先分配:
s := make([]int, 3) // [0 0 0],len=3
s[0] = 1
一个常见 bug:for range 里保存元素地址
words := []string{"a", "b", "c"}
ptrs := []*string{}
for _, w := range words {
ptrs = append(ptrs, &w) // 错误:w 是每次迭代复用的副本
}
for _, p := range ptrs {
fmt.Print(*p) // 输出 c c c
}
循环变量在 Go 1.22 之后每次迭代新建,但这里取的始终是同一个 w 的地址。正确写法取切片元素地址:
ptrs = append(ptrs, &words[i])
小结
- 数组是值类型,长度属于类型;切片是描述符,日常用切片。
- 再切片共享底层数组,独立副本用
copy或slices.Clone。 append可能原地写也可能重新分配,不要依赖它对原切片的影响。- 需要按索引写入时先
make,只追加可以直接append到 nil 切片。