Introduction
Go is a memory-safe language by design — but sometimes you need to bypass safety for performance. The unsafe package provides direct memory access, pointer arithmetic, and type punning. It is the foundation of many high-performance Go libraries, but misuse leads to crashes, data corruption, and undefined behavior.
Key Concepts
- unsafe.Pointer: A special pointer type that can be converted to/from any pointer type. It bypasses Go's type system.
- unsafe.Sizeof: Returns the size of a type in bytes, including padding.
- unsafe.Alignof: Returns the alignment requirement of a type.
- unsafe.Offsetof: Returns the byte offset of a struct field from the struct's start.
- uintptr: An integer type large enough to hold any pointer. Used for pointer arithmetic.
Real World Context
The Go standard library uses unsafe extensively for performance: reflect, sync, runtime, strings, and encoding/binary all use unsafe internally. High-performance libraries like fasthttp, sonic (fast JSON), and goccy/go-json rely on unsafe for zero-copy string/byte conversions and direct struct manipulation.
Deep Dive
unsafe.Pointer Conversions
The fundamental unsafe operation is pointer type conversion:
go// Convert *int to *float64 — reinterprets the bytes var i int64 = 42 f := *(*float64)(unsafe.Pointer(&i))
Go's rules for unsafe.Pointer conversions:
- Any pointer →
unsafe.Pointer✓ unsafe.Pointer→ any pointer ✓uintptr→unsafe.Pointer(only in specific patterns)- Pointer arithmetic via
uintptr(only in specific patterns)
Struct Field Access by Offset
gotype Config struct { Name string Port int Debug bool } c := Config{Name: "app", Port: 8080, Debug: true} // Access Port field directly via offset portPtr := (*int)(unsafe.Add(unsafe.Pointer(&c), unsafe.Offsetof(c.Port))) fmt.Println(*portPtr) // 8080
unsafe.Add (Go 1.17+) is the safe way to do pointer arithmetic — it's a single expression that the compiler can verify.
Zero-Copy String/Byte Conversion
The most common performance use of unsafe — converting between string and []byte without copying:
go// Zero-copy string to []byte (READ-ONLY — never modify the result!) func stringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } // Zero-copy []byte to string func bytesToString(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b)) }
These avoid the ~50-100 ns copy for large strings/byte slices.
Common Pitfalls
- Modifying bytes from a zero-copy string conversion — Strings in Go are immutable. Modifying the bytes behind a string causes undefined behavior and may crash.
- Storing uintptr values — A uintptr is just an integer; the GC doesn't know it points to anything. The object may be collected while you hold the uintptr.
- Breaking across GC cycles — Never convert
unsafe.Pointer→uintptrand back in separate statements. The GC may move the object between the two operations.
Best Practices
- Prefer unsafe.Add and unsafe.Slice — These Go 1.17+ functions provide safer patterns than raw pointer arithmetic.
- Isolate unsafe code — Keep unsafe operations in small, well-tested functions with clear contracts about safety requirements.
Summary
unsafe.Pointerenables type punning and pointer arithmetic, bypassing Go's type system.unsafe.Sizeof,Alignof,Offsetofinspect type memory layout.- Zero-copy string/byte conversion avoids allocation but requires immutability discipline.
- Use
unsafe.Addandunsafe.Slice(Go 1.17+) for safer pointer arithmetic. - Isolate unsafe code in small functions with clear safety contracts.
Code Examples
package main
import (
"fmt"
"unsafe"
)
// Zero-copy []byte to string — avoids allocation
func bytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
// Zero-copy string to []byte — result is READ-ONLY
func stringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func main() {
data := []byte("hello world")
s := bytesToString(data) // No copy, no allocation
fmt.Println(s) // "hello world"
fmt.Println(unsafe.Sizeof(int64(0))) // 8
fmt.Println(unsafe.Alignof(int64(0))) // 8
}