跳转至

数组与切片:固定大小的值,会变长的引用

这一章讲 Go 最常用的容器,以及一对必须分清的兄弟: 数组(array)定长,赋值即复制;切片(slice)变长,赋值只传引用。 混淆它俩是 Go 新手最高发的事故。主线例子会带你亲眼看一次共享引用的效果: 改一下数组,所有从它身上切出来的切片全部跟着变。

1. 数组:定长、从零号位开始、整体复制

书里给的定义:数组是一组同类型元素的集合,每个元素靠「索引」访问,索引从 0 开始; Go 的数组长度固定,而且是值类型——赋给另一个变量、或者传给函数, 复制的是整个数组,不是它的引用1。

声明与初始化,照原书全部写法2:

var arr [5]int                  // 定长 5 的整数数组,元素全是零值 → [0 0 0 0 0]
arr := [5]int{1, 2, 3}          // 给 3 个,剩下补零值 → [1 2 3 0 0]
arr := [...]int{1, 4, 7, 2}     // [...] 让编译器按初始化值的个数定长 → 长度 4

[...] 里的三个点叫省略号(ellipsis):长度交给编译器数3。 另外两种「只占坑」的初始化:arr[1] = 2; arr[3] = 4 单独喂某些索引, 其余保持零值 → [0 2 0 4 0]2。

为什么需要数组

书里的小例子很有说服力:不用容器存四个学生名字,你得写 name1、name2、name3、name4 四个变量—— 名字的数量在写代码那一刻就被定死了,运行时想加第五个?没门4。 数组把「一批同类的东西」收进一个名字里,这是所有容器的起点。

数组的三个日常操作

操作 写法 备注
长度 len(arr) 多维数组 len 返回最外层长度(二维数组的「行数」)5
比较 arr1 == arr2 类型、长度、每个元素都相同才 true6
复制 arr2 := arr1 整份复制;改 arr2 不影响 arr17

比较的例子值得看一眼,因为它藏着零值的影子: [4]int{1,4,7} 和 [4]int{1,4,7,0} 是相等的—— 前者最后一个元素没写,自动补了零值 06。

复制(赋值)的语义用一组数字走一遍(arr1 := [4]int{1,4,7,2}):

  arr2 := arr1        // 复制:[1 4 7 2] 整份抄给 arr2
  arr2[2] = 90        // 只改 arr2 的第三格
  arr1 → [1 4 7 2]    // 原样不动
  arr2 → [1 4 90 2]   // 只有副本变了

图说:数组像一叠复印好的文件,谁拿走一份都是全新的7。

遍历:两种 for

for i := 0; i < len(arr); i++ {   // 经典三件套
    sum += arr[i]
}

for index, value := range arr {   // range:一趟给你索引和值
    sum += value
}

range 一个容器,固定吐两个值:索引和元素。 只想要值? 把索引用空白标识符扔掉:for _, value := range arr—— 这是 Go 代码里出现频率最高的循环写法8。

数组的判决书

书里给数组列了四条限制,加起来就是切片的出生证明9:

  1. 长度在编译期定死,运行时不能追加;
  2. 不能用变量定义长度——[size]int 里 size 是变量直接报错;
  3. 越界访问/写入,编译器或运行时报 out of bound;
  4. 作为函数参数,整个数组被复制,大数组白白浪费内存和时间。

实战结论:Go 代码里几乎不写数组,切片才是默认容器。 数组活着的意义, 是作为切片的「底层货架」存在。

2. 切片:数组上的一个窗口

切片是数组的一个片段:自己不存数据,只是指向某段数组的「窗口」。 窗口有三个参数,记住这「三件套」,切片的行为全能推出来:

  ┌─────────────────────────────────────────────┐
  │  底层数组:  [1  2  3  4  5  6  7  8  9  10]   │
  │              ▲              ▲                 │
  │              │              │                 │
  │  slice1 = arrayval[2:5]    │                 │
  │     指针 ──→ 指向元素 3      │                 │
  │     长度 len = 3  (3,4,5)   │                 │
  │     容量 cap = 8  (从 3 到数组末尾,共 8 格)  │
  └─────────────────────────────────────────────┘

  图说:切片 = 指针 + 长度 + 容量。长度是窗口里现在有几格,
  容量是底层货架上从窗口开头到货架末尾还剩几格。

