How to Hack Go: Access and Modify Private Struct Fields with Reflection and Unsafe
This article demonstrates step‑by‑step how Go's reflection and unsafe packages can be leveraged to bypass the language's encapsulation, allowing reading, writing, and even assigning unaddressable private struct fields by manipulating internal flag bits and using low‑level pointer tricks.
Why the usual reflection fails
When you try to set a value using reflect.Value.Set on a non‑addressable or unexported field, Go panics with messages such as reflect: reflect.Value.Set using unaddressable value or
reflect: reflect.Value.Set using value obtained using unexported field. The runtime checks two conditions: the value must be addressable and the field must be exported . If either condition is false, Set calls mustBeAssignable or mustBeExported, which trigger a panic.
Reading a private field via reflection
First, create a package model with two structs, one with an unexported field:
package model
type Person struct {
Name string
age int // unexported field
}
func NewPerson(name string, age int) Person {
return Person{Name: name, age: age}
}
type Teacher struct {
Name string
Age int // exported field
}
func NewTeacher(name string, age int) Teacher {
return Teacher{Name: name, Age: age}
}In main, the unexported age cannot be accessed directly:
package main
import (
"fmt"
"reflect"
"unsafe"
"github.com/smallnest/private/model"
)
func main() {
p := model.NewPerson("Alice", 30)
fmt.Printf("Person: %+v
", p)
// fmt.Println(p.age) // compile error: p.age undefined
t := model.NewTeacher("smallnest", 18)
fmt.Printf("Teacher: %+v
", t)
}Using reflection we can read the value:
p := model.NewPerson("Alice", 30)
age := reflect.ValueOf(p).FieldByName("age")
fmt.Printf("original: %d, CanSet: %v
", age.Int(), age.CanSet()) // 30, falseThe call succeeds because FieldByName returns a Value that knows the field exists, but
CanSet</b> is false, indicating it is not writable.</p>
<h2>Why setting the field panics</h2>
<p>Attempting to assign triggers the panic shown earlier because the value is both unexported and unaddressable. The source of the check is in the Go runtime (<code>reflect/value.go), where Set calls mustBeAssignable and mustBeExported.
Bypassing the checks with unsafe
The trick is to flip the internal flag bits that mark a reflect.Value as addressable and exported. The flag field is unexported, so we obtain its address via reflection and then cast it to *uintptr using unsafe.Pointer:
func setPrivateField() {
var mu sync.Mutex
mu.Lock()
field := reflect.ValueOf(&mu).Elem().FieldByName("state")
state := field.Interface().(*int32)
fmt.Println(*state) // prints 1
// Obtain the flag field of the reflect.Value
flagField := reflect.ValueOf(&field).Elem().FieldByName("flag")
flagPtr := (*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))
// Clear the read‑only flag
*flagPtr &= ^uintptr(flagRO)
field.Set(reflect.ValueOf(int32(0)))
mu.Lock() // now works because state is 0
fmt.Println(*state)
}
// flag constants from the runtime (simplified)
const (
flagRO = flagStickyRO | flagEmbedRO
flagAddr = 1 << 8
// other flag definitions omitted for brevity
)Steps explained:
Obtain a reflect.Value for the target struct ( mu).
Extract the internal flag field via another reflection on the Value itself.
Clear the flagRO bit, which tells the runtime the field is not read‑only.
Now Set succeeds, modifying the private state field.
Assigning an unexported value to an exported field
Even after reading a private field, using it directly as the source for Set on an exported field still panics because the source Value is marked unexported. The same flag‑clearing technique can be applied to the source value:
func setUnexportedField2() {
alice := model.NewPerson("Alice", 30)
bob := model.NewTeacher("Bob", 40)
bobAge := reflect.ValueOf(&bob).Elem().FieldByName("Age")
aliceAge := reflect.ValueOf(&alice).Elem().FieldByName("age")
// Make aliceAge appear exported
flagField := reflect.ValueOf(&aliceAge).Elem().FieldByName("flag")
flagPtr := (*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))
*flagPtr &= ^uintptr(flagRO)
bobAge.Set(reflect.ValueOf(50)) // works
bobAge.Set(aliceAge) // now works as well
}Clearing flagRO on aliceAge removes the export restriction, allowing the value to be assigned to bobAge.
Setting a truly unaddressable value
When the original reflect.Value is created from a non‑pointer variable (e.g., reflect.ValueOf(x)), it is unaddressable. By toggling the flagAddr bit we can make it addressable:
func setUnaddressableValue() {
var x = 47
v := reflect.ValueOf(x)
fmt.Printf("original: %d, CanSet: %v
", v.Int(), v.CanSet()) // 47, false
// Flip the addressable flag
flagField := reflect.ValueOf(&v).Elem().FieldByName("flag")
flagPtr := (*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))
*flagPtr |= uintptr(flagAddr)
fmt.Printf("CanSet after: %v
", v.CanSet()) // true
v.SetInt(50)
fmt.Printf("after: %d
", v.Int()) // 50
}After the flag manipulation, SetInt succeeds without panic.
Takeaways
By directly mutating the internal flag bits of reflect.Value objects using unsafe, we can:
Read private struct fields.
Write private fields despite the language's protection.
Use an unexported field's value as the source for another assignment.
Make an otherwise unaddressable value settable.
These techniques are powerful but dangerous; they should only be used in specialized tools such as debuggers, deep‑copy libraries, or ORM frameworks where breaking encapsulation is justified.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
BirdNest Tech Talk
Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
