切片头是 {指针, 长度, 容量} 三个字,拷贝切片只拷贝这三个字,底层数组是共享的。这篇用真实输出验证几个最容易出事的场景:子切片被写、容量够时 append 悄悄改原切片、函数里 append、复用缓冲区。
a = [1 2 3 4 5](len 5 cap 5);b := a[1:3] = [2 3](len 2 cap 4);b 的数据地址 - a 的数据地址 = 8 字节 b[0] = 99 之后:a = [1 99 3 4 5](改到了 a) b = append(b, 100)(容量够,原地写):b = [99 3 100],a = [1 99 3 100 5](a[3] 被悄悄改成 100);b 仍与 a 共用数组?true 再追加 3 个(超过 cap 4,换了新数组):b = [99 3 100 200 300 400];共用数组?false;a 仍是 [1 99 3 100 5]
a[1:3] 的数据指针指向 a 的第 2 个元素,容量一直延伸到 a 的末尾,所以对它 append 时,只要不超过容量,就会写进 a 后面的位置。这是 Go 里最典型的“别名”意外:代码看起来只改了 b,a 却变了。超过容量时运行时换一个新数组,别名关系才断开,而从外面看不出哪次 append 会换数组。
c := a[1:3:3] = [2 3](len 2 cap 2);c2 := append(c, 100) → c2 = [2 3 100],a 仍是 [1 2 3 4 5];c2 的数据地址 == c 的数据地址?false d := slices.Clone(a[1:3]):d[0] = -7 之后 d = [-7 3],a = [1 2 3 4 5];d 的 cap = 2 e := make([]int, 2); copy(e, a[1:3]) 拷贝了 2 个:e[1] = -8 之后 e = [2 -8],a = [1 2 3 4 5]
三下标切片 a[low:high:max] 把容量限制在 max - low,所以之后的 append 一定换新数组,原切片不会被写。要独立的副本就用 slices.Clone 或 copy 到新切片。两者都拷贝了元素;如果元素本身是指针、切片或 map,拷贝的只是它们的头,里面的内容仍是共享的(本文没有展示这一点)。
fill(x) 里写 s[0] = -1:调用者的 x = [-1 2 3](元素被改) appendInside(x) 里 s = append(s, 9)(cap 10,容量够):调用者的 x = [-1 2 3](len 仍是 3);但 x[:4] = [-1 2 3 9](底层数组第 4 个位置已经被写成 9) 同样的调用,y 的 cap 只有 3(要换数组):y[:3] = [0 0 0],y 的 cap 仍为 3,数组没有被写
切片按值传递,所以函数里 s = append(s, x) 不会改变调用者的长度。但如果容量够,元素已经写进了共享的底层数组:调用者的 x 看不到(长度没变),x[:cap(x)] 能看到。这是“append 不能直接用返回值以外的方式”这条规则背后的原因:要把新切片返回或通过指针传回。
读了 alpha、beta、gamma 三个词,每次都读进同一个 buf:
saved[0] = "gamma"(直接存 buf[:n]);savedOK[0] = "alpha"(存 slices.Clone)
saved[1] = "gamm"(直接存 buf[:n]);savedOK[1] = "beta"(存 slices.Clone)
saved[2] = "gamma"(直接存 buf[:n]);savedOK[2] = "gamma"(存 slices.Clone)
saved[0] 与 saved[2] 的数据地址相同?true读取循环里复用同一块缓冲区是常见的性能优化,但要保存数据就必须拷贝。标准库里返回内部缓冲区切片的方法在文档里都有提醒:go doc bufio.Scanner.Bytes 写的是“The underlying array may point to data that will be overwritten by a subsequent call to Scan”,bufio.Reader.Peek 写的是“The bytes stop being valid at the next read call”(这两段是我用 go doc 读到的,没有运行这两个方法)。这里用一个手写的 buf 重现同样的后果:三个存下来的切片指向同一个数组。