切片是三字宽的值类型结构体,包含array、len、cap字段,栈上仅占24字节;传参赋值复制整个头,array仍指向同一底层数组,故元素修改相互影响。

切片不是指针,是三字宽的值类型结构体
Go 中的 slice 本质是一个包含三个字段的结构体:array(unsafe.Pointer)、len、cap。它本身不分配堆内存,只在栈上存这三字(24 字节),所以传参、赋值都是快速拷贝——你改的是“切片头”,不是底层数组。
- 误以为
s1 = s2是浅拷贝“引用”?错:这是整个 slice 头的复制,但array字段仍指向同一底层数组 → 修改元素会影响对方 - 误以为
s[i:j]一定新建数组?错:只是构造新 slice 头,指针仍指向原数组某偏移处,零分配 - 用
fmt.Printf("%p", &s)打印切片变量地址?没意义——它只是头地址,不是数据地址;要看底层数组得用reflect.ValueOf(s).UnsafeAddr()
扩容时何时复制?append 的 cap 判断逻辑
append 是否触发底层数组复制,只看当前 len(s) 是否成立。满足就直接写入,不分配;否则新建数组、拷贝、更新 slice 头。
- 常见错误:反复
append小切片却没预估容量 → 频繁扩容(2 倍增长)导致多次拷贝和内存碎片 - 正确做法:已知最终长度时,优先用
make([]T, 0, n)预分配,比如解析 JSON 数组前先json.Unmarshal到[]byte再切分 - 注意:
cap不是“剩余空间”,而是“从s[0]开始到底层数组末尾的总长”。s[1:3]的cap可能比原切片小很多,容易意外截断
共享底层数组带来的静默副作用
多个切片共用同一底层数组是常态,也是性能优势来源,但也是 bug 温床——尤其在函数返回、结构体字段、缓存场景中。
- 典型现象:函数内
return s[1:],调用方修改该切片,上游原始切片对应位置也被改了 - 结构体里存
slice字段?如果该字段来自参数或全局切片截取,实例间可能互相污染 - 修复方式明确:需要隔离时,用
copy(dst, src)显式复制;或用append([]T(nil), s...)强制分配新底层数组 - 调试技巧:用
unsafe.Slice(&s[0], len(s))+reflect.ValueOf().Pointer()对比不同切片的底层数组起始地址
为什么 len 和 cap 必须分开设计?
len 是语义长度(你“看到”的元素数),cap 是物理上限(你“能写到”的边界)。二者分离让切片既能安全封装(限制访问范围),又能高效复用(预留扩展空间)。
- 截取操作
s[i:j:k](三参数形式)可显式控制新切片的cap:它变成k - i,而非默认的cap(s) - i,防止下游意外越界写入 - 作为函数参数时,接收方无法通过
len推断cap,所以不能假设“还有空间”——append前必须检查或接受可能的扩容 - 序列化/网络传输前若只传
len数据但忽略cap,可能导致append后 panic:底层数组被回收或重用(如bytes.Buffer.Bytes()返回的切片)
切片的轻量性全靠这三字段协作,但它的“轻”也意味着责任下放:谁持有 slice 头,谁负责理解并管理其与底层数组的关系。最容易被忽略的,是认为“没 new 就没开销”——其实一次错误的截取,可能让整个大数组无法被 GC。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











