必须用 unsafe.Pointer 的5个合法场景是:零拷贝切片转换、访问结构体私有字段、CGO中传递指针、系统调用参数构造、反射底层操作;uintptr运算须一气呵成,避免GC误回收。

什么时候必须用 unsafe.Pointer?先看这 5 个合法场景
Go 官方明确承认的 unsafe.Pointer 合法用途只有 6 种(reflect、syscall、CGO、reflect.SliceHeader/reflect.StringHeader、指针类型转换、uintptr 算术后立即转回),其中最常被误用的是前 5 种:
-
零拷贝切片转换:比如把
[]byte底层数据直接解释为[]int32,必须用unsafe.Slice(Go 1.17+)或reflect.SliceHeader+unsafe.Pointer,不能直接强转(*[]int32)(unsafe.Pointer(&slice[0])) -
访问结构体私有字段:通过
unsafe.Offsetof计算偏移,再加到结构体指针上,但必须校验unsafe.Sizeof和unsafe.Alignof,否则跨平台失效 -
CGO 中传递指针:C 函数需要
int*,Go 传&x后转成(*C.int)(unsafe.Pointer(&x))——这里&x必须是堆/全局变量,局部变量会栈回收 -
系统调用参数构造:如
syscall.Syscall(SYS_write, uintptr(fd), uintptr(unsafe.Pointer(&slice[0])), uintptr(len(slice))),注意第三个参数是uintptr,不是unsafe.Pointer -
反射底层操作:比如从
reflect.Value获取原始地址:unsafe.Pointer(v.UnsafeAddr()),仅限v.CanAddr()为 true 时才安全
uintptr 不是“轻量版指针”,它是内存安全的断点
很多人把 uintptr 当作可存储、可计算的“安全指针”,这是最大误区。一旦转成 uintptr,GC 就彻底丢失对该内存的追踪。
- 错误写法:
ptr := uintptr(unsafe.Pointer(&x)); time.Sleep(time.Second); *(*int)(unsafe.Pointer(ptr)) = 42——中间间隔可能触发 GC 回收x - 正确做法:所有
uintptr运算必须“一气呵成”,即unsafe.Pointer(ptr + offset)必须在单条表达式里完成,且结果立刻用于解引用或传参 - 若需跨函数传递,必须保留一个合法 Go 指针(如传入原结构体指针并作为返回值返回),或用
runtime.KeepAlive(x)在作用域末尾显式延长生命周期
结构体字段偏移不是“数数”,对齐和填充才是关键
用 unsafe.Offsetof 访问字段前,必须验证整个结构体布局是否与预期一致。例如:
type Header struct {
Len int
Cap int
Data uintptr
}
这个结构体在 amd64 上总大小是 24 字节(两个 int 各 8 字节,uintptr 8 字节),但在 arm64 上,如果 int 是 4 字节,uintptr 是 8 字节,中间就会插入填充字节——导致 Data 偏移不是 16,而是 24。
- 务必用
unsafe.Sizeof(Header{})和unsafe.Offsetof(h.Data)实际校验,不能硬编码数值 - 避免在结构体中混用不同宽度类型(如
int+int64),容易因对齐规则变化导致跨平台崩溃 - 优先使用
unsafe.Slice替代手算偏移,它内部已做对齐与长度检查
为什么 reflect.SliceHeader 比手造结构体更安全?
有人自己定义结构体模拟 slice 内部(如 Len/Cap/Data),然后强制转换,这是高危操作。因为:
-
reflect.SliceHeader是 Go 运行时公开支持的 ABI,其字段顺序、对齐、大小在各版本中受维护(尽管文档标注为“internal”,但实际稳定性远高于自定义结构体) - Go 1.21 起明确禁止直接操作
[]byte底层数组地址构造结构体指针,reflect.SliceHeader是唯一推荐路径 - 示例安全写法:
sh := (*reflect.SliceHeader)(unsafe.Pointer(&slice)); sh.Len = newLen; sh.Cap = newCap,但注意:修改Len/Cap不影响原 slice,只影响该 header 副本
真正难的不是写出能跑的 unsafe 代码,而是写出在 Go 1.22、arm64、以及你三年后回头看仍敢线上运行的代码。每一次 unsafe.Pointer 转换,都该伴随一行注释:为什么不用 unsafe.Slice?为什么这个偏移不会在 Windows 上错?为什么 GC 不会在这行之后回收它?










