Go 专家(10):unsafe.Pointer 与 uintptr —— 内存地址操作、指针运算、go vet 检查
更新时间:2026-09-01。本文是
languages/go/expert/专家层第 10 篇,接 GC 三色标记深入。unsafe包是 Go 里最不安全的,但最接近机器码。理解unsafe.Pointer和uintptr的区别,才能理解 Go 的指针模型,写对内存操作。
本文要回答的问题
unsafe.Pointer和uintptr有什么区别?为什么 Go 不直接允许指针运算?unsafe的安全规则是什么?为什么违反规则可能导致野指针?unsafe.Sizeof和unsafe.Offsetof怎么用?- 什么时候用
unsafe?什么时候不用?
一、Go 指针模型
Go 里有三种和指针相关的类型:
var p *T // 普通指针,不能做运算
var up unsafe.Pointer // 通用指针,可以转换成任何 *T,也可以和 uintptr 互转
var u uintptr // 整数类型,保存指针地址,不是指针,GC 不保护转换规则:
// 合法转换
ptr := uintptr(unsafe.Pointer(p)) // Pointer → uintptr
p = (*T)(unsafe.Pointer(ptr)) // uintptr → Pointer
// 指针运算:unsafe.Pointer(uintptr(p) + offset)二、unsafe.Pointer vs uintptr
| 对比 | unsafe.Pointer | uintptr |
|---|---|---|
| 类型 | 指针类型 | 整数类型 |
| GC | 保护对象不被回收 | 不保护,GC 看不见 |
| 能做运算? | 不能,要先转 uintptr | 能,整数运算 |
| 野指针风险 | 低,只要规则对 | 高,GC 可能移动对象 |
核心区别: uintptr 是整数,GC 不知道它引用了对象,可能移动对象地址,导致 uintptr 的值变成野指针。所以记住:
uintptr 不能存放在变量里,只能在一行转换语句里用。
三、unsafe 安全规则
// ✅ 正确:一行转换,不用中间变量
// 取结构体第 n 个字段地址
type S struct {
a int
b string
c float64
}
s := &S{...}
// 拿到 c 的地址
p := (*float64)(unsafe.Pointer(uintptr(unsafe.Pointer(s)) + unsafe.Offsetof(s.c)))不对,中间有个 uintptr 变量:
// ❌ 错误:uintptr 存到变量里了,GC 可能移动对象
up := uintptr(unsafe.Pointer(s))
p := (*float64)(unsafe.Pointer(up + unsafe.Offsetof(s.c)))为什么错? GC 并发标记完成后,对象移动,地址变了,up 还是旧地址,指针就野了。
安全规则:
uintptr不能持久化,只能在一个表达式里完成转换和运算- 不能用
uintptr作为结构体字段 - 不能把
uintptr传给函数,参数栈上也可能触发 GC 移动 - 唯一允许存
uintptr的地方是 runtime 和调试代码
四、unsafe 常用操作
4.1 计算结构体大小和字段偏移
type S struct {
a int8
b int64
c int32
}
fmt.Println(unsafe.Sizeof(S{})) // 计算 S 大小,包括内存对齐
fmt.Println(unsafe.Alignof(S{})) // 对齐要求
fmt.Println(unsafe.Offsetof(S{}.b)) // 字段 b 的偏移用于内存对齐优化,见 结构体内存对齐。
4.2 指针运算
// 遍历 slice 元素,用指针运算代替索引
data := []int{1, 2, 3, 4}
ptr := unsafe.Pointer(&data[0])
for i := 0; i < len(data); i++ {
p := (*int)(unsafe.Pointer(uintptr(ptr) + uintptr(i)*unsafe.Sizeof(int(0))))
fmt.Println(*p)
}比索引快一点,但编译器已经会做索引优化,只有极端场景才需要。
4.3 string 转 []byte 不拷贝
// 常见 trick:unsafe 转换 string → []byte,不拷贝
func bytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func stringToBytes(s string) []byte {
b := []byte(s)
*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 16)) = len(s)
*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 24)) = cap(s)
*(*uintptr)(unsafe.Pointer(&b[0])) = *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + 16))
return b
}注意:转换后不能修改,string 是不可变的。用 unsafe 绕过了类型检查,修改会导致 undefined behavior。
五、go vet 检查
go vet 会检查常见的 unsafe 错误:
go vet ./...
# 会报告 "possible misuse of unsafe.Pointer"
# 提示你把 uintptr 存变量了一定跑 go vet,它能抓住 90% 的 unsafe 错误。
六、什么时候用 unsafe
合理场景:
- 内存对齐、结构体操作(runtime、reflect 内部)
- 零拷贝转换 string ↔ []byte(性能敏感场景)
- 与 C 交互(cgo)
- 查看底层结构(调试)
不合理场景:
- 为了酷,到处用 unsafe
- 想绕过 Go 类型系统,随意 cast
- 不知道安全规则就瞎用
七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 把 uintptr 存到变量 | GC 移动对象后指针野了 | 永远在一行表达式转换,不存 |
| 用 unsafe 修改 string 内容 | undefined behavior | 别这么干 |
| 认为 Go 对象地址不变 | Go GC 会做对象移动,地址会变 | 不依赖地址不变,用 unsafe 规则 |
相关与延伸
下一篇:cgo 深入 —— 调用 C 代码、类型转换、编译链接;unsafe 底层就是直接操作内存,和 C 的指针对比,见 C 指针基础。
一句话总结
unsafe.Pointer 与 uintptr:unsafe.Pointer 是通用指针,可以和任意 *T 互转,GC 保护;uintptr 是整数,存地址值,GC 不保护;规则:uintptr 不能持久化,只能在一个表达式里完成转换和运算;unsafe.Sizeof 计算大小,unsafe.Offsetof 计算字段偏移,unsafe.Alignof 计算对齐;合理场景是 cgo、零拷贝转换、内存对齐;go vet 会检查 unsafe 使用错误;违反安全规则就会出野指针,结果不可预测。