长度和容量的区别是本章最要紧的概念,书里的话原样接住: 长度是切片引用到的元素个数;容量是「从切片的第一个元素算起, 底层数组到末尾还剩多少个位置」——也就是这个窗口最多还能往后扩多少10。 cap 函数取容量:len = 3、cap = 8 的例子就来自 arrayval[2:5] 对长度 10 的数组11。

主线例子:两个切片,一条数据,一改全变

这是本章的主线,照原书完整走一遍12:

arrayval := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

slice1 := arrayval[1:8]   // 索引 1 到 7 → [2 3 4 5 6 7 8],len 7
slice2 := slice1[:6]      // 在切片上再切,取到第 6 格 → [2 3 4 5 6 7]

// 关键一步:动底层数组
arrayval[2] = 90

三个变量此刻的样子:

  arrayval → [1 2 90 4 5 6 7 8 9 10]    ← 原数组的 3 变成了 90
  slice1   → [2 90 4 5 6 7 8]           ← 跟着变!
  slice2   → [2 90 4 5 6 7]             ← 也跟着变!

图说:切片没有自己的数据,改数组就是改它们;反过来,通过切片改元素, 数组上也立刻可见——书里双向演示过(slice1[0] = 178 之后数组第一个元素变 178)13。

low:high 的切法:取 low 到 high-1,左闭右开;两个边界都能省略, 默认取 0 和数组长度,[:] 就是整个数组12。

判断(我们的,不是书里的): 「切片共享底层数组」是 Go 最高发的隐蔽 bug 源: 函数收到一个切片、往里 append 或改元素,调用方的数据悄悄变了。 防御姿势只有两个:要么函数里先 copy 一份再动手, 要么在注释里写明「会修改传入切片的元素」。 如果错,会错在: 对小函数和内部代码,共享恰恰是高效和简洁的来源, 一刀切地防御反而臃肿。这是「知道风险之后的取舍」,不是绝对禁令。

3. make 与 append:切片的出生证和生长剂

切片的三种出生方式:

方式 例子 结果
字面量 s := []int{1, 2, 3} 先造一个数组,再返回它的切片
从数组切 s := arr[1:4] 窗口指向已有数组
make s := make([]int, 5, 10) 造一个长度 5、容量 10 的切片

make 专门用来分配切片(还有 map 和 channel): make([]int, 长度, 容量),容量可以省略,省略时容量等于长度14。 用 make 造出来的切片,元素全是零值:make([]int, 2, 4) → [0 0],len 2, cap 415。

一个容易踩的坑书里特意点了:make 造的长度是 2 的切片, 直接写 slice1[2] = 78 会报错——第 2 格还在容量里、不在长度里, 得先让它「长」进来15。

append:唯一的生长方式

append 在切片末尾加元素,返回一个新的切片(必须接回返回值):

slice1 = append(slice1, 345, 646, 0)   // 一次加多个也行
slice1 = append(slice1, slice2...)      // ... 把另一个切片「拆散」加进来

三个细节,原书都有16:

  • 容量不够时,append 会悄悄换货架:申请一块更大的底层数组,把旧数据全搬过去, 再追加——旧的切片还指着旧货架,互不相干;
  • 往 nil 切片上 append 是合法的:var s []int 是 nil,s = append(s, 45) → [45];
  • 容量够的时候 append 不搬家,直接往后写——这时它和旧切片还共享着数组。

「扩容量」的手动版(老写法):造一个更大的切片、copy 过去、再赋回来; 书里也提了一句新写法:标准库 slices 包的 Grow 函数一行搞定17。

copy:按「较短的算」

n := copy(dst, src)   // 把 src 的元素拷进 dst,返回实际拷了几个

copy 不增长目标切片,只覆盖重叠的那一段;拷贝个数 = min(len(dst), len(src))。 目标比源长,尾部保留原样;目标比源短,装不下的不要; 目标是空的,什么都不发生(返回 0)18。

4. 切片操作工具箱

原书后半章是一组标准库操作,压缩成速查表:

