Go的slice头是24字节三字段结构体:data指针(8字节)、len(8字节)、cap(8字节),顺序固定,小端序存储,仅在64位系统上成立。
slice 头在内存里到底长啥样
go 的 slice 是一个三字段结构体:指向底层数组的指针、长度 len、容量 cap。它不是引用类型,也不是对象,而是一个值——复制时只拷贝这三个字段的字节。在 64 位系统上,slice 头固定占 24 字节:指针 8 字节 + len 8 字节 + cap 8 字节,顺序严格为 data、len、cap。
你可以用 unsafe.Sizeof 验证:
package main
import (
"fmt"
"unsafe"
)
func main() {
s := []int{1, 2, 3}
fmt.Println(unsafe.Sizeof(s)) // 输出 24(amd64)
}
怎么用 unsafe 把 slice 头拆成原始字节
想观察底层排布,最直接的方式是把 slice 头当成一段连续内存读出来。关键在于:不能取 &s(那是栈上变量地址),而要用 unsafe.Slice 或 unsafe.String 配合 unsafe.Pointer 转换。
- 先用
unsafe.Pointer(&s)获取 slice 变量自身的地址(即头结构体起始位置) - 再用
unsafe.Slice将其解释为长度为 24 的[]byte - 注意:必须确保
s不被编译器优化掉(比如加runtime.KeepAlive(s)或放在全局)
示例(amd64):
hdr := unsafe.Slice((*byte)(unsafe.Pointer(&s)), 24)
fmt.Printf("%x\n", hdr) // 会输出 24 字节十六进制,前 8 字节是 data 指针低位到高位
你会发现前 8 字节是数组地址(小端序),中间 8 字节是 len,后 8 字节是 cap —— 和 reflect.SliceHeader 字段顺序完全一致,但后者不保证内存布局兼容,**绝不能直接类型转换赋值**。
为什么不能用 reflect.SliceHeader{...} 直接构造 slice
常见误区:有人试图 new 一个 reflect.SliceHeader,填好 Data/Len/Cap,再用 unsafe.Slice 转回 slice。这在 Go 1.17+ 会触发 vet 检查,且运行时可能 panic,因为:
-
reflect.SliceHeader是文档明确声明“仅用于反射包内部”的伪结构,字段对齐和填充未保证跨版本稳定 - Go 运行时对 slice 头有隐式校验(比如
Data地址是否可读、是否越界),非法构造可能被立即拦截 - 即使侥幸通过,GC 无法跟踪你手工填入的
Data指针,极易造成悬垂指针或提前回收
正确做法永远是从已有 slice 或底层数组出发,用 unsafe.Slice(Go 1.17+)或 reflect.SliceHeader + unsafe.Slice 组合(需确保源数组生命周期足够长)。
不同架构下要注意字节序和字段大小
24 字节是 amd64/arm64 的结果,但在 32 位平台(如 386/arm)上,指针和整数都是 4 字节,slice 头总共 12 字节:data(4) + len(4) + cap(4)。如果代码需要跨平台处理 slice 头:
- 别硬编码 24 或 12,改用
unsafe.Sizeof(s) - 读取指针字段时,用
binary.LittleEndian.Uint64或Uint32匹配当前平台指针宽度 - 切勿假设
len和cap总是 8 字节——它们类型是int,和平台相关
另外,所有字段都是小端序存储,无论 CPU 架构(Go 运行时统一规定)。
真正容易被忽略的是:slice 头本身没有 GC 元信息,它的生命周期完全依赖持有者;一旦包含该头的变量离开作用域,头结构体就失效,哪怕底层数组还在 —— 这也是为什么不能靠保存 slice 头来“延迟访问”数据。











