在Go语言中,结构体(struct)中的字段如果是私有的,只能在定义该结构体的同一个包内访问。这是为了实现数据的封装和信息隐藏,提高代码的健壮性和安全性。
但是在某些情况下,我们可能需要在外部包中访问或修改结构体的私有字段。这时,我们可以使用 Go 语言提供的反射(reflect)机制来实现这一功能。
即使我们能够实现访问,这些字段你没有办法修改,如果尝试通过反射设置这些私有字段的值,会 panic。
甚至有时,我们通过反射设置一些变量或者字段的值的时候,会 panic, 报错 panic: reflect: reflect.Value.Set using unaddressable value
。
在本文中,你将了解到:
如何通过 hack 的方式访问外部结构体的私有字段
如何通过 hack 的方式设置外部结构体的私有字段
如何通过 hack 的方式设置 unaddressable 的值
首先我先介绍通过反射设置值遇到的 unaddressable 的困境。
通过反射设置一个变量的值
如果你使用过反射设置值的变量,你可能熟悉下面的代码,而且这个代码工作正常:
1
2
3
4
5
var x = 47
v := reflect.ValueOf(&x).Elem()
fmt.Printf("原始值: %d, CanSet: %v\n" , v.Int(), v.CanSet())
v.Set(reflect.ValueOf(50 ))
注意这里传入给 reflect.ValueOf
的是 x 的指针 &x
, 所以这个 Value 值是 addresable
的,我们可以进行赋值。
如果把 &x
替换成 x
, 我们再尝试运行:
1
2
3
4
5
var x = 47
v := reflect.ValueOf(x)
fmt.Printf("Original value: %d, CanSet: %v\n" , v.Int(), v.CanSet())
v.Set(reflect.ValueOf(50 ))
可以看到panic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Original value: 47 , CanSet: false
panic: reflect: reflect.Value.Set using unaddressable value
goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0 x1400012c410?)
/usr/ local/go/ src/reflect/ value.go:272 +0 x74
reflect.flag.mustBeAssignable(...)
/usr/ local/go/ src/reflect/ value.go:259
reflect.Value.Set({0 x104e13e40?, 0 x104e965b8?, 0 x104dec7e6?}, {0 x104e13e40?, 0 x104e0ada0?, 0 x2?})
/usr/ local/go/ src/reflect/ value.go:2319 +0 x58
main.setUnaddressableValue()
/Users/ smallnest/workspace/ study/private/m ain.go:27 +0 x1c0
main.main()
/Users/ smallnest/workspace/ study/private/m ain.go:18 +0 x1c
exit status 2
文章最后我会介绍如何通过 hack 的方式解决这个问题。
接下来我再介绍访问私有字段的问题。
访问外部包的结构体的私有字段
我们先准备一个 model
包,在它之下定义了两个结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package model
type Person struct {
Name string
age int
}
func NewPerson(name string , age int ) Person {
return Person{
Name: name,
age: age,
}
}
type Teacher struct {
Name string
Age int
}
func NewTeacher(name string , age int ) Teacher {
return Teacher{
Name: name,
Age: age,
}
}
注意Person
的age
字段是私有的,Teacher
的Age
字段是公开的。
在我们的main
函数中,你不能访问Person
的age
字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main;
import (
"fmt"
"reflect"
"unsafe"
"github.com/smallnest/private/model"
)
func main() {
p := model.NewPerson("Alice" , 30 )
fmt.Printf("Person: %+v\n" , p)
t := model.NewTeacher("smallnest" , 18 )
fmt.Printf("Teacher: %+v\n" , t)
}
那么真的就无法访问了吗?也不一定,我们可以通过反射的方式访问私有字段:
1
2
3
4
p := model.NewPerson("Alice" , 30 )
age := reflect.ValueOf(p).FieldByName("age" )
fmt.Printf("原始值: %d, CanSet: %v\n" , age.Int(), age.CanSet())
运行这个程序,可以看到我们获得了这个私有字段age
的值:
这样我们就绕过了Go语言的访问限制,访问了私有字段。
设置结构体的私有字段
但是如果我们尝试修改这个私有字段的值,会 panic:
或者
1
age.Set(reflect.ValueOf(50 ))
报错信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
原始值: 30 , CanSet: false
panic: reflect: reflect.Value.SetInt using value obtained using unexported field
goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0 x2?)
/usr/ local/go/ src/reflect/ value.go:269 +0 xb4
reflect.flag.mustBeAssignable(...)
/usr/ local/go/ src/reflect/ value.go:259
reflect.Value.SetInt({0 x1050ac0c0?, 0 x14000118f20?, 0 x1050830a8?}, 0 x32)
/usr/ local/go/ src/reflect/ value.go:2398 +0 x44
main.setUnexportedField()
/Users/ smallnest/workspace/ study/private/m ain.go:37 +0 x1a0
main.main()
/Users/ smallnest/workspace/ study/private/m ain.go:18 +0 x1c
exit status 2
实际上,reflect.Value
的Set
方法会做一系列的检查,包括检查是否是addressable
的,以及是否是exported的字段:
1
2
3
4
5
func (v Value) Set(x Value) {
v.mustBeAssignable()
x.mustBeExported()
...
}
v.mustBeAssignable()
检查是否是addressable
的,而且是exported的字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (f flag) mustBeAssignable() {
if f&flagRO != 0 || f&flagAddr == 0 {
f.mustBeAssignableSlow()
}
}
func (f flag) mustBeAssignableSlow() {
if f == 0 {
panic (&ValueError{valueMethodName(), Invalid})
}
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
包含两个字段,这两个字段都是私有的:
1
2
3
4
type Mutex struct {
state int32
sema uint32
}
正常情况下你只能通过Mutex.Lock
和Mutex.Unlock
来间接的修改这两个字段。
现在我们演示通过 hack 的方式修改Mutex
的state
字段的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func setPrivateField() {
var mu sync.Mutex
mu.Lock()
field := reflect.ValueOf(μ).Elem().FieldByName("state" )
state := field.Interface().(*int32 )
fmt.Println(*state)
flagField := reflect.ValueOf(&field).Elem().FieldByName("flag" )
flagPtr := (*uintptr )(unsafe.Pointer(flagField.UnsafeAddr()))
*flagPtr &= ^uintptr (flagRO)
field.Set(reflect.ValueOf(int32 (0 )))
mu.Lock()
fmt.Println(*state)
}
type flag uintptr
const (
flagKindWidth = 5
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
,如果是,就会报错,下面就是一个例子:
1
2
3
4
5
6
7
8
9
10
11
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
,这里会报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
panic : reflect: reflect.Value.Set using value obtained using unexported field
goroutine 1 [running]:
reflect.flag.mustBeExportedSlow(0 x1400012a000?)
/usr/ local/go/ src/reflect/ value.go:250 +0 x70
reflect.flag.mustBeExported(...)
/usr/ local/go/ src/reflect/ value.go:241
reflect.Value.Set({0 x102773a60?, 0 x1400012a028?, 0 x60?}, {0 x102773a60?, 0 x1400012a010?, 0 x1027002b8?})
/usr/ local/go/ src/reflect/ value.go:2320 +0 x88
main.setUnexportedField2()
/Users/ smallnest/workspace/ study/private/m ain.go:50 +0 x168
main.main()
/Users/ smallnest/workspace/ study/private/m ain.go:18 +0 x1c
exit status 2
原因alice
的age
值被识别为私有字段,它是不能用来赋值给公开字段的。
有了上一节的经验,我们同样可以绕过这个检查,实现这个赋值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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" )
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的值设置值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func setUnaddressableValue() {
var x = 47
v := reflect.ValueOf(x)
fmt.Printf("原始值: %d, CanSet: %v\n" , v.Int(), v.CanSet())
flagField := reflect.ValueOf(&v).Elem().FieldByName("flag" )
flagPtr := (*uintptr )(unsafe.Pointer(flagField.UnsafeAddr()))
*flagPtr |= uintptr (flagAddr)
fmt.Printf("CanSet: %v\n" , v.CanSet())
v.SetInt(50 )
fmt.Printf("修改后的值: %d\n" , v.Int())
}
运行这个程序,不会报错,可以看到我们成功的给unaddressable的值设置了新的值。
回顾
我们通过修改Value
值的flag标志位,可以绕过reflect
的检查,实现了访问私有字段、设置私有字段的值、用私有字段设置值,以及给unaddressable的值设置值。
这些都是unsafe
的方式,一般情况下不鼓励进行这样的hack操作,但是这种技术也不是完全没有用户,如果你正在写一个debugger,用户在断点出可能想修改某些值,或者你在写深拷贝的库,或者编写某种ORM库,或者你就像突破限制,访问第三方不愿意公开的字段,你有可能会采用这种非常规的技术。
我是鸟窝,一位老程序员,在百度写代码。如果你感觉这篇文章给你带来了帮助,请点击下方点赞按钮或者评论区进行评论。