想干什么 用什么 例子(照原书)
排序(升序) sort.Ints/Strings/Float64s [23 0 105 58] → [0 23 58 105]19
自定义排序 slices.SortFunc(带比较函数) 见原文20
判等 reflect.DeepEqual(s1, s2) 类型+元素全同才 true21
查找 sort.SearchInts(s, x) 二分查找,要求切片已升序;找不到时返回「该插入的位置」:SearchInts([0 9 16 105 345], 78) → 322
反转 sort.Sort(sort.Reverse(sort.IntSlice(s))) [0 888 9 16 105 345] → [888 345 105 16 9 0]23
追加 append 见上节

DeepEqual 的例子值得停一下:make([]int, 4) 造出来的 [0 0 0 0], 和字面量 []int{23, 0, 105, 0} 比是不等的(元素不同), 而和「前三个元素手动设成 23,0,105 的」切片是相等的21。

SearchInts 的第二个返回值语义特别容易误用:它不一定找到了元素! 返回的是「如果要把目标插进来、该插在哪个位置」。 确认「真的存在」还得再比一下该位置的元素(补充,不在书里,来自通用知识)。

二维切片与结构体切片

二维数组 arr := [3][2]int{{1,2},{4,5},{7,8}} 是「数组的数组」,每行定长24。 二维切片 [][]int{{1,2,78,54},{0,3,4},{5,6}} 是「切片的切片」,每行可以长短不一25—— 这是切片比数组灵活的一个实在场景。

切片也能装结构体:[]struct{ roll_no int; name string; present bool }, 输出形如 [{1 Rahul true} {3 Akansha false}]——struct 是什么、 第 11 章专讲,这里只看到「切片能装任何同类型的东西」26。

5. 作者的判断与证据

书里的说法 性质
「数组的这些限制催生了切片」 事实+动机叙述,属实
「切片比数组强大,能做多种操作」 属实,但「强大」附带了共享引用的风险,书里未强调
「sort 的包总是升序排序」 属实;反转要靠 Reverse 包一层23

6. 边界与局限

  • 切片之间不能用 == 比较(语法禁止,只能 DeepEqual)——书里只演示了 DeepEqual, 没解释为什么;原因:切片含指针,== 比较指针还是比数据没有直观答案;
  • append 的「容量不够就搬家」没有给具体增长策略(翻倍?1.25 倍?)—— 实现细节且随版本调整,不建议依赖(补充,不在书里);
  • 删除切片中间元素的惯用写法(append(s[:i], s[i+1:]...))原书没有讲, 属于常用补遗(补充,不在书里,来自通用知识);
  • slices 包(Go 1.21 引入)只在 Grow 一处被提到, 它的 Sort/Contains/Delete 等常用函数未展开。

7. 可带走的

  1. 数组:定长、索引从 0、值语义(赋值/传参 = 整份复制);
  2. 数组的四种初始化,[...] 让编译器数长度;零值自动补齐([1,4,7] == [1,4,7,0]);
  3. len 取长度;二维数组的 len 是行数;
  4. 切片 = 指针 + 长度 + 容量,是数组的窗口,不拥有数据;
  5. len 是窗口里的格子数,cap 是窗口最多能扩到的格子数;
  6. 改底层数组,所有切片跟着变——共享是特性,也是 bug 源;
  7. 出生三法:字面量、从数组切、make;nil 切片也能 append;
  8. append 必须接回返回值;容量不够时会换底层数组(旧切片不受影响);
  9. copy(dst, src) 按较短的长度拷,不增长目标;
  10. 排序用 sort.Ints/Strings/Float64s;判等用 reflect.DeepEqual; 查找用 sort.SearchInts(先排序,找不到时返回插入位置)。

8. 原文地图

