golang利用unsafe操作未导出变量-Pointer使用详解

2020-01-28 13:07:26王旭

前言

unsafe.Pointer其实就是类似C的void *,在golang中是用于各种指针相互转换的桥梁。uintptr是golang的内置类型,是能存储指针的整型,uintptr的底层类型是int,它和unsafe.Pointer可相互转换。uintptr和unsafe.Pointer的区别就是:unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象,uintptr类型的目标会被回收。golang的unsafe包很强大,基本上很少会去用它。它可以像C一样去操作内存,但由于golang不支持直接进行指针运算,所以用起来稍显麻烦。

切入正题。利用unsafe包,可操作私有变量(在golang中称为“未导出变量”,变量名以小写字母开始),下面是具体例子。

在$GOPATH/src下建立poit包,并在poit下建立子包p,目录结构如下:

$GOPATH/src

----poit

--------p

------------v.go

--------main.go

以下是v.go的代码:


package p

import (
 "fmt"
)

type V struct {
 i int32
 j int64
}

func (this V) PutI() {
 fmt.Printf("i=%dn", this.i)
}

func (this V) PutJ() {
 fmt.Printf("j=%dn", this.j)
}

意图很明显,我是想通过unsafe包来实现对V的成员i和j赋值,然后通过PutI()和PutJ()来打印观察输出结果。

以下是main.go源代码:


package main

import (
 "poit/p"
 "unsafe"
)

func main() {
 var v *p.V = new(p.V)
 var i *int32 = (*int32)(unsafe.Pointer(v))
 *i = int32(98)
 var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))
 *j = int64(763)
 v.PutI()
 v.PutJ()
}

当然会有些限制,比如需要知道结构体V的成员布局,要修改的成员大小以及成员的偏移量。我们的核心思想就是:结构体的成员在内存中的分配是一段连续的内存,结构体中第一个成员的地址就是这个结构体的地址,您也可以认为是相对于这个结构体偏移了0。相同的,这个结构体中的任一成员都可以相对于这个结构体的偏移来计算出它在内存中的绝对地址。

具体来讲解下main方法的实现:


var v *p.V = new(p.V)

new是golang的内置方法,用来分配一段内存(会按类型的零值来清零),并返回一个指针。所以v就是类型为p.V的一个指针。


var i *int32 = (*int32)(unsafe.Pointer(v))

将指针v转成通用指针,再转成int32指针。这里就看到了unsafe.Pointer的作用了,您不能直接将v转成int32类型的指针,那样将会panic。刚才说了v的地址其实就是它的第一个成员的地址,所以这个i就很显然指向了v的成员i,通过给i赋值就相当于给v.i赋值了,但是别忘了i只是个指针,要赋值得解引用。