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

go 中切片是引用类型,直接赋值不会复制底层数组;若需保持原切片不变,必须显式创建独立副本(如用 append([]byte(nil), src...)),否则 shuffle 等就地操作会同时影响原始数据。

在 Go 语言中,[]byte 是一个切片(slice),其底层结构包含指向数组的指针、长度(len)和容量(cap)。当你执行 cryptkey := alphabet 时,并未创建新底层数组,而是让 cryptkey 和 alphabet 共享同一块内存。因此,后续对 cryptkey 的任何就地修改(如交换元素)都会直接反映在 alphabet 上——这正是你观察到“两个切片都被打乱”的根本原因。

要真正隔离数据,必须进行深拷贝(deep copy)。最简洁、惯用且安全的方式是使用 append 构造新切片:

out := append([]byte(nil), b...)

该语句等价于:分配一个长度为 len(b)、类型为 []byte 的新切片,并将 b 的所有元素逐个复制进去。[]byte(nil) 提供空切片作为起点,append 自动处理内存分配,语义清晰且零分配冗余(相比 make([]byte, len(b)) + copy() 更简练)。

修正后的完整示例:

package main

import (
    "fmt"
    "math/rand"
    "time" // 注意:添加 time 包以正确初始化随机种子
)

func main() {
    alphabet := []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz.")
    cryptkey := alphabet // 此时仍共享底层数组(但后续 shuffle 不再影响它)

    fmt.Println("Original alphabet:", string(alphabet))

    // 初始化随机种子,避免每次运行结果相同
    rand.Seed(time.Now().UnixNano())
    cryptkey = shuffle(cryptkey)

    fmt.Println("After shuffle — alphabet unchanged:", string(alphabet))
    fmt.Println("Shuffled cryptkey:", string(cryptkey))
}

func shuffle(b []byte) []byte {
    l := len(b)
    if l <= 1 {
        return append([]byte(nil), b...) // 边界情况也保证副本
    }
    out := append([]byte(nil), b...) // ✅ 关键:创建独立副本

    for i := range out {
        dest := rand.Intn(l)
        out[i], out[dest] = out[dest], out[i]
    }
    return out
}

⚠️ 注意事项

  • rand.Intn 在 Go 1.20+ 已弃用,生产环境建议改用 rand.New(rand.NewSource(seed)).Intn(l) 或 rand.New(rand.NewPCG()),但本例为简洁保留旧用法(需配合 rand.Seed);
  • 切勿使用 out := b[:len(b):len(b)] 或 out := b,它们仍共享底层数组;
  • 若需高性能批量复制,copy(dst, src) 亦可,但 append(..., b...) 更符合 Go 的惯用风格且不易出错。

总结:Go 中切片赋值不等于复制数据。凡涉及可能修改切片内容的函数(如 shuffle、reverse、sort.Slice 等),务必在函数内部通过 append([]T(nil), s...) 或 copy 显式创建副本,才能保障输入参数的不可变性(immutability)与函数纯度(purity)。