bigywater.tech
Go · 20

数组是值类型:传参、赋值、比较时发生了什么

数组在 Go 里是值,不是指针。这带来几个直接后果:赋值与传参都会整块拷贝,range 数组会先拷贝一份,数组的长度是类型的一部分。这篇逐项用真实输出验证,并量一量传大数组的拷贝代价。

24 字节
[3]int 的大小;[4096]int 是 32768 字节,传参时整块拷贝
v 是 1、2、3
range 数组时循环里改 arr[2],v 仍是旧值;range arr[:] 则看到 100
2039 ns vs 1200 ns
传 32 KiB 数组按值与按指针

值语义

实测 · 本机执行输出
  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。

range 与拷贝

实测 · 本机执行输出
  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(本文没有运行)。

分步演示

数组的值语义
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。基准测试 7 轮取中位数,函数都 //go:noinline;编译错误是 go build 的原样输出
    • 没有验证:函数内联后传大数组是否还拷贝、range 数组的拷贝是否会被编译器优化掉、amd64 上的耗时、多维数组的内存布局、slices.Equal 的行为
    京ICP备2026067502号-1京公网安备11010502063487号