Go Reflect and Unsafe: Under the Hood of Go

Go Reflect and Unsafe: Under the Hood
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.
Use with Extreme Caution
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.
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").
Bypassing Safety: unsafe
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.
When to Go Low-Level
Database ORMsReflection is essential for mapping arbitrary database rows to user-defined Go structs.
Protobuf / JSONAlmost all high-performance serializers use reflection to inspect data structures without knowing their types at compile time.
Big Data processingThe 'unsafe' package is often used to convert byte slices to structs without copying memory, saving massive amounts of CPU time.
Performance vs. Safety
| Task / Feature | Standard Code | Reflect / Unsafe |
|---|---|---|
| Speed | Blazing fast (Static) | Variable (Reflection is ~10-100x slower) |
| Type Safety | Full compiler verification | Runtime checks or no checks at all |
| Readability | High | Low (Meta-programming is often convoluted) |
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.
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.
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:
These 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.
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.
Next Steps
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.
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.
