Introduction
Beyond basic type punning, unsafe enables several powerful patterns: struct serialization without reflection, MMIO (memory-mapped I/O), and custom memory pools. These patterns trade safety for performance in the most demanding scenarios — typically 5-20x faster than their safe equivalents.
Key Concepts
- Type punning: Reinterpreting a value's bytes as a different type without conversion.
- unsafe.Add: Adds a byte offset to a pointer — the safe way to do pointer arithmetic in Go 1.17+.
- unsafe.Slice: Creates a slice from a pointer and length — avoids manual SliceHeader manipulation.
- Reflection-free serialization: Reading struct bytes directly, bypassing
encoding/binaryandreflect.
Real World Context
High-performance databases (Badger, Pebble) use unsafe to read/write records directly from memory-mapped files. Network protocol libraries use unsafe to cast byte buffers directly to struct pointers, avoiding per-field deserialization. These patterns can process millions of records per second.
Deep Dive
Direct Struct Serialization
Instead of encoding field-by-field, cast the entire struct to bytes:
gotype Header struct { Magic uint32 Version uint16 Length uint32 } func headerToBytes(h *Header) []byte { size := unsafe.Sizeof(Header{}) return unsafe.Slice((*byte)(unsafe.Pointer(h)), size) } func bytesToHeader(b []byte) *Header { return (*Header)(unsafe.Pointer(unsafe.SliceData(b))) }
This is 10-20x faster than encoding/binary.Read because it avoids reflection entirely.
Accessing Unexported Fields
unsafe can access private fields via offset (for testing and debugging):
gotype secretStruct struct { public int private string // unexported } s := secretStruct{public: 1, private: "hidden"} // Access private field via offset ptr := unsafe.Add(unsafe.Pointer(&s), unsafe.Offsetof(s.private)) val := *(*string)(ptr) fmt.Println(val) // "hidden"
Warning: This breaks encapsulation and should only be used for debugging/testing.
Memory-Mapped I/O
go// Map a file into memory data, _ := syscall.Mmap(fd, 0, size, syscall.PROT_READ, syscall.MAP_SHARED) // Cast to a slice of structs — zero-copy records := unsafe.Slice((*Record)(unsafe.Pointer(unsafe.SliceData(data))), numRecords) // Access records directly from the memory-mapped file fmt.Println(records[0].Name)
The unsafe.Pointer Safety Rules
Go specifies exactly six valid unsafe.Pointer conversion patterns. All other patterns are undefined behavior:
- Convert
*Ttounsafe.Pointerto*U(type punning) - Convert
unsafe.Pointertouintptrand back in a single expression unsafe.Add(ptr, offset)for pointer arithmeticreflect.Value.Pointer()or.UnsafeAddr()tounsafe.Pointerreflect.SliceHeader/reflect.StringHeaderconversions (deprecated — useunsafe.Slice/unsafe.String)- Passing
unsafe.Pointertosyscall.Syscall
Common Pitfalls
- Endianness assumptions — Direct struct casting assumes the CPU's byte order. This breaks on big-endian architectures if you don't account for it.
- Struct padding differences across architectures — A struct may have different padding on 32-bit vs 64-bit systems. Use explicit fixed-size types for serialization.
Best Practices
- Add build tags for architecture constraints — If your unsafe code assumes 64-bit or little-endian, use
//go:build amd64 || arm64. - Write comprehensive tests — Unsafe code bypasses the type checker. Tests are your only safety net. Test on multiple architectures if possible.
Summary
- Direct struct serialization via unsafe is 10-20x faster than reflection-based encoding.
unsafe.Addandunsafe.Sliceare the modern, safer patterns for pointer arithmetic.- Memory-mapped I/O combined with unsafe enables zero-copy data access.
- Follow Go's six valid
unsafe.Pointerconversion patterns — all others are undefined behavior. - Add architecture build tags and comprehensive tests for unsafe code.
Code Examples
// Direct struct serialization — 10-20x faster than encoding/binary
type Packet struct {
Type uint8
Length uint32
Seq uint64
}
func encodePacket(p *Packet) []byte {
size := unsafe.Sizeof(Packet{})
// Cast struct pointer to byte slice — zero copy
return unsafe.Slice((*byte)(unsafe.Pointer(p)), size)
}
func decodePacket(b []byte) *Packet {
// Cast byte slice to struct pointer — zero copy
return (*Packet)(unsafe.Pointer(unsafe.SliceData(b)))
}