In Go, struct fields can be private (unexported) or public (exported). Unexported fields begin with a lowercase letter and are meant to be inaccessible outside the package they are defined in. However, by using the reflect
and unsafe
packages, you can bypass these restrictions. While this approach can be useful for testing or debugging, it should be used sparingly and responsibly as it breaks encapsulation and can lead to unpredictable behavior.
In this blog, we’ll explore how to modify private variables of a struct using unsafe
and reflect
. We’ll also dive into the internals of how these packages work and provide clear examples, including memory layout details and the use of unsafe.NewAt
.
In Go, a struct is a contiguous block of memory where its fields are laid out sequentially. Each field has a memory offset from the start of the struct. Exported and unexported fields are treated the same in memory — the difference is purely at the language level enforced by the compiler.
package main
type Example struct {
PublicField string
privateField int // unexported field
}
The memory layout of Example
would look something like this:
| PublicField (string) |
| privateField (int) |
Even though privateField
is unexported, its data is still present in memory.
To better understand how structs are stored in memory, consider the following:
package main
import (
"fmt"
"unsafe"
)
type Example struct {
PublicField string
privateField int
}
func main() {
ex := Example{
PublicField: "Hello",
privateField: 42,
}
fmt.Printf("Struct memory layout:\n")
fmt.Printf("Address of publicField: %p\n", unsafe.Pointer(&ex.PublicField))
fmt.Printf("Address of privateField: %p\n", unsafe.Pointer(&ex.privateField))
}
Struct memory layout:
Address of publicField: 0xc000014070
Address of privateField: 0xc000014080
Here, the fields are laid out sequentially, but there may be padding between them to satisfy alignment requirements.
The reflect
package in Go provides the ability to inspect and manipulate the fields of a struct at runtime. However, it enforces visibility rules—unexported fields cannot be accessed directly. This restriction can be bypassed by pairing reflect
with unsafe
.
Key components of reflect
we’ll use:
reflect.Value
: Represents the value of a Go variable.
reflect.StructField
: Describes a struct field, including its offset.
package main
import (
"fmt"
"reflect"
)
type Example struct {
PublicField string
privateField int
}
func main() {
ex := Example{
publicField: "Hello",
privateField: 42,
}
// Use reflection to inspect the fields
t := reflect.TypeOf(ex)
v := reflect.ValueOf(ex)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("Field: %s, Value: %v, Exported: %t\n", field.Name, value, field.IsExported())
}
}
This will print:
Field: publicField, Value: Hello, Exported: true
Field: privateField, Value: 42, Exported: false
While we can read privateField
, modifying it will result in a panic unless we use unsafe
.
The unsafe
package allows you to perform low-level memory operations. It provides tools to:
Obtain the memory address of a variable.
Convert between types without type safety.
Create pointers to arbitrary memory locations using unsafe.NewAt
.
Key types and functions:
unsafe.Pointer
: A generic pointer type.
uintptr
: An integer type that can hold pointer values.
unsafe.NewAt
: Constructs a pointer to a value at a specific memory address.
Using reflect
, we can obtain the memory offset of a struct field. Then, using unsafe.Pointer
, we can calculate the field’s address and modify its value.
reflect.Value
of the struct.reflect.Value.Field
to get a reflect.Value
of the private field.fieldValue.CanSet()
.UnsafeAddr
method to obtain the memory address of the field.unsafe.Pointer
and modify the value. package main
import (
"fmt"
"reflect"
"unsafe"
)
type Example struct {
PublicField string
privateField int
}
func main() {
ex := Example{
PublicField: "Hello",
privateField: 42,
}
fmt.Printf("Before modification: %+v\n", ex)
// Step 1: Get the reflect.Value of the struct
v := reflect.ValueOf(&ex).Elem()
// Step 2: Iterate through fields
for i := 0; i < v.NumField(); i++ {
fieldValue := v.Field(i)
if !fieldValue.CanSet() {
// Step 3: Access private fields using unsafe
fieldValue = reflect.NewAt(fieldValue.Type(), unsafe.Pointer(fieldValue.UnsafeAddr())).Elem()
}
// Step 4: Modify field values
switch fieldValue.Kind() {
case reflect.Int:
fieldValue.SetInt(99)
case reflect.String:
fieldValue.SetString("Updated")
}
}
fmt.Printf("After modification: %+v\n", ex)
}
Before modification: {PublicField:Hello privateField:42}
After modification: {PublicField:Updated privateField:99}
Here, reflect.NewAt
creates a pointer to the memory location of each field, allowing us to modify unexported fields safely.
reflect
Worksreflect.Value
internally stores the type and a pointer to the data.unsafe
Worksunsafe.Pointer
allows you to treat memory addresses as arbitrary types.Consider this struct:
type Example struct {
A int32
B int64
}
Memory offsets:
| A (int32) | Padding (4 bytes) | B (int64) |
The padding ensures B is aligned to an 8-byte boundary, which is required for int64
.
Using unsafe
and reflect
, you can access and modify private fields in Go structs. While this is a powerful capability, it comes with significant risks and should only be used when absolutely necessary. Always prefer idiomatic Go practices and reserve these techniques for cases where no other solutions exist.