Go 06:复制 slice 后为什么仍会改写任务输入
05 解决了单次文件输入的关闭责任;顺序批量入口则把多条文本放进 []string。现在需要从待处理批次中删除第一项,再追加一项。如果调用者也保留着原切片,处理函数返回后它的内容会不会变?一个看似无害的 input = input[1:],配合随后的 append,足以让仍由调用者持有的数组发生变化。
同一个累计工程为 --batch 增加 --drop-first 和 --append text,其他 --text、--file 输入不变。两项编辑选项只对批量输入有效;原有 stdout 暂存逻辑继续保证:即使编辑后某一项不合法,本次调用也不输出半份成功结果。源码、失败前后的原始测试与当前累计工程在06 素材包,下载后用SHA256SUMS核验。
独立复跑步骤列出本章正反例、源码校验及运行命令。
失败的断言针对谁的数组
TestPrepareBatchDoesNotChangeInput 构造 backing := []string{"one", "two", "guard", "spare"},再取 input := backing[:2:4] 交给批量编辑函数。这一写法保证 len(input) == 2、cap(input) == 4,数组第三格有一个可观察的 guard。首次实现只截去第一项,再调用 append(input, "three");返回结果是正确的 two, three,但旧数组第三格变为 three。首次 go test -count=1 -run TestPrepareBatchDoesNotChangeInput -v . 退出 1,原始失败行是 input backing array modified: [one two three spare],保存在 writing-plans/go-language/evidence/06/alias-before.*。
为什么 input = input[1:] 后改到第三格?新切片的第一项指向旧数组第二格,长度从 2 变成 1,仍有剩余容量;append 将 three 写在新切片末尾,正好是旧数组第三格。赋给局部变量 input 只复制 slice 值,没复制元素。testdata/slice-alias-bug_test.go 保留独立的错误操作:它不依赖当前已经修复的 prepareBatch,从最终归档运行 go test -count=1 -v ./testdata/slice-alias-bug_test.go 仍以 1 退出,显示相同的被覆盖数组。失败测试有用的前提是确实构造有剩余容量的切片;若仅拿 strings.Split 的一次返回值测试,append 是否复用底层数组可能变成未控制的前提。
在 Go 1.27.1 的语言规范里,数组长度是数组类型的一部分;数组赋给另一变量会复制数组值。TestArrayValueCopiesElements 修改 [2]string 的副本,原数组首项仍是 one。slice 则描述底层数组的一段,值里有长度、容量和对底层数组的引用;复制 slice 值只让两个描述符指向同一数组。append 容量充足时重用原数组,容量不足时才分配新数组。go run ./testdata/slice-append.go 在本工具链下退出 0,stdout 为:
1 | |
前一行构造多余容量,并用第二个切片观察追加位置被覆盖;后一行从满容量切片追加,再改新结果首项,旧数组的 first 保持不变。示例只比较两种已知容量,不能据此猜测扩容系数,更不能靠「某次 append 恰好扩容」保证数据不被共享。
编辑前明确取得自己的存储
当前 prepareBatch 在任何截取或追加之前执行:
1 | |
无论后续 append 是否复用 owned 的数组,都不会改写传入的 input 的数组。根包回归测试除了检查 backing[2] 仍为 guard,还改动了返回切片的首项,再断言原输入的第二项仍为 two。相同测试修复后退出 0,记录在 alias-after.*。这里复制的是字符串元素组成的切片;字符串本身是不可修改的值。如果元素换成指针或内部带引用的对象,copy 只复制每个元素值,不能自动获得深层独占存储。
CLI 正例 --batch 'one two|Go 写 Go' --drop-first --append again 返回 task[0] words=3 bytes=9 runes=7、task[1] words=1 bytes=5 runes=5,stderr 空、应用退出 0。--batch Go --drop-first 删除仅有的一项后被拒绝,stdout 空、退出 2;--batch Go --append '' 生成空项后触发原有文本校验,同样没有成功输出、退出 2;在 --file 上使用 --append 也被用法检查拒绝。编译二进制后的实际双流及退出码在 evidence/06/edited.*、empty.*、bad-append.*、incompatible.*,--batch、--file 原入口各有重测。go test -count=1 -v ./... 在两个包均退出 0。这里的 stdout 原子性仍是内存暂存带来的外部输出边界,没证明批次业务状态回滚。
练习:把 owned := input 代替 make/copy,在上面的测试中会怎样?答案:owned 与 input 共享数组。截取后追加覆盖 guard,TestPrepareBatchDoesNotChangeInput 失败;即使恰好在某个输入上触发扩容,返回切片在扩容前仍可能和原数组共享,不能以偶然通过代替拥有权契约。将 input[:2:2] 缩小容量可使这一次追加分配新数组,但无条件隔离输出与输入更直接的方法是先复制。
本章仅验证顺序批量与本版 Go 上数组、切片的共享边界。没有运行 race、HTTP、SQL、其他平台或发布;go1.27.1 的源码 SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0 来自 Go 镜像,官方 Gitiles 交叉核验仍未完成。是否能从另一协程安全地访问数组,需要另行建立同步关系并运行对应测试。下一篇用 map 为顺序任务建索引,区分不存在的键和存在但值为零的键。
参考资料
- Go 语言规范:Array types、Slice types、Appending and copying slices。对应 Go 1.27.1 的本地
doc/go_spec.html及冻结信息见writing-plans/go-language/research/06.md。 - 内置函数 copy。本次实际核对
go doc builtin.copy,断言copy复制的只是元素值,不是嵌套引用图。






