专栏名称: GoCN
最具规模和生命力的 Go 开发者社区
目录
相关文章推荐
环球人物  ·  “全世界最强壮的男孩”神秘消失始末 ·  昨天  
南方人物周刊  ·  宋佳 永远生机勃勃|2024魅力人物 ·  2 天前  
每日人物  ·  机票跌到200块,我却高兴不起来 ·  2 天前  
人物  ·  中年后,我还有哭的自由吗? ·  2 天前  
南方人物周刊  ·  哪吒敖丙“藕饼”大火,“得CP者得天下”? ·  3 天前  
51好读  ›  专栏  ›  GoCN

没有什么不可能:修改 Go 结构体的私有字段

GoCN  · 公众号  ·  · 2024-08-12 11:58

正文

在 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: %v\n", 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: %v\n", 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: %+v\n", 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: %+v\n", t) // Teacher: {Name:Alice Age:30}
}

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

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

 age := reflect.ValueOf(p).FieldByName("age")
 fmt.Printf("原始值: %d, CanSet: %v\n", 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<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: %v\n", v.Int(), v.CanSet()) // 47, false
 // v.Set(reflect.ValueOf(50))

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






请到「今天看啥」查看全文