如何避免 Go 中字节切片在函数调用中被意外修改

go 中切片是引用类型,直接赋值(如 `cryptkey := alphabet`)仅复制头信息,底层数组共享;若函数内原地修改切片内容,原始切片也会被改变。解决方法是创建独立副本,例如使用 `append([]byte(nil), b...)`。

在 Go 语言中,切片([]byte)本质上是一个包含指向底层数组的指针、长度(len)和容量(cap)的结构体。当你执行 cryptkey := alphabet 时,并未复制底层数据,只是复制了这个结构体——因此 alphabet 和 cryptkey 指向同一块内存区域。后续对 cryptkey 的任何原地修改(如交换元素),都会同步反映在 alphabet 上。

上述问题中的 shuffle 函数正是如此:它接收切片 b,将 out := b 赋值给新变量,但 out 与 b(进而与 alphabet)仍共用底层数组。因此洗牌操作直接修改了原始字母表。

✅ 正确做法:在 shuffle 内部创建深拷贝(即独立底层数组):

func shuffle(b []byte) []byte {
    l := len(b)
    // 创建新底层数组:安全、高效、惯用
    out := append([]byte(nil), b...)
    for i := range out {
        dest := rand.Intn(l)
        out[i], out[dest] = out[dest], out[i]
    }
    return out
}

append([]byte(nil), b...) 是 Go 官方推荐的零分配开销切片拷贝方式:它会分配一块新内存并逐字节复制 b 的内容,确保 out 与输入切片完全隔离。

⚠️ 注意事项:

  • 不要使用 make([]byte, len(b)); copy(dst, b) —— 虽然等效,但 append(...) 更简洁;
  • 避免 out := b[:] 或 out := b,它们不产生新底层数组;
  • 若需可复用的通用拷贝函数,可封装为:
    func cloneBytes(src []byte) []byte {
        return append([]byte(nil), src...)
    }

总结:Go 切片的“引用语义”是强大特性的双刃剑。当需要函数保持输入不可变时,务必显式创建副本——这是编写健壮、可预测 Go 代码的基本守则。