Go语言中,结构体(struct)中的字段如果是私有的,只能在定义该结构体的同一个包内访问。这是为了实现数据的封装和信息隐藏,提高代码的健壮性和安全性。

但是在某些情况下,我们可能需要在外部包中访问或修改结构体的私有字段。这时,我们可以使用Go语言提供的反射(reflect)机制来实现这一功能。

即使我们能够实现访问,这些字段你没有办法修改,如果尝试通过反射设置这些私有字段的值,会panic。

甚至有时,我们通过反射设置一些变量或者字段的值的时候,会panic,报错panic:reflect:reflect.Value.Set using unaddressable value

在本文中,你将了解到:

  1. 如何通过hack的方式访问外部结构体的私有字段
  2. 如何通过hack的方式设置外部结构体的私有字段
  3. 如何通过hack的方式设置unaddressable的值

首先我先介绍通过反射设置值遇到的unaddressable的困境。

通过反射设置一个变量的值

如果你使用过反射设置值的变量,你可能熟悉下面的代码,而且这个代码工作正常:

var x=47

v:=reflect.ValueOf(&x).Elem()

fmt.Printf("原始值:%d,CanSet:%vn",v.Int(),v.CanSet())//47,false

v.Set(reflect.ValueOf(50))

注意这里传入给reflect.ValueOf的是x的指针&x,所以这个Value值是addresable的,我们可以进行赋值。

如果把&x替换成x,我们再尝试运行:

var x=47

v:=reflect.ValueOf(x)

fmt.Printf("Original value:%d,CanSet:%vn",v.Int(),v.CanSet())//47,false

v.Set(reflect.ValueOf(50))

可以看到panic:

Original value:47,CanSet:false

panic:reflect:reflect.Value.Set using unaddressable value

goroutine 1[running]:

reflect.flag.mustBeAssignableSlow(0x1400012c410?)

/usr/local/go/src/reflect/value.go:272+0x74

reflect.flag.mustBeAssignable(...)

/usr/local/go/src/reflect/value.go:259

reflect.Value.Set({0x104e13e40?,0x104e965b8?,0x104dec7e6?},{0x104e13e40?,0x104e0ada0?,0x2?})

/usr/local/go/src/reflect/value.go:2319+0x58

main.setUnaddressableValue()

/Users/smallnest/workspace/study/private/main.go:27+0x1c0

main.main()

/Users/smallnest/workspace/study/private/main.go:18+0x1c

exit status 2

文章最后我会介绍如何通过hack的方式解决这个问题。

接下来我再介绍访问私有字段的问题。

访问外部包的结构体的私有字段

我们先准备一个model包,在它之下定义了两个结构体:

package model

type Person struct{

Name string

age int

}

func NewPerson(name string,age int)Person{

return Person{

Name:name,

age:age,//unexported field

}

}

type Teacher struct{

Name string

Age int//exported field

}

func NewTeacher(name string,age int)Teacher{

return Teacher{

Name:name,

Age:age,

}

}

注意Person的age字段是私有的,Teacher的Age字段是公开的。

在我们的main函数中,你不能访问Person的age字段:

package main;

import(

"fmt"

"reflect"

"unsafe"

"github.com/smallnest/private/model"

)

func main(){

p:=model.NewPerson("Alice",30)

fmt.Printf("Person:%+vn",p)

//fmt.Println(p.age)//error:p.age undefined(cannot refer to unexported field or method age)

t:=model.NewTeacher("smallnest",18)

fmt.Printf("Teacher:%+vn",t)//Teacher:{Name:Alice Age:30}

}

那么真的就无法访问了吗?也不一定,我们可以通过反射的方式访问私有字段:

p:=model.NewPerson("Alice",30)

age:=reflect.ValueOf(p).FieldByName("age")

fmt.Printf("原始值:%d,CanSet:%vn",age.Int(),age.CanSet())//30,false

