Go禁止对*int等普通指针做加减运算,必须用unsafe.Pointer配合uintptr偏移,且转换需原子完成;推荐unsafe.Add和unsafe.Offsetof,优先使用unsafe.Slice或reflect替代裸指针算术。

Go 里不能直接对 *int 做加减运算
Go 语言明确禁止对普通指针(如 *int、*string)执行算术运算,比如 p + 1 会直接报错:invalid operation: p + 1 (mismatched types *int and int)。这不是语法糖缺失,而是类型安全设计的一部分——编译器不让你绕过类型系统去“猜”内存布局。
如果你真需要偏移访问(比如解析二进制协议、操作结构体内存布局、对接 C 接口),必须切换到 unsafe.Pointer,再用 uintptr 做整数偏移,最后转回具体类型指针。
常见错误是试图对 *T 强转成 uintptr 后相加,却忘了 Go 的 GC 可能在此期间移动对象,导致指针失效——所以必须确保整个转换过程原子、无中间变量逃逸,且目标内存生命周期可控(比如栈上变量、或已固定地址的 unsafe.Slice / reflect.SliceHeader 等场景)。
用 unsafe.Offsetof 和 unsafe.Add 计算结构体字段偏移
手动算字段偏移容易出错:结构体可能有填充字节(padding),不同架构对齐规则也不同。别硬算 4 + 8 + 2 这种表达式。
立即学习“go语言免费学习笔记(深入)”;
-
unsafe.Offsetof(T{}.Field)是唯一可靠方式,返回字段相对于结构体起始地址的字节偏移 - Go 1.17+ 推荐用
unsafe.Add(ptr, offset)替代旧式uintptr(unsafe.Pointer(ptr)) + offset,更安全、语义清晰 - 偏移量必须是
uintptr类型;int在 32 位平台可能溢出,不要混用
示例:获取结构体第二个字段地址
type Header struct {
Magic uint32
Len uint16
}
h := Header{Magic: 0xdeadbeef, Len: 123}
p := unsafe.Pointer(&h)
lenPtr := (*uint16)(unsafe.Add(p, unsafe.Offsetof(h.Len)))
unsafe.Pointer 转换链不能中断,也不能跨函数传递原始 uintptr
这是最常踩的坑:把 uintptr 当普通整数传参、存在 map 里、或在 goroutine 间共享,会导致 GC 无法追踪该地址对应的对象,轻则读到垃圾数据,重则 panic 或崩溃。
- 所有
unsafe.Pointer ↔ uintptr转换必须在同一表达式内完成,例如(*T)(unsafe.Add(unsafe.Pointer(&x), offset)) - 不能写成:
u := uintptr(unsafe.Pointer(&x)); u += offset; (*T)(unsafe.Pointer(u))—— 中间u是纯整数,GC 完全看不见它和&x的关系 - 函数参数若需传偏移后指针,应直接传
unsafe.Pointer,而不是uintptr
正确模式是“一次转、一次用、不保存”,尤其注意反射、cgo 回调、channel 发送等场景。
替代方案优先考虑 unsafe.Slice 和 reflect,而非裸指针算术
90% 的所谓“需要指针偏移”的需求,其实只是想把一段内存当数组用,或者动态读结构体字段——这时 unsafe.Slice(Go 1.17+)和 reflect.StructField.Offset 更安全、更易读、且不破坏内存模型。
-
unsafe.Slice((*byte)(unsafe.Pointer(&data)), size)比手动加减uintptr更直观,且返回的是切片,可直接 range、copy - 用
reflect.TypeOf(T{}).Field(i).Offset获取字段偏移,适合运行时动态处理,避免硬编码 - 如果只是序列化/反序列化,直接用
encoding/binary或gob,比手撕偏移靠谱得多
只有当你明确知道内存布局、控制对象生命周期、且性能压倒一切时,才该碰 unsafe.Add 链。
偏移计算本身很简单,难的是保证那一串 unsafe 操作全程不被 GC 干扰、不越界、不违反对齐——稍一松懈,就是静默错误或 crash。










