沒有什麼不可能:修改 Go 結構體的私有字段
在 Go 語言中, 結構體 (struct) 中的字段如果是私有的, 只能在定義該結構體的同一個包內訪問。這是爲了實現數據的封裝和信息隱藏, 提高代碼的健壯性和安全性。
但是在某些情況下, 我們可能需要在外部包中訪問或修改結構體的私有字段。這時, 我們可以使用 Go 語言提供的反射 (reflect) 機制來實現這一功能。
即使我們能夠實現訪問,這些字段你沒有辦法修改,如果嘗試通過反射設置這些私有字段的值,會 panic。
甚至有時,我們通過反射設置一些變量或者字段的值的時候,會 panic, 報錯 panic: reflect: reflect.Value.Set using unaddressable value
。
在本文中,你將瞭解到:
-
如何通過 hack 的方式訪問外部結構體的私有字段
-
如何通過 hack 的方式設置外部結構體的私有字段
-
如何通過 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<<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: %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