运行这个程序,可以看到我们获得了这个私有字段age的值:

原始值:30,CanSet:false

这样我们就绕过了Go语言的访问限制,访问了私有字段。

设置结构体的私有字段

但是如果我们尝试修改这个私有字段的值,会panic:

age.SetInt(50)

或者

age.Set(reflect.ValueOf(50))

报错信息:

原始值:30,CanSet:false

panic:reflect:reflect.Value.SetInt using value obtained using unexported field

goroutine 1[running]:

reflect.flag.mustBeAssignableSlow(0x2?)

/usr/local/go/src/reflect/value.go:269+0xb4

reflect.flag.mustBeAssignable(...)

/usr/local/go/src/reflect/value.go:259

reflect.Value.SetInt({0x1050ac0c0?,0x14000118f20?,0x1050830a8?},0x32)

/usr/local/go/src/reflect/value.go:2398+0x44

main.setUnexportedField()

/Users/smallnest/workspace/study/private/main.go:37+0x1a0

main.main()

/Users/smallnest/workspace/study/private/main.go:18+0x1c

exit status 2

实际上,reflect.Value的Set方法会做一系列的检查,包括检查是否是addressable的,以及是否是exported的字段:

func(v Value)Set(x Value){

v.mustBeAssignable()

x.mustBeExported()//do not let unexported x leak

...

}

v.mustBeAssignable()检查是否是addressable的,而且是exported的字段:

func(f flag)mustBeAssignable(){

if f&flagRO!=0||f&flagAddr==0{

f.mustBeAssignableSlow()

}

}

func(f flag)mustBeAssignableSlow(){

if f==0{

panic(&ValueError{valueMethodName(),Invalid})

}

//Assignable if addressable and not read-only.

if f&flagRO!=0{

panic("reflect:"+valueMethodName()+"using value obtained using unexported field")

}

if f&flagAddr==0{

panic("reflect:"+valueMethodName()+"using unaddressable value")

}

}

f&flagRO==0代表是可写的(exported),f&flagAddr!=0代表是addressable的,当这两个条件任意一个不满足时,就会报错。

既然我们明白了它检查的原理,我们就可以通过hack的方式绕过这个检查,设置私有字段的值。我们还是要使用unsafe代码。

这里我们以标准库的sync.Mutex结构体为例,sync.Mutex包含两个字段,这两个字段都是私有的:

type Mutex struct{

state int32

sema uint32

}

正常情况下你只能通过Mutex.Lock和Mutex.Unlock来间接的修改这两个字段。

现在我们演示通过hack的方式修改Mutex的state字段的值:

func setPrivateField(){

var mu sync.Mutex

mu.Lock()

field:=reflect.ValueOf(&mu).Elem().FieldByName("state")

state:=field.Interface().(*int32)

fmt.Println(*state)//❶

flagField:=reflect.ValueOf(&field).Elem().FieldByName("flag")

flagPtr:=(*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))

//修改flag字段的值

*flagPtr&=^uintptr(flagRO)//❷

field.Set(reflect.ValueOf(int32(0)))

mu.Lock()//❸

fmt.Println(*state)

}

type flag uintptr

const(

flagKindWidth=5//there are 27 kinds

flagKindMask flag=1<<flagKindWidth-1

flagStickyRO flag=1<<5

flagEmbedRO flag=1<<6

flagIndir flag=1<<7

flagAddr flag=1<<8

flagMethod flag=1<<9

flagMethodShift=10

flagRO flag=flagStickyRO|flagEmbedRO

)

❶处我们已经介绍过了,访问私有字段的值,这里会打印出1

❶处我们清除了flag字段的flagRO标志位,这样就不会报reflect:reflect.Value.SetInt using value obtained using unexported field错误了

❸处不会导致二次加锁带来的死锁,因为state字段的值已经被修改为0了,所以不会阻塞。最后打印结果还是1

这样我们就可以实现了修改私有字段的值了。

