go中数组长度编译期固定且为值类型,切片则通过len/cap和底层数组实现动态特性;操作时需注意截取后cap变化、避免隐式共享、预估容量防扩容、必要时显式拷贝。

数组长度在编译期固定,len 返回的是常量
Go 中的数组是值类型,声明时必须指定长度,比如 [3]int 和 [4]int 是完全不同的类型。这意味着你不能把一个 [3]int 直接赋给 [4]int 变量,连函数参数都不兼容。
常见错误是误以为 len(arr) 可以动态变化——其实它在编译时就确定了,且不能被修改。如果你看到代码里反复用 for i := 0; i 遍历数组,没问题;但若后续想“扩容”,这条路走不通。
- 数组适合已知大小、生命周期短、需栈上分配的场景(如哈希种子、小缓冲区)
- 传数组进函数会复制整个底层数组,
[1024]byte传参开销明显 - 想避免复制?改用指针:
func f(p *[3]int),但这时你操作的是原数组,不是副本
切片共享底层数组,append 可能引发意外覆盖
切片本质是三元组:ptr(指向底层数组首地址)、len(当前长度)、cap(容量)。append 在 len 时直接复用底层数组,不分配新内存;一旦超出 <code>cap,则分配新底层数组并拷贝数据。
这导致一个经典陷阱:多个切片共用同一底层数组,其中一个 append 后写入,可能覆盖另一个切片的数据。
立即学习“go语言免费学习笔记(深入)”;
data := [5]int{0, 1, 2, 3, 4}
s1 := data[0:2] // [0 1], cap=5
s2 := data[3:4] // [3], cap=2
s1 = append(s1, 99) // 触发扩容?不,cap 还够(5 > 3),写入位置是 data[2] → 原来 data[2]==2 被改成 99
// 此时 s2[0] 仍是 3,但 data[2] 已变 —— 如果其他逻辑依赖 data[2],就出问题了
- 安全做法:需要独立数据时,显式拷贝:
newSlice := append([]int(nil), oldSlice...) -
append不保证返回切片与原切片是否共用底层数组,永远不要假设 - 查看容量用
cap(s),别只看len(s);调试时打印&s[0]可验证地址是否一致
用 make([]T, len, cap) 精确控制初始容量
很多性能问题源于频繁扩容。比如循环中不断 append 却没预估长度,会导致多次底层数组重分配和拷贝。
make([]int, 0, 100) 创建一个长度为 0、容量为 100 的切片,后续 100 次 append 都不会触发扩容。
- 如果已知最终长度,直接
make([]T, n)更简洁(此时 len == cap) - 如果长度波动大但有上限,用
make([]T, 0, maxEstimate)+append更省心 - 注意:
make([]T, n)会初始化元素(零值),而make([]T, 0, n)不初始化底层数组,仅预留空间 - 对字符串拼接等场景,优先考虑
strings.Builder,它内部也用类似机制管理缓冲区
切片截取时,cap 会按底层数组剩余长度计算
这是最易被忽略的隐式行为:s[low:high] 截取后,新切片的 cap 变为 cap(s) - low(而不是 high - low)。
a := [5]int{0,1,2,3,4}
s := a[:] // len=5, cap=5
t := s[2:3] // len=1, cap=3 ← 注意:从索引2开始,底层数组还剩 3 个元素(索引2/3/4)
u := append(t, 5, 6) // u = [2 5 6],没扩容,因为 cap(t)==3 允许追加两个元素
- 这意味着通过截取可“暴露”原底层数组更多空间,但也可能让调用方意外写满整块内存
- 若要彻底隔离,必须拷贝:
copy(dst, src)或append([]T(nil), src...) - 函数接收切片参数时,除非明确文档说明会复用底层数组,否则调用方应默认其行为不可见
切片的 cap 行为不是 bug,是设计选择;但它的隐蔽性容易在跨包、长期维护或并发写入时埋下隐患。真正难的不是记住规则,而是在封装接口时主动切断底层数组泄露路径。










