unsafe 包让你绕过类型系统,但规范里有一套“合法模式”,违反了在大多数情况下“碰巧能跑”,直到栈被搬动或 GC 改变时才出错。这篇用实验展示:合法写法、uintptr 为什么不是指针(栈会搬)、窥探内部布局的脆弱,以及 go vet 与 checkptr 能抓住哪些误用、抓不住哪些。
模式 1:*(*uint64)(unsafe.Pointer(&f)) = 0x3ff8000000000000;math.Float64bits(f) = 0x3ff8000000000000;相等?true 模式 3:unsafe.Add(&arr[0], 8) → 30;uintptr(&arr[0]) + 3*Sizeof → 40(都在一个表达式里完成,合法) unsafe.Offsetof:A = 0、B = 8、C = 16;Sizeof(Rec) = 24;Alignof(Rec) = 8 用 Offsetof 访问字段:*pc = 7(r.C = 7) unsafe.String / unsafe.Slice / unsafe.StringData:s = "hello",sl 长度 5
unsafe 文档列出的合法模式在实验里都能得到预期结果:同大小类型间的转换(float64 与 uint64 的位模式)、在同一个表达式里把 unsafe.Pointer 转成 uintptr 做加法再转回来、用 unsafe.Add 做指针偏移、用 Offsetof 访问字段。新代码优先用 unsafe.Add 而不是 uintptr 运算。
局部变量 x 的地址:增长前(用 uintptr 保存)0xabe0;增长后(取 &x)0x7be0;不相等?true(栈被搬走了,x 跟着搬) 用增长前保存的 unsafe.Pointer p 读 x:42(运行时在拷贝栈时把 p 也改写了);p == &x?true 堆上对象(逃逸到堆):3 次 GC 之后地址不变?true(当前实现的 GC 不移动堆对象,但语言规范不保证) 栈上的局部变量(不逃逸):3 次 GC 之后地址不变?false
uintptr 只是一个整数,GC 与栈拷贝都不认识它。goroutine 栈增长时运行时会把整个栈搬到新地址,并把栈上所有指针(包括 unsafe.Pointer 变量)改写成新地址;uintptr 变量不会被改写,变成指向旧栈的悬空数字。堆对象在当前实现里不会被 GC 移动,但语言规范并没有保证这一点——所以“把地址存成整数以后再用”是一种在今天碰巧能用、明天可能出错的写法。栈上变量在 GC 之后地址也可能变(栈收缩),这是实验里观察到的。
*(*[]byte)(unsafe.Pointer(&buf)) = "peek"(长度 4,容量 64);buf.Len() = 4,buf.Cap() = 64
用 unsafe 读标准库类型的未导出字段能工作,是因为布局恰好如此;它与库的内部实现耦合,升级一个版本就可能静默读错。能用公开方法就不要这样。
uintptr 存进变量再转回指针(两条语句,且先做了加法):
go vet:main.go:15:15: possible misuse of unsafe.Pointer
默认:退出码 0,0;checkptr:退出码 2,fatal error: checkptr: pointer arithmetic result points to invalid allocation
未对齐的转换:b 在堆上,从 b[1] 起转成 *[1]*int(元素含指针):
go vet:无警告
默认:退出码 0,<nil>;checkptr:退出码 2,fatal error: checkptr: misaligned pointer conversion
同样的未对齐转换,但目标是 *int64(元素不含指针):
go vet:无警告
默认:退出码 0,0;checkptr:退出码 0,0
unsafe.Add 越过堆上 16 字节切片末尾 100 字节:
go vet:无警告
默认:退出码 0,0;checkptr:退出码 2,fatal error: checkptr: pointer arithmetic result points to invalid allocation
把堆上的 *[4]byte 转成 *[8]byte(越过原对象):
go vet:无警告
默认:退出码 0,0;checkptr:退出码 0,0go vet 的 unsafeptr 检查只抓“uintptr 转回 unsafe.Pointer”这一类。运行时的 checkptr(-race 会自动打开,也可以用 -gcflags=all=-d=checkptr=1 单独打开)能抓到更多:指针运算越界、含指针类型的未对齐转换。从实验也看到它不是万能的:目标类型不含指针的未对齐转换、把 4 字节对象转成 8 字节数组类型的情形,都没有被报告(后一种我推测是因为 4 字节的对象实际落在 8 字节的分配块里,没有验证)。把 checkptr 当作测试时的辅助,而不是正确性的证明。