使用unexported字段的Value设置公开字段

看reflect.Value.Set的源码,我们可以看到它会检查参数的值是否unexported,如果是,就会报错,下面就是一个例子:

func setUnexportedField2(){

alice:=model.NewPerson("Alice",30)

bob:=model.NewTeacher("Bob",40)

bobAgent:=reflect.ValueOf(&bob).Elem().FieldByName("Age")

aliceAge:=reflect.ValueOf(&alice).Elem().FieldByName("age")

bobAgent.Set(aliceAge)//❹

}

注意❹处,我们尝试把alice的私有字段age的值赋值给bob的公开字段Age,这里会报错:

panic:reflect:reflect.Value.Set using value obtained using unexported field

goroutine 1[running]:

reflect.flag.mustBeExportedSlow(0x1400012a000?)

/usr/local/go/src/reflect/value.go:250+0x70

reflect.flag.mustBeExported(...)

/usr/local/go/src/reflect/value.go:241

reflect.Value.Set({0x102773a60?,0x1400012a028?,0x60?},{0x102773a60?,0x1400012a010?,0x1027002b8?})

/usr/local/go/src/reflect/value.go:2320+0x88

main.setUnexportedField2()

/Users/smallnest/workspace/study/private/main.go:50+0x168

main.main()

/Users/smallnest/workspace/study/private/main.go:18+0x1c

exit status 2

原因alice的age值被识别为私有字段,它是不能用来赋值给公开字段的。

有了上一节的经验,我们同样可以绕过这个检查,实现这个赋值:

func setUnexportedField2(){

alice:=model.NewPerson("Alice",30)

bob:=model.NewTeacher("Bob",40)

bobAgent:=reflect.ValueOf(&bob).Elem().FieldByName("Age")

aliceAge:=reflect.ValueOf(&alice).Elem().FieldByName("age")

//修改flag字段的值

flagField:=reflect.ValueOf(&aliceAge).Elem().FieldByName("flag")

flagPtr:=(*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))

*flagPtr&=^uintptr(flagRO)//❺

bobAgent.Set(reflect.ValueOf(50))

bobAgent.Set(aliceAge)//❻

}

❺处我们修改了aliceAge的flag字段,去掉了flagRO标志位,这样就不会报错了,❻处我们成功的把alice的私有字段age的值赋值给bob的公开字段Age。

这样我们就可以实现了使用私有字段的值给其他Value值进行赋值了。

给unaddressable的值设置值

回到最初的问题,我们尝试给一个unaddressable的值设置值,会报错。

结合上面的hack手段,我们也可以绕过限制,给unaddressable的值设置值:

func setUnaddressableValue(){

var x=47

v:=reflect.ValueOf(x)

fmt.Printf("原始值:%d,CanSet:%vn",v.Int(),v.CanSet())//47,false

//v.Set(reflect.ValueOf(50))

flagField:=reflect.ValueOf(&v).Elem().FieldByName("flag")

flagPtr:=(*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))

//修改flag字段的值

*flagPtr|=uintptr(flagAddr)//设置可寻址标志位

fmt.Printf("CanSet:%vn",v.CanSet())//true

v.SetInt(50)

fmt.Printf("修改后的值:%dn",v.Int())//50

}

运行这个程序,不会报错,可以看到我们成功的给unaddressable的值设置了新的值。

回顾

我们通过修改Value值的flag标志位,可以绕过reflect的检查,实现了访问私有字段、设置私有字段的值、用私有字段设置值,以及给unaddressable的值设置值。

这些都是unsafe的方式,一般情况下不鼓励进行这样的hack操作,但是这种技术也不是完全没有用户,如果你正在写一个debugger,用户在断点出可能想修改某些值,或者你在写深拷贝的库,或者编写某种ORM库,或者你就像突破限制,访问第三方不愿意公开的字段,你有可能会采用这种非常规的技术。