因为slice是值传递,但其结构体中的array指针字段指向同一底层数组,修改元素即直接操作该共享内存,故原slice对应位置值同步改变。

函数内改元素为什么原 slice 会变
因为 slice 是值传递,但这个“值”里包含一个 array unsafe.Pointer 字段,指向底层数组起始地址。只要没触发扩容或重新赋值,所有共享该指针的 slice 都在操作同一块内存。
常见错误现象:s[0] = 100 在函数里执行后,调用方看到原 slice 第一个元素真变了——这不是 bug,是设计使然。
- 只要没改变
s变量本身的值(比如没做s = append(s, x)),它就始终指着那块老内存 - 修改元素、遍历赋值、
copy()写入等操作,都直接作用于底层数组 - 即使你传的是
s[1:3]这种子切片,只要指针没变,改它照样影响原始数组对应位置
append 后不接收返回值,新增元素就丢了
append 永远返回新 slice,不是就地修改。原变量仍保存旧结构体(含旧指针、旧 len/cap),所以不赋值就等于什么都没发生。
使用场景:循环累积数据、构建动态列表时最容易踩这个坑。
- 必须写成
s = append(s, x),少一个s =就失效 - 如果原
cap不够,append分配新数组并复制数据,此时新 slice 的array指针已变,和原 slice 彻底断开 - 如果原
cap足够,append只更新新 slice 的len和可能的cap,指针不变,但原变量还是不知道这事
用三索引切片控制容量,能切断底层数组共享
写 s[i:j:k] 时,新 slice 的 cap 被设为 k - i,后续 append 一旦超出这个 cap,立刻分配新底层数组——这是主动隔离副作用的最轻量手段。
性能 / 兼容性影响:几乎无开销,Go 1.2+ 全支持,且编译器不做额外检查,全靠你写对。
- 例如
s := []int{1,2,3,4,5}; sub := s[1:3:3],sub的cap == 2,再append(sub, 6)就会新建数组 - 这比
make+copy更省内存,也比传指针 + 显式锁更简单 - 注意:三索引语法要求
k ,越界会 panic
nil slice 和空 slice 对底层数组的影响不同
var s []int 是 nil slice,s == nil 为 true,它没有底层数组;而 s := make([]int, 0) 或 s := []int{} 是空 slice,有底层数组(可能为 nil 指针,也可能指向某块小内存),append 行为不同。
容易踩的坑:把 nil slice 当作普通 slice 直接 append,虽能工作,但首次扩容必分配新数组,且无法和任何已有底层数组共享。
- nil slice 的
len和cap都是 0,append第一次总是走全新分配路径 - 空 slice 如果由
make([]T, 0, n)构造,cap == n,后续小量append不触发扩容 - 判断是否共享底层数组,不能只看
len == 0,得看s != nil && cap(s) > 0
底层数组共享不是偶然发生的意外,而是 slice 设计的默认状态。真正需要警惕的,是那些你以为“只是读”、结果却悄悄写了底层数组的地方——比如传进来的 []byte 被某个库函数 append 或 copy 了,而你完全没意识到。











