沒有什麼不可能:修改 Go 結構體的私有字段

在 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,
 }
}

注意Personage字段是私有的,TeacherAge字段是公開的。

在我們的main函數中,你不能訪問Personage字段:

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.ValueSet方法會做一系列的檢查,包括檢查是否是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.LockMutex.Unlock來間接的修改這兩個字段。

現在我們演示通過 hack 的方式修改Mutexstate字段的值:

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

原因aliceage值被識別爲私有字段,它是不能用來賦值給公開字段的。

有了上一節的經驗,我們同樣可以繞過這個檢查,實現這個賦值:

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

❺ 處我們修改了aliceAgeflag字段,去掉了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)(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 庫,或者你就像突破限制,訪問第三方不願意公開的字段,你有可能會採用這種非常規的技術。



本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8HmuQJMapxyX7xVfVN_xpg