数组在 Go 里是值,不是指针。这带来几个直接后果:赋值与传参都会整块拷贝,range 数组会先拷贝一份,数组的长度是类型的一部分。这篇逐项用真实输出验证,并量一量传大数组的拷贝代价。
a := [3]int{1,2,3}; b := a; b[0] = 99:a = [1 2 3],b = [99 2 3](赋值拷贝了整个数组)
modify(a) 里写 a[0] = 100:调用者的 a = [1 2 3](传参拷贝了一份)
modifyPtr(&a) 里写 a[0] = 100:调用者的 a = [100 2 3](传的是指针)
unsafe.Sizeof:[3]int = 24,[0]int = 0,[4096]int = 32768,Pair{string, [2]int} = 32
结构体里的数组也是值:p2 := p1; p2.Pos[0] = 50:p1.Pos = [1 2],p2.Pos = [50 2];p1 == p2?false
[...]int{5: 1, 2, 3: 7} = [0 0 0 7 0 1 2](len 7,下标键可以乱序,后面的元素接着前一个下标)数组变量代表整块内存:b := a 与函数传参都拷贝所有元素,结构体里的数组字段也一样。要在函数里修改调用者的数组,必须传指针(或者传切片)。零长度数组 [0]int 大小是 0。
for i, v := range arr(循环里 arr[2] = 100): [0]=1 [1]=2 [2]=3;循环后 arr = [1 2 100] for i, v := range arr[:](循环里 arr[2] = 100): [0]=1 [1]=2 [2]=100 for i, v := range &arr(循环里 arr[2] = 100): [0]=1 [1]=2 [2]=100 for i := range arr(只要下标,不拷贝): [0]=1 [1]=2 [2]=100
for i, v := range arr 对数组 arr 会先求值并拷贝整个数组,循环里对 arr 的修改不会反映到 v 上;只写 for i := range arr 或者 range &arr、range arr[:] 就没有这个拷贝。语言规范里对此有明确说明,这里用实验验证了结果。编译器是否在某些情况下把拷贝优化掉,我没有检查。
[2]int{1,2} == [2]int{1,2} → true;map[[2]int]string 里查 {1,2} → "a",查 {9,9} → "";[2]int 与 [3]int 是不同类型数组的元素类型可比较时,数组就可比较,因此可以做 map 的键(map[[2]int]string)——这是用切片做不到的。长度是类型的一部分,[2]int 与 [3]int 是不同的类型,不能互相赋值或比较。
sumVal(big) 按值传 32 KiB 中位 2039.13 ns/op(最小 2037.81 / 最大 2087.18) sumPtr(&big) 传指针 中位 1199.74 ns/op(最小 1192.67 / 最大 1220.70) sumSlice(big[:]) 传切片 中位 1190.99 ns/op(最小 1188.81 / 最大 1199.30)
按值传 32 KiB 数组比传指针多约 840 ns,与求和本身的 1200 ns 相比是显著的。这只是 //go:noinline 函数的数字;如果函数被内联,编译器可能不需要真的拷贝,这种情况我没有测。
main.go:6:15: invalid operation: a == b (mismatched types [3]int and [4]int) main.go:8:10: invalid operation: s1 == s2 (slice can only be compared to nil) main.go:9:10: invalid map key type []int
三个典型的编译错误:长度不同的数组比较(mismatched types)、切片只能和 nil 比较、切片不能做 map 键。需要比较切片内容时用 slices.Equal(本文没有运行)。