IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    smallnest发表于 2024-08-08 15:14:22
    love 0

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

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

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

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

    在本文中,你将了解到:

    1. 如何通过 hack 的方式访问外部结构体的私有字段
    2. 如何通过 hack 的方式设置外部结构体的私有字段
    3. 如何通过 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()) // 47, false
    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()) // 47, false
    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(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 包,在它之下定义了两个结构体:

    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, // 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字段:

    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)
    // 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}
    }

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

    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()) // 30, false

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

    1
    原始值: 30, CanSet: false

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

    设置结构体的私有字段

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

    1
    age.SetInt(50)

    或者

    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(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的字段:

    1
    2
    3
    4
    5
    func (v Value) Set(x Value) {
    v.mustBeAssignable()
    x.mustBeExported() // do not let unexported x leak
    ...
    }

    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})
    }
    // 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包含两个字段,这两个字段都是私有的:

    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()))
    // 修改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,如果是,就会报错,下面就是一个例子:

    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(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值被识别为私有字段,它是不能用来赋值给公开字段的。

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

    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")
    // 修改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的值设置值:

    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()) // 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: %v\n", v.CanSet()) // true
    v.SetInt(50)
    fmt.Printf("修改后的值: %d\n", v.Int()) // 50
    }

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

    回顾

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

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

    我是鸟窝,一位老程序员,在百度写代码。如果你感觉这篇文章给你带来了帮助,请点击下方点赞按钮或者评论区进行评论。



沪ICP备19023445号-2号
友情链接