Go禁止指针算术,unsafe.Pointer与unsafe.Add是唯一合法偏移手段;unsafe.Slice可安全构造切片,避免手动操作SliceHeader导致GC失效。

Go 里不能直接做指针加减,unsafe 不是“替代方案”而是唯一底层出口
Go 明确禁止指针算术运算(比如 p + 1、p++),这不是语法限制,而是语言设计层面的强制约束。你没法绕过它写“安全”的指针算术——因为 Go 根本不提供这个能力。unsafe 包里的 unsafe.Pointer 和 unsafe.Add(Go 1.17+)不是“替代”,而是你唯一能触达内存偏移的地方,且必须自己承担全部风险。
常见错误现象:invalid operation: p + 1 (mismatched types *int and int);或用 uintptr 强转后做加减却没及时转回 unsafe.Pointer,导致 GC 误判指针失效(悬空引用)。
-
unsafe.Add是推荐方式(Go 1.17+),它接受unsafe.Pointer和uintptr偏移量,返回新unsafe.Pointer,语义清晰且不易漏掉类型转换 - 老版本用
uintptr(unsafe.Pointer(p)) + offset必须立刻转回unsafe.Pointer,中间不能有函数调用或变量赋值,否则 GC 可能在此刻回收原对象 - 偏移量单位是字节,不是元素个数——别想当然除以
unsafe.Sizeof后就当 C 那样用,得自己算对齐和大小
用 unsafe.Slice 替代手动计算切片头(Go 1.17+)
很多场景下你以为在做“指针算术”,实际只是想从某地址开始取一段连续内存当切片用——比如解析二进制协议、操作 mmap 内存、或对接 C 函数返回的裸指针。这时候不该手撸 unsafe.Pointer 加减再构造 reflect.SliceHeader,而该用 unsafe.Slice。
示例:从 *byte 起始地址读取 1024 字节为切片
data := (*byte)(unsafe.Pointer(&buf[0])) slice := unsafe.Slice(data, 1024) // 类型自动推导为 []byte
-
unsafe.Slice接收*T和长度n,返回[]T,内部处理了对齐、大小和 GC 可见性,比手造reflect.SliceHeader安全得多 - 它不检查底层数组是否足够长,越界访问仍是 panic 或未定义行为,但至少避免了 header 构造时字段顺序/大小写错等低级失误
- 不能用于非切片场景(比如只想移动指针位置而不立即建切片),这时还得回到
unsafe.Add
为什么不用 reflect.SliceHeader 手动构造?
有人会翻旧文档看到用 reflect.SliceHeader 改 Data 字段来“伪造”切片,这在 Go 1.17 后已明确不安全且可能被编译器优化掉。核心问题是:Data 字段类型是 uintptr,不是 unsafe.Pointer,GC 看不到它指向的对象,一旦原对象被回收,切片就成野指针。
- 即使你确保原对象生命周期足够长,Go 1.20+ 的逃逸分析和内联优化也可能让这种写法在某些构建模式下崩溃
-
unsafe.Slice和unsafe.String是官方提供的、经过充分测试的安全封装,它们保证 GC 能追踪到背后内存 - 若必须兼容老版本(reflect.SliceHeader{Data: uintptr(unsafe.Pointer(p)), Len: n, Cap: n},但需用
unsafe.Slice等价逻辑校验对齐,并加注释说明风险
真正该警惕的:跨包、跨 goroutine、跨 CGO 边界的指针传递
哪怕你用对了 unsafe.Add 和 unsafe.Slice,只要把生成的指针或切片传给其他包、goroutine 或 C 函数,问题就立刻升级。C 函数不会帮你保活 Go 对象,goroutine 调度可能让持有指针的栈帧提前销毁,第三方库更不会理解你的 unsafe 意图。
- CGO 边界必须用
C.CBytes或C.calloc分配内存,别把 Go 切片头直接扔给 C——Go 的 slice header 在 C 看来就是三个无意义整数 - goroutine 间传递指针前,确认源数据是全局变量、堆分配对象,或用
sync.Pool管理生命周期,别传栈上局部变量的地址 - 如果目标库要求
*C.struct_foo,优先用C.GoBytes复制数据,而不是试图用unsafe把 Go struct 强转过去——布局、对齐、填充都可能不一致
最常被忽略的一点:unsafe 代码的可维护性不在语法难度,而在生命周期契约的显式表达。没人能单看一行 unsafe.Add 知道它依赖哪个变量没被 GC、哪个 C 函数还没释放内存——这些必须靠注释、封装函数名和作用域控制来补全。


