主题 原书章 原文位置
数组定义:定长、值类型 Arrays and Slices text/16-fm-learning-go-programming.txt:79(搜「fixed length」)
初始化与零值补齐 Arrays and Slices text/16-fm-learning-go-programming.txt:160(搜「predefined values」) · text/16-fm-learning-go-programming.txt:220(搜「0 2 0 4 0」)
[...] 省略号定长 Arrays and Slices text/16-fm-learning-go-programming.txt:232(搜「ellipsis」)
数组的动机 Arrays and Slices text/16-fm-learning-go-programming.txt:280(搜「grow or shrink」)
len 与越界 Arrays and Slices text/16-fm-learning-go-programming.txt:379(搜「out of bound」) · text/16-fm-learning-go-programming.txt:415(搜「Length of 2D array」)
数组比较(零值补齐也相等) Arrays and Slices text/16-fm-learning-go-programming.txt:421(搜「same type and size」) · text/16-fm-learning-go-programming.txt:484(搜「arr2 and arr5 true」)
数组复制是整份拷贝 Arrays and Slices text/16-fm-learning-go-programming.txt:499(搜「behaves like a value」) · text/16-fm-learning-go-programming.txt:517(搜「arr2 is」)
数组传参与求和 Arrays and Slices text/16-fm-learning-go-programming.txt:544(搜「func sum」) · text/16-fm-learning-go-programming.txt:568(搜「sum of array elements is: 14」)
range 遍历与空白标识符 Arrays and Slices text/16-fm-learning-go-programming.txt:616(搜「range arr」) · text/16-fm-learning-go-programming.txt:667(搜「blank identifier」)
数组的四条限制 Arrays and Slices text/16-fm-learning-go-programming.txt:708(搜「invalid array length」)
切片定义与 nil 切片 Arrays and Slices text/16-fm-learning-go-programming.txt:729(搜「segment of an array」) · text/16-fm-learning-go-programming.txt:741(搜「nil slice」)
切片的切法 low:high Arrays and Slices text/16-fm-learning-go-programming.txt:774(搜「low indicates」) · text/16-fm-learning-go-programming.txt:822(搜「4 5 6 7」)
再切片不复制、共享底层数组 Arrays and Slices text/16-fm-learning-go-programming.txt:837(搜「does not copy」) · text/16-fm-learning-go-programming.txt:915(搜「2 90 4 5 6 7 8」)
三件套:指针/长度/容量 Arrays and Slices text/16-fm-learning-go-programming.txt:972(搜「pointer to an array segment」) · text/16-fm-learning-go-programming.txt:1075(搜「capacity of a slice」)
len=3 cap=8 的例子 Arrays and Slices text/16-fm-learning-go-programming.txt:1096(搜「arrayval[2:5]」) · text/16-fm-learning-go-programming.txt:1108(搜「capacity of slice 8」)
make 与「长度外写入报错」 Arrays and Slices text/16-fm-learning-go-programming.txt:1126(搜「capacity of the slice is optional」) · text/16-fm-learning-go-programming.txt:1156(搜「error as length」)
手动扩容与 slices.Grow Arrays and Slices text/16-fm-learning-go-programming.txt:1168(搜「increase the capacity」) · text/16-fm-learning-go-programming.txt:1207(搜「capacity of slice1: 14」) · text/16-fm-learning-go-programming.txt:1210(搜「slices.Grow」)
copy 的签名与「不增长」 Arrays and Slices text/16-fm-learning-go-programming.txt:1285(搜「destination, source」) · text/16-fm-learning-go-programming.txt:1456(搜「does not add any new value」)
sort 排序 Arrays and Slices text/16-fm-learning-go-programming.txt:1462(搜「ascending order」)
DeepEqual 判等 Arrays and Slices text/16-fm-learning-go-programming.txt:1546(搜「DeepEqual」) · text/16-fm-learning-go-programming.txt:1606(搜「slice1 equal to slice4: true」)
SearchInts 与插入位置 Arrays and Slices text/16-fm-learning-go-programming.txt:1612(搜「binary search」) · text/16-fm-learning-go-programming.txt:1645(搜「SearchInts(slice1, 78)」)
反转 Arrays and Slices text/16-fm-learning-go-programming.txt:1699(搜「sort.Reverse」)
append 与 ... 展开 Arrays and Slices text/16-fm-learning-go-programming.txt:1726(搜「new slice is allocated」) · text/16-fm-learning-go-programming.txt:1801(搜「slice2...」)

  1. 出处:「Arrays and Slices」第 79 段(text/16-fm-learning-go-programming.txt:79,搜「fixed length」)。 ↩

  2. 出处:「Arrays and Slices」第 163–229 段(text/16-fm-learning-go-programming.txt:160,搜「predefined values」;text/16-fm-learning-go-programming.txt:220,搜「0 2 0 4 0」)。 ↩↩

  3. 出处:「Arrays and Slices」第 232–241 段(text/16-fm-learning-go-programming.txt:232,搜「ellipsis」)。 ↩

  4. 出处:「Arrays and Slices」第 247–291 段(text/16-fm-learning-go-programming.txt:280,搜「grow or shrink」)。 ↩

  5. 出处:「Arrays and Slices」第 379–415 段(text/16-fm-learning-go-programming.txt:415,搜「Length of 2D array」)。 ↩

  6. 出处:「Arrays and Slices」第 421–493 段(text/16-fm-learning-go-programming.txt:484,搜「arr2 and arr5 true」)。 ↩↩

  7. 出处:「Arrays and Slices」第 499–517 段(text/16-fm-learning-go-programming.txt:499,搜「behaves like a value」;text/16-fm-learning-go-programming.txt:517,搜「arr2 is」)。 ↩↩

  8. 出处:「Arrays and Slices」第 604–702 段(text/16-fm-learning-go-programming.txt:616,搜「range arr」;text/16-fm-learning-go-programming.txt:667,搜「blank identifier」)。 ↩

  9. 出处:「Arrays and Slices」第 708 段(text/16-fm-learning-go-programming.txt:708,搜「invalid array length」)。 ↩

  10. 出处:「Arrays and Slices」第 1051–1081 段(text/16-fm-learning-go-programming.txt:1075,搜「capacity of a slice」)。 ↩

  11. 出处:「Arrays and Slices」第 1096–1108 段(text/16-fm-learning-go-programming.txt:1108,搜「capacity of slice 8」)。 ↩

  12. 出处:「Arrays and Slices」第 774–831 段(text/16-fm-learning-go-programming.txt:822,搜「4 5 6 7」)。 ↩↩

  13. 出处:「Arrays and Slices」第 837–918、988–1048 段(text/16-fm-learning-go-programming.txt:915,搜「2 90 4 5 6 7 8」;text/16-fm-learning-go-programming.txt:1042,搜「178」)。 ↩

  14. 出处:「Arrays and Slices」第 1117–1132 段(text/16-fm-learning-go-programming.txt:1126,搜「capacity of the slice is optional」)。 ↩

  15. 出处:「Arrays and Slices」第 1141–1162 段(text/16-fm-learning-go-programming.txt:1156,搜「error as length」)。 ↩↩

  16. 出处:「Arrays and Slices」第 1726–1783 段(text/16-fm-learning-go-programming.txt:1726,搜「new slice is allocated」;text/16-fm-learning-go-programming.txt:1783,搜「45 67」)。 ↩

  17. 出处:「Arrays and Slices」第 1168–1210 段(text/16-fm-learning-go-programming.txt:1207,搜「capacity of slice1: 14」;text/16-fm-learning-go-programming.txt:1210,搜「slices.Grow」)。 ↩

  18. 出处:「Arrays and Slices」第 1282–1456 段(text/16-fm-learning-go-programming.txt:1288,搜「minimum of the length」;text/16-fm-learning-go-programming.txt:1456,搜「does not add any new value」)。 ↩

  19. 出处:「Arrays and Slices」第 1462–1537 段(text/16-fm-learning-go-programming.txt:1528,搜「0 23 58 105」)。 ↩

  20. 出处:「Arrays and Slices」第 1540 段(text/16-fm-learning-go-programming.txt:1540,搜「SortFunc」)。 ↩

  21. 出处:「Arrays and Slices」第 1546–1606 段(text/16-fm-learning-go-programming.txt:1606,搜「slice1 equal to slice4: true」)。 ↩↩

  22. 出处:「Arrays and Slices」第 1612–1660 段(text/16-fm-learning-go-programming.txt:1612,搜「binary search」;text/16-fm-learning-go-programming.txt:1645,搜「SearchInts(slice1, 78)」)。 ↩

  23. 出处:「Arrays and Slices」第 1666–1720 段(text/16-fm-learning-go-programming.txt:1699,搜「sort.Reverse」)。 ↩↩

  24. 出处:「Arrays and Slices」第 295–367 段(text/16-fm-learning-go-programming.txt:331,搜「3x2」)。 ↩

  25. 出处:「Arrays and Slices」第 1213–1270 段(text/16-fm-learning-go-programming.txt:1258,搜「slice1 [[1 2 78 54]」)。 ↩

  26. 出处:「Arrays and Slices」第 921–963 段(text/16-fm-learning-go-programming.txt:963,搜「Rahul」)。 ↩