每日一Go-35、深入Go-unsafe 包 —— 指针操作、内存布局、slice/string hack、注意事项

10 阅读4分钟

    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”获取源码

2、pan.baidu.com/s/1B6pgLWfS…

unsafe.Pointer 就像一把能打开任何房门的万能钥匙——强大无比,但用错一次就可能把自己锁在危险的地方。


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!