Go Reflect and Unsafe: Under the Hood of Go

Go Reflect & Unsafe: The Deep-Space Mirror
The modules we've covered so far have focused on Go's safe, idiomatic path. But sometimes, especially when building generic libraries, ORMs, or high-performance networking stacks, you need to go deeper.
This post covers two of the most powerful and controversial tools in the Go arsenal: the reflect package for inspecting types at runtime, and the unsafe package for bypassing Go's memory safety rules.
What Are Go's reflect and unsafe Packages?
Go's reflect package lets you inspect and manipulate variables at runtime without knowing their types at compile time — enabling generic serialization, ORM field mapping, and meta-programming. The unsafe package bypasses Go's type system entirely, allowing raw pointer arithmetic and zero-copy type conversions for maximum performance. Both should be used sparingly and only when standard language features cannot solve the problem.
The reflect package can be slow and brittle, while unsafe can cause your program to crash in ways that are hard to debug. As a rule of thumb, only use these tools if you are building foundational infrastructure where standard features are insufficient.
1. The Deep-Space Mirror: Type-Check Physics
Reflection in Go is the process of inspecting the Interface Tag Mirror.
The Inspection Physics
- The Empty Interface Mirror: Every
interface{}is actually a pair of pointers: one to the type information and one to the data. Reflection "splits" this mirror to see the underlying silicon. - The Cost of Discovery: Unlike static code where the compiler knows the type, reflection must perform Runtime Discovery. This requires traversing the internal type mirror (Module 7), which is
10-50xslower than static calls. - The Result: You can build "Universal Mirrors" (like JSON encoders) that work for any struct without knowing its layout at compile time.
2. Inspecting the Unknown: reflect
Reflection is the ability of a program to examine its own structure, particularly through types. In Go, every variable can be represented as an interface{}, which the reflect package can then pull apart.
func main() {
u := User{ID: 1, Name: "Alice"}
// Get the Type and Value
val := reflect.ValueOf(u)
typ := reflect.TypeOf(u)
fmt.Printf("Type: %v, Kind: %v\n", typ, typ.Kind())
// Iterate through fields
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
value := val.Field(i)
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
}Reflection is how encoding/json knows which fields to look for when you call Marshal(myStruct). It uses reflection to "walk" the struct and read the tags (e.g., json:"id").
As the name implies, the unsafe package allows you to do things that the Go compiler normally forbids, such as arbitrary pointer arithmetic or converting between incompatible types without the performance overhead of traditional conversion.
3. The Unsafe Mirror: Pointer & Memory Geometry
The unsafe package is the "Sledgehammer" of the Go runtime, used to manipulate the Physical Memory Mirror.
The Manipulation Physics
- The Pointer Bypass Mirror: Go normally prevents you from casting an
*intto a*float64.unsafe.Pointeracts as a "Bypass Mirror," allowing you to point any variable at any memory address. - The Memory Layout Mirror: Using
unsafe.Offsetof, we can see exactly how many bytes the compiler inserts as padding to maintain Word Alignment (Module 3). - The Zero-Copy Mirror: In high-performance I/O, we use
unsafeto "Mirror" a byte slice directly into a struct without copying the data, bypassing the CPU's memory-to-memory copy taxi.
4. Bypassing Safety: unsafe
func main() {
i := 10
// Get a pointer to the integer
p := unsafe.Pointer(&i)
// Treat that pointer as a pointer to a float (very dangerous!)
f := (*float64)(p)
fmt.Println(*f)
}When to Go Low-Level
Performance vs. Safety
| Task / Feature | Standard Code | Reflect / Unsafe |
|---|---|---|
| No comparison data available | ||
Best Practices for Meta-Programming
- Avoid Reflection: If you can solve a problem using interfaces or standard logic, do it. Reflection makes code harder to maintain and slower to execute.
- Verify Kinds: When using reflection, always check
val.Kind()to ensure you're dealing with the type you expect (e.g.,reflect.Struct). - Encapsulate Unsafe: If you must use
unsafe, wrap it in a safe, well-tested package so the rest of your application doesn't have to deal with raw pointers.
Modifying Values with Reflection
The reflect package can also set values, not just read them — but only if you obtain the value via a pointer and the underlying field is exported.
func setField(obj interface{}, fieldName string, value interface{}) {
v := reflect.ValueOf(obj).Elem() // Elem() dereferences the pointer
field := v.FieldByName(fieldName)
if field.IsValid() && field.CanSet() {
field.Set(reflect.ValueOf(value))
}
}
type Config struct {
Timeout int
}
func main() {
cfg := &Config{Timeout: 30}
setField(cfg, "Timeout", 60)
fmt.Println(cfg.Timeout) // 60
}This technique powers configuration loaders that map environment variables to struct fields automatically, common in frameworks like Viper and envconfig.
Reading Struct Tags
Struct tags are the metadata strings you write after a field's type (e.g., json:"id"). Reflection is the only way to read these at runtime.
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" db:"full_name"`
}
func printTags(v interface{}) {
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("%s: json=%s, db=%s\n",
field.Name,
field.Tag.Get("json"),
field.Tag.Get("db"))
}
}This is precisely how encoding/json, GORM, and other libraries discover field mappings at runtime. For practical ORM usage, see the Go database and SQL with GORM guide.
unsafe.Sizeof and Memory Layout
Beyond pointer conversion, unsafe provides three compile-time functions useful for low-level memory work:
type Point struct {
X int32
Y int32
}
p := Point{1, 2}
fmt.Println(unsafe.Sizeof(p)) // 8 bytes (2 × 4 bytes)
fmt.Println(unsafe.Alignof(p)) // 4 bytes alignment
fmt.Println(unsafe.Offsetof(p.Y)) // 4 bytes offset from startThese are essential when writing interoperability code that exchanges raw binary data with C libraries via CGo, or when parsing binary network protocols at the byte level.
Go Generics vs. Reflection
Since Go 1.18, generics have replaced many use cases that previously required reflection. If you need a function that works on multiple types, prefer a generic function — it is type-safe, compile-time verified, and significantly faster.
// Generic approach (Go 1.18+) — preferred
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}Reserve reflection for cases where types are genuinely unknown at compile time — such as deserializing arbitrary JSON payloads or building ORMs that work with any user-defined struct.
The official reflect package documentation and the unsafe package specification are the authoritative references when working with these advanced tools.
For the broader context of testing reflection-heavy code, our Go testing and benchmarks guide shows how to benchmark reflection versus generics in a fair comparison. And for understanding how interfaces underpin reflection, the Go methods, interfaces, and OOP guide provides the necessary foundation.
We have successfully navigated the advanced "Internals" of Go. Now, we're pivoting toward the physical world of systems. In the next tutorial, we will explore Testing in Go, learning how to use the built-in testing framework to ensure your reflection-heavy or concurrent code is rock solid.
Phase 28: Deep Internals Mastery Checklist
- Verify Kind Sovereignty: Before using reflection, always verify the
Kind()of the value. CallingField()on a non-struct reflect mirror will trigger a panic. - Audit Alignment Geometry: If using
unsafefor memory mapping, ensure your struct fields are aligned to 8-byte boundaries to avoid "Silicon Bus" errors on ARM/x64. - Implement Sovereign Wrappers: Encapsulate all reflection or unsafe logic within a single, well-tested package. Never let raw
unsafe.Pointermirrors leak into your business logic. - Test Performance Delta: Use Go's benchmark mirror (Module 16) to verify that reflection is actually necessary. Often, Generics provide a safer, faster mirror.
- Use Static Analysis Mirrors: Run
go vetandgosecto catch illegalunsafepointer arithmetic that might break during Go runtime upgrades.
Read next: Go Mastery Final Knowledge Test: The Silicon Exam →
Common Mistakes with Reflect and Unsafe
1. Using reflect when a type switch would do
reflect.TypeOf(v).Kind() is far slower than a type switch and harder to read. Use type switches (switch v := i.(type)) for a small, known set of types. Reserve reflect for genuinely dynamic cases like serialisation libraries or dependency injection.
2. Calling reflect.Value.Interface() on unexported fields
Accessing unexported struct fields via reflection panics at runtime. Check field.CanInterface() before calling .Interface(). Unexported fields are intentionally hidden from outside packages — even reflect cannot bypass this safely.
3. unsafe.Pointer arithmetic without alignment guarantees
Misaligned memory access via unsafe.Pointer can cause bus errors on some architectures. Always verify that the pointer arithmetic preserves the alignment required by the target type. See the unsafe package documentation.
4. Holding a reflect.Value longer than the underlying data
If the underlying value is stack-allocated or garbage collected, a reflect.Value pointing at it becomes invalid. This is especially dangerous when reflecting over slice elements that may be reallocated.
5. Using unsafe across package boundaries
unsafe.Pointer patterns that depend on internal struct layout break silently when the layout changes. Never export unsafe.Pointer values across package boundaries — they make your API fragile across Go versions.
Frequently Asked Questions
When is reflect appropriate in Go?
Use reflect in framework-level code that must operate on arbitrary user-defined types: JSON/XML encoders, ORMs, dependency injection containers, test assertion libraries. Application code should almost never need reflect. The laws of reflection blog post is the canonical guide.
What does unsafe.Sizeof return?
unsafe.Sizeof(x) returns the size in bytes of the value x as it would be stored in memory, without evaluating x. It is a compile-time constant — useful for interoperating with C via cgo or for manual memory layout calculations.
Is it safe to use unsafe in production code?
Occasionally, yes — the Go standard library itself uses unsafe in performance-critical paths. But it bypasses all of Go's memory safety guarantees. Any use of unsafe must be carefully reviewed, thoroughly tested, and isolated behind a safe API surface.
