Unsafe Package Advanced¶
Introduction¶
The unsafe package provides operations that step outside Go's type system. It exists because some tasks — syscalls, hardware interaction, interoperability with C, and extreme performance optimization — are impossible within Go's safety guarantees.
The name is deliberate: unsafe code bypasses the compiler's type checking, can break with any Go release, and can cause memory corruption, crashes, or security vulnerabilities. The Go compatibility guarantee explicitly excludes packages that import unsafe. That said, unsafe is used extensively in Go's own standard library (runtime, reflect, sync, syscall), and understanding it is essential for advanced Go development.
Syntax & Usage¶
unsafe.Pointer¶
unsafe.Pointer is a special pointer type that can be converted to and from any pointer type. It is the bridge between Go's type system and raw memory access.
import "unsafe"
// Any *T can convert to unsafe.Pointer
x := 42
p := unsafe.Pointer(&x) // *int → unsafe.Pointer
// unsafe.Pointer can convert to any *T
y := (*float64)(p) // unsafe.Pointer → *float64 (reinterprets bits!)
fmt.Println(*y) // Garbage — int and float64 have different layouts
Legal Conversion Patterns¶
The Go spec defines exactly six legal patterns for using unsafe.Pointer. Any other use is undefined behavior:
// Pattern 1: *T1 → unsafe.Pointer → *T2
// Convert between pointer types (type punning)
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
func Float64frombits(b uint64) float64 {
return *(*float64)(unsafe.Pointer(&b))
}
// Pattern 2: unsafe.Pointer → uintptr (for arithmetic, NOT for storage)
// Pattern 3: unsafe.Pointer → uintptr → arithmetic → unsafe.Pointer
// MUST be in a SINGLE expression — never store intermediate uintptr
p := unsafe.Pointer(&someStruct)
fieldPtr := unsafe.Pointer(uintptr(p) + unsafe.Offsetof(someStruct.Field))
// Pattern 4: syscall.Syscall with uintptr arguments
// The compiler knows to keep the pointer alive during the syscall
syscall.Syscall(SYS_READ, fd, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
// Pattern 5: reflect.Value.Pointer or reflect.Value.UnsafeAddr → unsafe.Pointer
// Must convert result to unsafe.Pointer IMMEDIATELY
p = unsafe.Pointer(reflect.ValueOf(&x).Pointer())
// Pattern 6: reflect.SliceHeader/StringHeader Data fields
// Deprecated in Go 1.20+ — use unsafe.Slice and unsafe.String instead
unsafe.Sizeof, unsafe.Alignof, unsafe.Offsetof¶
type Example struct {
A bool // 1 byte
B int64 // 8 bytes
C bool // 1 byte
D int32 // 4 bytes
}
fmt.Println(unsafe.Sizeof(Example{})) // 24 (with padding!)
fmt.Println(unsafe.Alignof(Example{})) // 8
fmt.Println(unsafe.Sizeof(Example{}.A)) // 1
fmt.Println(unsafe.Sizeof(Example{}.B)) // 8
fmt.Println(unsafe.Offsetof(Example{}.A)) // 0
fmt.Println(unsafe.Offsetof(Example{}.B)) // 8 (7 bytes of padding after A)
fmt.Println(unsafe.Offsetof(Example{}.C)) // 16
fmt.Println(unsafe.Offsetof(Example{}.D)) // 20
// Memory layout of Example (24 bytes):
// [A][pad][pad][pad][pad][pad][pad][pad] bytes 0-7
// [B][B ][B ][B ][B ][B ][B ][B ] bytes 8-15
// [C][pad][pad][pad][D ][D ][D ][D ] bytes 16-23
Struct Padding Optimization¶
// ❌ Wasteful layout — 24 bytes due to padding
type Bad struct {
A bool // 1 + 7 padding
B int64 // 8
C bool // 1 + 3 padding
D int32 // 4
} // Total: 24 bytes
// ✅ Optimized layout — 16 bytes, no wasted padding
type Good struct {
B int64 // 8
D int32 // 4
A bool // 1
C bool // 1 + 2 padding
} // Total: 16 bytes
// Use 'fieldalignment' tool to detect this:
// go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
// fieldalignment -fix ./...
Pointer Arithmetic (Accessing Struct Fields by Offset)¶
type Header struct {
Magic uint32
Version uint16
Flags uint16
Length uint32
}
func readHeader(data []byte) Header {
if len(data) < int(unsafe.Sizeof(Header{})) {
panic("data too short")
}
return *(*Header)(unsafe.Pointer(&data[0]))
}
// Accessing a specific field by offset
func getVersion(h *Header) uint16 {
p := unsafe.Pointer(h)
versionPtr := (*uint16)(unsafe.Pointer(
uintptr(p) + unsafe.Offsetof(h.Version),
))
return *versionPtr
}
Converting Between Slice Types (Zero-Copy)¶
// Convert []byte to []uint32 without copying
// ⚠️ Alignment and endianness must be correct!
func bytesToUint32s(b []byte) []uint32 {
if len(b)%4 != 0 {
panic("length must be multiple of 4")
}
return unsafe.Slice((*uint32)(unsafe.Pointer(&b[0])), len(b)/4)
}
// Convert string to []byte without copying (READ-ONLY — do not modify!)
func stringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
// Convert []byte to string without copying
func bytesToString(b []byte) string {
return unsafe.String(&b[0], len(b))
}
unsafe.String and unsafe.Slice (Go 1.20+)¶
Go 1.20 introduced safer alternatives to the old reflect.SliceHeader/StringHeader hacks:
// unsafe.String(ptr *byte, len IntegerType) string
// Creates a string from a byte pointer and length
ptr := &someBytes[0]
s := unsafe.String(ptr, len(someBytes))
// unsafe.StringData(s string) *byte
// Returns a pointer to the underlying bytes of a string
dataPtr := unsafe.StringData("hello")
// unsafe.Slice(ptr *T, len IntegerType) []T
// Creates a slice from a pointer and length
arr := [5]int{1, 2, 3, 4, 5}
slc := unsafe.Slice(&arr[0], 5) // []int backed by arr
// unsafe.SliceData(s []T) *T
// Returns a pointer to the underlying array of a slice
p := unsafe.SliceData(slc)
The uintptr Danger: GC Can Move Memory¶
// ❌ DANGEROUS: storing uintptr across statements
p := uintptr(unsafe.Pointer(&x))
// ... GC may run here, moving x to a new address ...
// ... p is now a dangling pointer (GC doesn't track uintptr) ...
y := (*int)(unsafe.Pointer(p)) // UNDEFINED BEHAVIOR
// ✅ SAFE: single expression, no intermediate storage
y := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + offset))
// The compiler ensures &x stays alive for the entire expression
The critical insight: unsafe.Pointer is tracked by the garbage collector (it keeps the pointed-to object alive), but uintptr is just a number — the GC doesn't know it refers to an object. If the GC moves the object between the conversion to uintptr and the conversion back to unsafe.Pointer, you get a dangling pointer.
Real-World Usage in the Standard Library¶
sync.Pool internals (simplified)¶
// The runtime uses unsafe to access pool-local storage per-P (processor)
type poolLocal struct {
poolLocalInternal
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
// Cache-line padding prevents false sharing between processors
strings.Builder (zero-copy String())¶
// strings.Builder.String() returns the accumulated string without copying
// by converting the byte slice directly to a string via unsafe
func (b *Builder) String() string {
return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}
reflect package (type metadata access)¶
// reflect uses unsafe to read type metadata from interface headers
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
Syscall Interop¶
// Using unsafe for system calls (Linux example)
func mmap(addr uintptr, length uintptr, prot int, flags int, fd int, offset int64) (uintptr, error) {
r, _, err := syscall.Syscall6(
syscall.SYS_MMAP,
addr,
length,
uintptr(prot),
uintptr(flags),
uintptr(fd),
uintptr(offset),
)
if err != 0 {
return 0, err
}
return r, nil
}
// Reading a C struct returned by a syscall
type Statx struct {
Mask uint32
Blksize uint32
Attributes uint64
Nlink uint32
UID uint32
GID uint32
Mode uint16
// ... more fields
}
func readStatx(buf []byte) *Statx {
return (*Statx)(unsafe.Pointer(&buf[0]))
}
CGo Interop¶
/*
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char name[64];
double score;
} Record;
*/
import "C"
import "unsafe"
func newRecord(id int, name string, score float64) *C.Record {
r := (*C.Record)(C.malloc(C.sizeof_Record))
r.id = C.int(id)
r.score = C.double(score)
// Copy Go string to C char array
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
C.strncpy(&r.name[0], cname, 63)
return r
}
func freeRecord(r *C.Record) {
C.free(unsafe.Pointer(r))
}
Quick Reference¶
| Function | Purpose | Return Type |
|---|---|---|
unsafe.Pointer(p) |
Convert any *T to generic pointer |
unsafe.Pointer |
(*T)(unsafe.Pointer(p)) |
Convert generic pointer to *T |
*T |
uintptr(unsafe.Pointer(p)) |
Convert pointer to integer (for arithmetic) | uintptr |
unsafe.Sizeof(v) |
Size of value's type in bytes | uintptr |
unsafe.Alignof(v) |
Alignment requirement of value's type | uintptr |
unsafe.Offsetof(s.f) |
Byte offset of struct field f |
uintptr |
unsafe.Slice(p, len) |
Create slice from pointer + length (Go 1.17+) | []T |
unsafe.String(p, len) |
Create string from *byte + length (Go 1.20+) |
string |
unsafe.StringData(s) |
Get *byte pointer to string data (Go 1.20+) |
*byte |
unsafe.SliceData(s) |
Get pointer to slice's backing array (Go 1.20+) | *T |
unsafe.Add(p, len) |
Pointer arithmetic (Go 1.17+) | unsafe.Pointer |
Best Practices¶
-
Justify every use of unsafe — add a comment explaining why it's necessary and what invariants must hold.
-
Isolate unsafe code — put it in a small, well-tested internal package. Export safe wrappers.
-
Use
unsafe.Sliceandunsafe.String(Go 1.17+/1.20+) instead ofreflect.SliceHeader/StringHeader— they're safer and the old approach is deprecated. -
Never store
uintptrvalues — convertunsafe.Pointer → uintptr → unsafe.Pointerin a single expression only. -
Run
go vet— it detects some categories of unsafe misuse, including the uintptr-storage pattern. -
Test on multiple architectures — unsafe code that works on amd64 may break on arm64 due to alignment differences.
Common Pitfalls¶
Storing uintptr Across Statements
The most common unsafe bug. The GC does not track uintptr values — the pointed-to object can be moved or collected.
// ❌ GC can move the object between these two lines
addr := uintptr(unsafe.Pointer(&myStruct))
// ... GC may run here ...
ptr := unsafe.Pointer(addr) // Dangling pointer!
// ✅ Single expression — compiler keeps object alive
ptr := unsafe.Pointer(uintptr(unsafe.Pointer(&myStruct)) + offset)
// ✅ Or use unsafe.Add (Go 1.17+)
ptr := unsafe.Add(unsafe.Pointer(&myStruct), offset)
Violating Alignment Requirements
Accessing a uint64 at an unaligned address causes a crash on some architectures (ARM, MIPS) and is always undefined behavior.
Modifying String Backing Data
Go strings are immutable. Using unsafe to modify a string's underlying bytes violates this invariant and can cause unpredictable behavior (strings may be interned, shared, or used as map keys).
Go Version Compatibility
Code using unsafe is NOT covered by the Go 1 compatibility promise. Internal runtime layouts change between versions. Pin your Go version and re-test unsafe code on every upgrade.
Incorrect Sizeof for Slices and Strings
Performance Considerations¶
-
Zero-copy string↔[]byte conversion: The most common performance use of unsafe. Saves one allocation (~50ns) per conversion. Worth it in hot paths processing millions of strings (JSON parsers, web frameworks).
-
Struct memory layout: Reordering fields to minimize padding can save 10–30% memory for structs allocated millions of times. Use
fieldalignmenttool. -
Direct memory access: Avoiding
encoding/binaryby casting byte slices to struct pointers eliminates per-field decoding overhead. 5–10x faster for binary protocol parsing, but sacrifices portability (endianness). -
Type punning:
Float64bits/Float64frombitsusing unsafe are 100x faster thanmath.Float64bitswas before the compiler learned to inline it (now they're equivalent — the compiler recognizes the pattern). -
Profile first: Most unsafe "optimizations" save nanoseconds. In a web service doing 10ms database calls, saving 50ns on string conversion is irrelevant. Profile to find actual bottlenecks.
// Benchmark: string conversion
func BenchmarkStringCopy(b *testing.B) {
bs := []byte("hello world this is a longer string for benchmarking")
for i := 0; i < b.N; i++ {
_ = string(bs) // ~50ns (allocates)
}
}
func BenchmarkStringUnsafe(b *testing.B) {
bs := []byte("hello world this is a longer string for benchmarking")
for i := 0; i < b.N; i++ {
_ = unsafe.String(&bs[0], len(bs)) // ~0.3ns (no allocation)
}
}
Interview Tips¶
Interview Tip
When asked about unsafe, start with when it's justified: syscalls, C interop, performance-critical zero-copy operations, and accessing runtime internals. Then immediately discuss the risks: no GC tracking of uintptr, alignment violations, Go version incompatibility. This shows you understand both the power and the responsibility.
Interview Tip
A common question is "How would you convert []byte to string without allocation?" Show unsafe.String(&b[0], len(b)) for Go 1.20+, but immediately add the caveat: "The byte slice must not be modified after conversion, because strings are immutable and the compiler/runtime rely on that invariant."
Interview Tip
If asked about struct padding, explain that Go aligns fields to their natural alignment (an int64 must start at an 8-byte boundary). Show how reordering fields from largest to smallest minimizes padding. Mention unsafe.Sizeof, unsafe.Alignof, unsafe.Offsetof as the tools to inspect layout, and fieldalignment as the automated fixer.
Interview Tip
The uintptr gap (GC can move objects between unsafe.Pointer → uintptr and uintptr → unsafe.Pointer) is a deep knowledge question. Explain that unsafe.Pointer is GC-tracked but uintptr is not — it's just an integer. The fix is to do the entire conversion in a single expression so the compiler keeps the original pointer alive.
Key Takeaways¶
unsafe.Pointeris the bridge between Go's type system and raw memory — use it to convert between pointer types and perform pointer arithmetic.- Six legal patterns are defined by the spec for
unsafe.Pointeruse — anything else is undefined behavior. - Never store
uintptracross statements — the GC doesn't track it, so the target object may be moved or collected. - Use
unsafe.Sliceandunsafe.String(Go 1.17+/1.20+) instead of the deprecatedSliceHeader/StringHeaderapproach. unsafe.Sizeof,unsafe.Alignof, andunsafe.Offsetofinspect memory layout — use them to optimize struct padding.- Isolate unsafe code, justify every use, and test on multiple architectures. Unsafe code is not covered by the Go 1 compatibility promise.
- Most programs never need
unsafe— exhaust interfaces, generics, and standard library options first.