当前位置 博文首页 > golang unsafe.Pointer与uintptr

    golang unsafe.Pointer与uintptr

    作者:orlion 时间:2021-01-06 12:01

    原文地址:https://blog.fanscore.cn/p/33/

    先说结论

    • uintptr 是一个地址数值,它不是指针,与地址上的对象没有引用关系,垃圾回收器不会因为有一个uintptr类型的值指向某对象而不回收该对象。
    • unsafe.Pointer是一个指针,类似于C的void *,它与地址上的对象存在引用关系,垃圾回收器因为有一个unsafe.Pointer类型的值指向某对象而不回收该对象。
    • 任何指针都可以转为unsafe.Pointer
    • unsafe.Pointer可以转为任何指针
    • uintptr可以转换为unsafe.Pointer
    • unsafe.Pointer可以转换为uintptr
    • 指针不能直接转换为uintptr

    为什么需要uintptr这个类型呢?

    理论上说指针不过是一个数值,即一个uint,但实际上在go中unsafe.Pointer是不能通过强制类型转换为一个uint的,只能将unsafe.Pointer强制类型转换为一个uintptr。

    var v1 float64 = 1.1
    var v2 *float64 = &v1
    _ = int(v2) // 这里编译报错:cannot convert unsafe.Pointer(v2) (type unsafe.Pointer) to type uint
    

    但是可以将一个unsafe.Pointer强制类型转换为一个uintptr:

    var v1 float64 = 1.1
    var v2 *float64 = &v1
    var v3 uintptr = uintptr(unsafe.Pointer(v2))
    v4 := uint(v3)
    fmt.Println(v3, v4) // v3和v4打印出来的值是相同的
    

    可以理解为uintptr是专门用来指针操作的uint。
    另外需要指出的是指针不能直接转为uintptr,即

    var a float64
    uintptr(&a) 这里会报错,不允许将*float64转为uintptr
    

    一个??

    通过上面的描述如果你还是一头雾水的话,不妨看下下面这个实际案例:

    package foo
    
    type Person struct {
    	Name string
    	age  int
    }
    

    上面的代码中我们在foo包中定义了一个结构体Person,只导出了Name字段,而没有导出age字段,就是说在另外的包中我们只能直接操作Person.Name而不能直接操作Person.age,但是利用unsafe包可以绕过这个限制使我们能够操作Person.age

    package main
    
    func main() {
    	p := &foo.Person{
    		Name: "张三",
    	}
    
    	fmt.Println(p)
    	// *Person是不能直接转换为*string的,所以这里先将*Person转为unsafe.Pointer,再将unsafe.Pointer转为*string
    	pName := (*string)(unsafe.Pointer(p)) 
    	*pName = "李四"
    
    	// 正常手段是不能操作Person.age的这里先通过uintptr(unsafe.Pointer(pName))得到Person.Name的地址
    	// 通过unsafe.Sizeof(p.Name)得到Person.Name占用的字节数
    	// Person.Name的地址 + Person.Name占用的字节数就得到了Person.age的地址,然后将地址转为int指针。
    	pAge := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))))
    	// 将p的age字段修改为12
    	*pAge = 12
    
    	fmt.Println(p)
    }
    

    打印结果为:

    $ go run main.go
    &{张三 0}
    &{李四 12}
    

    需要注意的是下面这段代码比较长:

    pAge := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))))
    

    但是尽量不要分成两段代码,像这样:

    temp := uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))
    pAge := (*int)(unsafe.Pointer(temp)
    

    原因是在第二行语句时,已经没有指针指向p了,这时p可能会回收掉了,这时得到的地址temp就是个野指针了,不知道指向谁了,是比较危险的。

    另外一个原因是在当前Go(golang版本:1.14)的内存管理机制中不会迁移内存,但是不保证以后的版本内存管理机制中有迁移内存的操作,一旦发生了内存迁移指针地址发生变更,上面的分段代码就有可能出现严重问题。

    关于Go的内存管理可以参看这篇文章:https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/,读完这篇文章相信你就能理解上面的内存迁移问题。

    除了上面两点外还有一个原因是在Go 1.3上,当栈需要增长时栈可能会发生移动,对于下面的代码:

    var obj int
    fmt.Println(uintptr(unsafe.Pointer(&obj)))
    bigFunc() // bigFunc()增大了栈
    fmt.Println(uintptr(unsafe.Pointer(&obj)))
    

    完全有可能打印出来两个地址。

    通过上面的例子应该明白了为什么这个包名为unsafe,因为使用起来确实有风险,所以尽量不要使用这个包。

    我之所以研究unsafe.Pointer完全是因为我要在多线程的环境中采用原子操作避免竞争问题,所以我用到了atomic.LoadPointer(addr *unsafe.Pointer)。不过我后面发现了atomic包提供了一个atomic.Value结构体,这个结构体提供的方法使我避免显式使用了unsafe.Pointer。所以你也正在使用atomic.LoadPointer()不妨看看atomic.Value是不是可以解决你的问题,这是我一点提醒。

    参考资料

    • Go语言实战笔记(二十七)| Go unsafe Pointer
    • garbage collection - Does Go guarantee constant addresses? - Stack Overflow
    下一篇:没有了