Go数组与切片:理解顺序数据结构
顺序存储的数据结构,是日常开发里最常见的一类。Go 同时提供了数组和切片,但真正高频使用的往往是切片。很多刚接触 Go 的人会疑惑:既然切片这么常用,为什么还要保留数组?
答案就在于,数组和切片虽然相关,却并不是同一个东西。理解它们之间的关系,是后面掌握函数参数、底层数据共享和性能行为的关键一步。
一、这一篇主要学什么
这一篇的重点是:
- 数组和值语义是什么;
- 切片为什么更常用;
len、cap、append这些操作到底意味着什么;- 切片为什么会共享底层数组;
- 扩容、拷贝和修改行为有哪些容易踩坑的地方。
二、数组和切片为什么不是一回事
很多人一开始会把切片当成“动态数组”,这个理解不算错,但不够准确。更严谨的说法是:
- 数组是固定长度的值类型;
- 切片是对底层数组的一层轻量级描述。
数组长度是类型的一部分,所以 [3]int 和 [5]int 是完全不同的类型。数组变量在赋值和传参时,会复制整份数据。
切片则更像一个描述结构,通常包含三部分信息:
- 指向底层数组某个位置的指针;
- 当前长度
len; - 当前容量
cap。
也正因为如此,切片在函数传递和动态扩容场景里更实用,所以工作中大多数集合场景都会优先使用切片。
三、数组的核心特点是什么
数组最大的特点是:
- 长度固定;
- 属于值类型;
- 内存布局连续;
- 适合表示长度确定的数据。
例如:
1 | var nums [3]int |
这里的 [3]int 本身就是类型的一部分。数组的这个特性使它在某些底层场景、固定大小缓冲区场景里仍然有价值,但在日常业务开发里不如切片灵活。
很多初学者对数组不熟,是因为它在 Go 业务代码里的存在感确实不如切片强。但如果不了解数组,后面就很难真正理解切片的底层行为。
四、切片的本质到底是什么
切片不是数组本身,而是“引用底层数组的一段视图”。
例如:
1 | arr := [5]int{1, 2, 3, 4, 5} |
这里的 s 并没有复制出一份全新数组,而是指向 arr 中一段连续区域。此时:
len(s)是 3;cap(s)通常从切片起点算到原底层数组末尾。
这正是切片最重要、也最容易让人误判的地方:多个切片很可能共享同一个底层数组。
五、为什么 len 和 cap 要分开设计
len 表示当前切片里“可直接看到和使用”的元素个数,cap 表示从切片起点到底层数组末尾这段空间的容量。
这两个值分开存在,是为了支持切片的灵活增长。
len决定你当前能访问哪些元素;cap决定你还有多少底层空间可用于扩展。
如果你只知道长度,而不知道底层剩余空间,就很难支持高效的追加操作。这也是 append 能工作得比较自然的一个基础。
六、append 到底做了什么
append 表面看只是“往切片后面加元素”,但它背后其实有两种情况:
1. 容量足够
如果底层数组还有剩余空间,append 可能直接写入原底层数组,不会分配新数组。
2. 容量不够
如果底层空间不足,运行时通常会分配一块更大的新数组,把原有数据拷贝过去,再返回新的切片。
这意味着:append 之后返回的切片,底层数组未必还是原来那块。也正因为如此,append 的返回值通常必须接住。
很多初学者容易写出这种代码:
1 | append(nums, 4) |
但没有把结果重新赋值回去,这样往往不会得到预期效果。更稳妥的写法是:
1 | nums = append(nums, 4) |
七、一个基础示例
1 | package main |
表面上看,这段代码只是追加元素并打印长度和容量;但真正值得理解的是:
- 切片为什么能扩容;
- 扩容之后底层是否还是原来的数组;
- 共享数据时会不会互相影响。
只有把这些问题想明白,你后面在函数传参、批量处理、JSON 解析和标准库使用中,才不容易踩坑。
八、切片共享底层数组会带来什么影响
这是 Go 切片最需要反复强调的点。
如果两个切片共享同一个底层数组,那么其中一个切片修改元素,另一个切片看到的内容也可能跟着变化。例如:
- 一个函数拿到切片后修改了元素;
- 调用方发现原数据也被改了;
- 多个子切片操作互相污染。
这类问题在小代码里可能只是“有点怪”,但在真实项目里会直接造成隐蔽 bug。
所以你要建立一个基本意识:切片传参虽然是值传递,但这个值里包含了指向底层数组的描述信息,因此表现上会像“共享数据”。
九、函数传参时为什么总让人误解
Go 只有值传递,没有传统意义上的引用传递。切片传参时,传递的是“切片头部信息的副本”,不是整个底层数组的拷贝。
这会带来两个现象:
- 在函数里修改切片元素,通常会影响外部底层数组;
- 在函数里对切片做
append,如果触发扩容,新的底层数组可能只在函数内生效,外部未必感知得到。
这也是为什么很多人会觉得切片“像引用又不是引用”。更准确地说,切片是带有共享底层数据语义的值类型描述结构。
十、学习数组与切片时常见的注意事项
1. 不要把数组和切片混为一谈
数组是固定长度的值类型,切片是动态视图结构。两者语义不同,不能因为语法相似就当成一种东西。
2. append 后要接住返回值
因为 append 可能返回新的底层存储,忽略返回值很容易出错。
3. 警惕底层数组共享
一个切片改了元素,另一个相关切片可能被连带影响。这是 Go 开发里非常常见的坑。
4. 需要隔离数据时要主动 copy
如果你不希望两个切片共享底层数据,就应该主动复制,而不是默认它们彼此独立。
5. 不要只看能不能跑,要关注内存和语义
切片的问题很多时候不是语法错误,而是数据共享、扩容时机和内存行为没想明白。
十一、学习建议
- 区分数组和切片,不要把它们混为一谈;
- 练习时多打印
len和cap; - 当切片作为函数参数传递时,注意修改行为是否会影响调用方;
- 主动做几个“子切片共享底层数组”的实验;
- 学会在需要隔离数据时使用拷贝思路。
总结
数组和切片是 Go 里非常基础、也非常重要的内容。切片的高频使用,决定了你后面读标准库、写接口返回值、处理 JSON 数组时都会频繁遇到它。
真正要掌握这部分,不只是会写 []int{} 和 append,而是要理解:
- 数组是固定长度的值类型;
- 切片是对底层数组的描述;
len和cap分别描述可见长度与底层容量;append可能复用原数组,也可能触发新分配;- 切片共享底层数组,是很多问题的根源。
把这些底层逻辑想清楚,后面再学 map、函数、并发、标准库和框架代码时,你会明显轻松很多。
参考资源: