Go Slice 和 Array 的区别

Go 1.24

初学 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 切片。

下一步