unsafe是Go官方提供的“逃生舱”,只要用它,你就不再受Go类型系统保护,也不再享受内存安全。但unsafe在性能敏感、系统编程、零拷贝场景中至关重要。
一、unsafe.Pointer: unsafe的核心
打开Go源码,可用看见这样地类型定义
type
Pointer
ArbitraryType
//Pointer定义为 任意类型的指针
它的本质是一个能绕过Go类型检查的通用指针。有点像C语言里的*void。
继续看源码里的注释
// Pointer represents a pointer to an arbitrary type. There are four special operations
// available for type Pointer that are not available for other types:
// - A pointer value of any type can be converted to a Pointer.
// - A Pointer can be converted to a pointer value of any type.
// - A uintptr can be converted to a Pointer.
// - A Pointer can be converted to a uintptr.
翻译过来就是:Pointer表示指向任意类型的指针。指针类型有四种特殊操作,这些操作其他类型不具备:
-
任何类型的指针值都可以转换成Pointer
-
Pointer可以转换为任何类型的指针值
-
uintptr可以转换为Pointer
-
Pointer可以转换为uintptr
总结起来就是两条:
-
*T和unsafe.Pointer可以互转;
-
unsafe.Pointer和uintptr可以互转。
1.1 *T <--> unsafe.Pointer
fmt.Println("Hello,Codee君")
var x int = 10
p := unsafe.Pointer(&x)
fmt.Println("x:", x)
fmt.Println("p:", p)
px := (*int)(p)
fmt.Println("*px:", *px)
// 输出
Hello,Codee君
x: 10
p: 0xc000110068
*px: 10
1.2 unsafe.Pointer <--> uintptr
// unsafe.Pointer <--> uintptr
addr := uintptr(p) + unsafe.Sizeof(x)
fmt.Println("addr:", addr)
p2 := unsafe.Pointer(addr)
fmt.Println("p2:", p2)
// 输出
addr: 824634335344
p2: 0xc000096070
二、内存布局
Go 不保证字段排列,但在同一版本 + 构建模式下是稳定的,因此实际工程中仍大量使用。
2.1 计算字段偏移
type CodeeJun struct {
A int32
B int64
C byte
}
func TestUnsafeOffsetof(t *testing.T) {
offsetA := unsafe.Offsetof((*CodeeJun)(nil).A)
offsetB := unsafe.Offsetof((*CodeeJun)(nil).B)
offsetC := unsafe.Offsetof((*CodeeJun)(nil).C)
log.Println(offsetA, offsetB, offsetC)
if offsetA == 0 && offsetB == 8 && offsetC == 16 {
t.Log("offset of A, B and C are correct")
} else {
t.Error("offset of B and C are not correct")
}
}
// Output:
// 0 8 16
2.2 直接按偏移读写字段(非常危险,但速度快)
func TestUnsafeOffsetofRW(t *testing.T) {
cj := CodeeJun{A: 1, B: 2, C: 3}
base := unsafe.Pointer(&cj)
offsetB := unsafe.Offsetof((*CodeeJun)(nil).B)
pb := (*int64)(unsafe.Pointer(uintptr(base) + offsetB))
*pb = 99
if *pb == 99 {
t.Log("B is written correctly")
} else {
t.Error("B is not written correctly")
}
}
三、Slice/String Hack(零拷贝技术)
这是unsafe最常用、也是最容易写出bug的地方。
3.1 string<-->[]byte 零拷贝
func TestUnsafeZeroCopy(t *testing.T) {
s := "Codee君"
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
res := *(*[]byte)(unsafe.Pointer(&bh)) //res目前是只读的
// res[0] = 67 //不能修改[]byte的值,否则会panic
log.Println(res[0])
log.Println(res)
if string(res) == "Codee君" {
t.Log("zero copy is correct")
} else {
t.Error("zero copy is not correct")
}
sb := []byte(s)
log.Println(sb)
// 真正零拷贝
sbStr := *(*string)(unsafe.Pointer(&sb))
log.Println(sbStr)
if sbStr == "Codee君" {
t.Log("zero copy is correct")
} else {
t.Error("zero copy is not correct")
}
}
3.2 slice扩容hack(手搓slice)
type CodeeJunSlice struct {
Data uintptr
Len int
Cap int
}
func MakeAnySlice[T any](addr uintptr, len, cap int) []T {
hdr := CodeeJunSlice{Data: addr, Len: len, Cap: cap}
return *(*[]T)(unsafe.Pointer(&hdr))
}
func TestUnsafeSlice(t *testing.T) {
array := [5]int{1, 2, 3, 4, 5}
addr := uintptr(unsafe.Pointer(&array[0]))
newSlice := MakeAnySlice[int](addr, 3, 5)
log.Println(newSlice)
if newSlice[0] == 1 && newSlice[1] == 2 && newSlice[2] == 3 {
t.Log("zero copy is correct")
} else {
t.Error("zero copy is not correct")
}
newSlice[0] = 9
log.Println(newSlice)
if array[0] == 9 {
t.Log("zero copy is correct")
} else {
t.Error("zero copy is not correct")
}
}
3.3 指针运算
func Add(ptr unsafe.Pointer, offset uintptr) unsafe.Pointer {
return unsafe.Pointer(uintptr(ptr) + offset)
}
func TestUnsafePointerArithmetics(t *testing.T) {
array := [5]int{1, 2, 3, 4, 5}
addr := uintptr(unsafe.Pointer(&array[0]))
secondAddr := Add(unsafe.Pointer(addr), unsafe.Sizeof(int(0)))
log.Println(secondAddr)
if *(*int)(secondAddr) == 2 {
t.Log("pointer arithmetics is correct")
} else {
t.Error("pointer arithmetics is not correct")
}
thirdAddr := Add(secondAddr, unsafe.Sizeof(int(0)))
log.Println(thirdAddr)
if *(*int)(thirdAddr) == 3 {
t.Log("pointer arithmetics is correct")
} else {
t.Error("pointer arithmetics is not correct")
}
}
四、unsafe的真实工程用途
unsafe在Go内部和高性能框架中大量使用,主要用在以下地方:
-
高性能网络框架。如 fasthttp、gnet,大量使用零拷贝 string/byte hack。
-
序列化工具。flatbuffers、msgpack-go、capnproto-go。
-
内存池。手工管理对象的生命周期、减少 GC 压力。
-
mmap/共享内存。把 mmap 后的地址映射为 slice。
-
反射内部加速。有些库会配合 unsafe + reflect 直接访问结构字段。
五、使用unsafe的注意事项与风险
unsafe 可以提升性能,但代价是放弃 Go 的内存安全。
5.1 GC不再保护你,uintptr永远不能长期持有
// ❌ GC可能会移动或清理这段内存,p就成了野指针
p := uintptr(unsafe.Pointer(ptr))
// ✔ 必须在同一行完成运算
p := unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + offset)
5.2 string是不可修改的
b := StringToBytes(s)
b[0] = 'X' // 可能崩溃
5.3 字段布局不是跨版本稳定的
struct 字段排列一般不会变,但以下操作都可能影响:
-
字段顺序变化
-
编译器版本变化
-
CPU 架构变化(32/64 bit 对齐规则不同)
-
build tag / cgo 影响
5.4 尽量不要在业务层使用unsafe
六、如何安全地写unsafe
6.1 封装成小函数,不散落在业务里
6.2 单一职责:函数只做一件事,不要又 hack 内存又顺便处理业务逻辑。
6.3 必须加注释:例如这个函数返回的slice不可修改
6.4 尽量限制在小模块内:避免后来维护者踩坑
*源码地址*
1、公众号“Codee君”回复“每日一Go”获取源码
unsafe.Pointer 就像一把能打开任何房门的万能钥匙——强大无比,但用错一次就可能把自己锁在危险的地方。
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!