Unsafe Pointer Tricks for Performance
Master safe and unsafe pointer patterns to achieve zero-copy conversions and direct memory access while maintaining correctness.
Introduction to unsafe.Pointer
unsafe.Pointer is Go's mechanism for performing unrestricted pointer arithmetic and type conversions. It bypasses Go's type system to enable direct memory access, which is sometimes necessary for maximum performance.
import "unsafe"
var x int = 42
ptr := unsafe.Pointer(&x) // Any pointer becomes unsafe.Pointer
xAgain := (*int)(ptr) // Convert back to typed pointerWhile powerful, unsafe code sacrifices safety guarantees. The Go runtime cannot verify that unsafe operations are correct, and mistakes can cause undefined behavior, data corruption, or security vulnerabilities.
Warning: Unsafe pointer usage violates Go's memory safety model. Only use it when the performance benefit is substantial and the code can be thoroughly tested.
When to Consider Unsafe
Unsafe pointer tricks are worth considering in these limited scenarios:
1. Zero-Copy Conversions Between Strings and Byte Slices
Converting between string and []byte normally requires copying the underlying data. For large strings, this copy can be expensive.
// Safe: 1ms for 1GB string
func stringToSliceSafe(s string) []byte {
b := make([]byte, len(s))
copy(b, s) // Copies entire string
return b
}
// Unsafe: nanoseconds
func stringToSliceUnsafe(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}2. Struct Casting for Serialization
Casting a struct directly to bytes bypasses encoding overhead.
type Header struct {
Magic uint32
Version uint8
Length uint32
}
// Safe: Uses reflection/encoding
func encodeHeaderSafe(h Header) []byte {
buf := make([]byte, binary.Size(h))
binary.Write(buf, binary.LittleEndian, h)
return buf
}
// Unsafe: Direct memory copy (nanoseconds)
func encodeHeaderUnsafe(h Header) []byte {
return unsafe.Slice((*byte)(unsafe.Pointer(&h)), unsafe.Sizeof(h))
}3. Accessing Unexported Struct Fields
For testing or debugging, unsafe can access private fields (not recommended for production).
4. Manual Memory Layout Control
When you need precise control over memory layout and alignment, unsafe provides the tools.
5. Direct Memory-Mapped I/O Access
For kernel modules or embedded systems, unsafe is essential for memory-mapped registers.
The Six Valid Unsafe.Pointer Conversion Patterns
The Go specification defines six valid patterns for converting to and from unsafe.Pointer. Violating these patterns is undefined behavior.
Pattern 1: Pointer Type Conversion
Converting between different pointer types:
var x int = 42
var p *int = &x
// Valid: pointer to pointer
var up unsafe.Pointer = unsafe.Pointer(p)
var q *int = (*int)(up)
// Pattern: T1 to unsafe.Pointer to T2
var f *float64 = (*float64)(unsafe.Pointer(p)) // Reinterpret bitsPattern 2: unsafe.Pointer to uintptr (and Back)
Converting to integer for arithmetic:
var p *int = &x
addr := uintptr(unsafe.Pointer(p))
// Modify address
addr += 8 // Move 8 bytes forward
// Convert back (valid if pointing to valid allocation)
newp := (*int)(unsafe.Pointer(addr))Critical: Intermediate uintptr values cannot be stored or passed around. They must be converted back to a pointer immediately.
// ✗ Invalid: storing uintptr breaks GC
var savedAddr uintptr = uintptr(unsafe.Pointer(&x))
// ... later ...
ptr := (*int)(unsafe.Pointer(savedAddr)) // Undefined behavior!
// ✓ Valid: immediate conversion
ptr := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x))))Pattern 3: Arithmetic Offset with unsafe.Add
Using unsafe.Add for pointer arithmetic (Go 1.17+):
// Before Go 1.17, required uintptr arithmetic
ptr := unsafe.Pointer(&arr[0])
nextElem := (*int)(unsafe.Add(ptr, unsafe.Sizeof(int(0))))
// Equivalent to:
nextElem := (*int)(unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(int(0))))unsafe.Add is safer because it keeps the uintptr conversion isolated.
Pattern 4: Conversion to unsafe.Slice
Creating a slice from a pointer (Go 1.17+):
// Create slice from raw pointer
arr := (*[100]int)(unsafe.Pointer(&buffer[0]))
slice := unsafe.Slice(arr[0:], 100)
// Or directly
slice := unsafe.Slice((*int)(unsafe.Pointer(&buffer)), 100)This is safe only if the underlying memory is valid and allocated for the given length.
Pattern 5: Conversion to unsafe.String
Creating a string from a pointer (Go 1.20+):
// Read null-terminated C string
cStr := (*C.char)(unsafe.Pointer(ptr))
goStr := C.GoString(cStr) // Go's C interop helper
// Or unsafe (requires length)
bytes := unsafe.Slice((*byte)(unsafe.Pointer(cStr)), len)
str := unsafe.String((*byte)(unsafe.Pointer(cStr)), len)Pattern 6: unsafe.StringData and unsafe.SliceData
Getting pointers to string/slice data (Go 1.20+):
s := "hello"
ptr := unsafe.StringData(s) // *byte pointing to 'h'
slice := []int{1, 2, 3}
ptr := unsafe.SliceData(slice) // *int pointing to first elementPattern 1 in Depth: Zero-Copy String/Byte Conversions
This is the most common safe use of unsafe pointers.
The Safer APIs (Go 1.20+)
Go 1.20 introduced unsafe.String, unsafe.StringData, and unsafe.SliceData specifically for this purpose:
// ✓ Recommended (Go 1.20+)
func unsafeStringToSlice(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func unsafeSliceToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}How It Works
A string in Go is internally:
type stringHeader struct {
Data *byte // Pointer to characters
Len int // Length
}A slice is:
type sliceHeader struct {
Data *byte // Pointer to elements
Len int // Length
Cap int // Capacity
}For strings backed by immutable data, reading via []byte is safe if you don't modify the bytes:
s := "hello"
b := unsafe.Slice(unsafe.StringData(s), len(s))
// Reading is safe
ch := b[0] // 'h'
// Writing is UNSAFE - corrupts the string!
b[0] = 'H' // ✗ undefined behaviorBenchmark: String/Slice Conversion
package main
import (
"testing"
"unsafe"
)
func BenchmarkStringToSliceSafe(b *testing.B) {
s := "hello world hello world hello world"
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Creates copy
buf := make([]byte, len(s))
copy(buf, s)
_ = buf
}
}
func BenchmarkStringToSliceUnsafe(b *testing.B) {
s := "hello world hello world hello world"
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Zero-copy
buf := unsafe.Slice(unsafe.StringData(s), len(s))
_ = buf
}
}
func BenchmarkSliceToStringSafe(b *testing.B) {
slice := []byte("hello world hello world")
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Creates copy
s := string(slice)
_ = s
}
}
func BenchmarkSliceToStringUnsafe(b *testing.B) {
slice := []byte("hello world hello world")
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Zero-copy (but dangerous!)
s := unsafe.String(unsafe.SliceData(slice), len(slice))
_ = s
}
}Expected Results
BenchmarkStringToSliceSafe-8 100000000 12.5 ns/op 32 B/op 1 allocs/op
BenchmarkStringToSliceUnsafe-8 1000000000 0.35 ns/op 0 B/op 0 allocs/op
BenchmarkSliceToStringSafe-8 100000000 11.3 ns/op 32 B/op 1 allocs/op
BenchmarkSliceToStringUnsafe-8 1000000000 0.52 ns/op 0 B/op 0 allocs/opThe unsafe version is 30-40x faster because it avoids memory allocation and copying.
Pattern 2 in Depth: Struct Casting for Serialization
Converting a struct to bytes without encoding overhead:
// Network packet header
type PacketHeader struct {
SourceIP uint32
DestIP uint32
Protocol uint8
_padding [3]byte // Explicit padding for alignment
Length uint16
}
func encodePacketUnsafe(p PacketHeader) []byte {
return unsafe.Slice((*byte)(unsafe.Pointer(&p)), unsafe.Sizeof(p))
}
func decodePacketUnsafe(b []byte) PacketHeader {
return *(*PacketHeader)(unsafe.Pointer(unsafe.SliceData(b)))
}Safety Considerations
This only works safely if:
- Struct layout is predictable: No interior pointers, no
interface{}fields - Padding is controlled: Use explicit padding fields or
//go:packed - Endianness is consistent: Both sides use the same CPU byte order
- No dynamic data: All fields are fixed-size (no slices, strings, maps)
Benchmark: Struct Serialization
type Point struct {
X, Y, Z float64
}
func BenchmarkEncodeSafe(b *testing.B) {
p := Point{1.0, 2.0, 3.0}
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf := make([]byte, 24)
binary.LittleEndian.PutUint64(buf[0:8], math.Float64bits(p.X))
binary.LittleEndian.PutUint64(buf[8:16], math.Float64bits(p.Y))
binary.LittleEndian.PutUint64(buf[16:24], math.Float64bits(p.Z))
_ = buf
}
}
func BenchmarkEncodeUnsafe(b *testing.B) {
p := Point{1.0, 2.0, 3.0}
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf := unsafe.Slice((*byte)(unsafe.Pointer(&p)), unsafe.Sizeof(p))
_ = buf
}
}Results show unsafe is 5-10x faster for struct serialization.
Pattern 3: Pointer Arithmetic with unsafe.Add
Moving pointers within allocated memory:
func readUint32LE(data []byte, offset int) uint32 {
// Safe: validates bounds
return binary.LittleEndian.Uint32(data[offset : offset+4])
// Unsafe: no bounds check
ptr := unsafe.Pointer(unsafe.SliceData(data))
ptr = unsafe.Add(ptr, offset)
return *(*uint32)(ptr)
}Valid Use Case: Array Iteration
func sumArrayUnsafe(arr []int64) int64 {
sum := int64(0)
ptr := unsafe.Pointer(unsafe.SliceData(arr))
end := unsafe.Add(ptr, len(arr)*int(unsafe.Sizeof(int64(0))))
for ptr != end {
sum += *(*int64)(ptr)
ptr = unsafe.Add(ptr, unsafe.Sizeof(int64(0)))
}
return sum
}This is rarely faster than safe iteration because the compiler already optimizes safe loops well.
The Dangers of Unsafe Pointers
Understanding what can go wrong is essential:
1. Garbage Collection Invalidation
The garbage collector can move objects in memory. Storing uintptr breaks this:
// ✗ WRONG
var savedAddr uintptr
func init() {
x := 42
savedAddr = uintptr(unsafe.Pointer(&x)) // Stores address
}
func later() {
p := (*int)(unsafe.Pointer(savedAddr)) // Might point to garbage!
// x may have moved, or the memory might be reused
}2. Pointer Escaping Go's Type System
Type safety is lost:
s := "hello"
b := unsafe.Slice(unsafe.StringData(s), len(s))
b[0] = 'H' // ✗ Corrupts the string!
// String is now broken, other code using s will see corrupted data3. Memory Aliasing Violations
Writing through one pointer invalidates assumptions about another:
var x int = 5
var y int = 10
// ✗ Creates unsafe aliasing
px := unsafe.Pointer(&x)
py := (*int)(px) // Treats x as if it's y
*py = 20 // Corrupts x!4. Alignment Violations
Misaligned access can cause crashes on some architectures:
// ✗ Bad: may cause crash on ARM
b := []byte{1, 2, 3, 4, 5, 6, 7, 8}
ptr := unsafe.Pointer(unsafe.SliceData(b))
ptr = unsafe.Add(ptr, 1) // Misaligned!
val := *(*uint32)(ptr) // May crashReal-World Unsafe in the Go Standard Library
The stdlib uses unsafe carefully in performance-critical code:
strings.Builder
// strings/builder.go simplified
type Builder struct {
addr *Builder
buf []byte
}
func (b *Builder) String() string {
return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}Avoids copying the buffer when converting to string.
sync.Pool
// sync/pool.go
type poolLocal struct {
poolLocalInternal
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}Uses unsafe to control memory layout and prevent false sharing (padding).
reflect Package
// reflect/value.go
func (v Value) Bytes() []byte {
return *(*[]byte)(unsafe.Pointer(&v.ptr))
}Reinterprets internal representation as byte slice.
Runtime Safety Tools
Go provides tools to catch unsafe mistakes:
The -race Detector
go run -race main.goDetects data races involving unsafe pointers:
WARNING: DATA RACE
Write at 0x00c0001a0000 by goroutine 5:
main.modifyUnsafe()
main.go:20 +0x4c
Previous write at 0x00c0001a0000 by goroutine 4:
main.modifyUnsafe()
main.go:20 +0x4cPointer Checking with -checkptr
go build -msan -checkptr ./...Checks for invalid pointer operations:
RUNTIME ERROR: unsafe pointer conversion
cgo argument has Go pointer to Go pointerMemory Sanitizer (MSAN)
go test -msan ./...Detects memory errors (only works on Linux x86_64):
SUMMARY: MemorySanitizer: use-of-uninitialized-value /path/to/main.go:42Safer Alternatives to Unsafe
Before using unsafe, consider these safer options:
1. Use encoding/binary
For serialization:
// ✓ Safe, clear
var h PacketHeader
binary.Read(buf, binary.LittleEndian, &h)
// vs Unsafe
h := *(*PacketHeader)(unsafe.Pointer(unsafe.SliceData(buf)))2. Use reflect Package
Inspect types safely:
// ✓ Safe
t := reflect.TypeOf(x)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Analyze field
}
// vs Unsafe
ptr := unsafe.Pointer(&x)
// Manual offset calculation3. Use Generics (Go 1.18+)
Type-safe alternatives to unsafe casting:
// ✓ Safe with generics
func min[T interface{ int | float64 }](a, b T) T {
if a < b { return a }
return b
}
// vs Unsafe interface{} casting
func minUnsafe(a, b interface{}) interface{} {
// Type assertion needed
}4. Copy When Needed
Sometimes explicit copying is better:
// ✓ Clear intent, safe
buf := make([]byte, len(s))
copy(buf, s)
// vs Unsafe zero-copy (risky if buf is modified)
buf := unsafe.Slice(unsafe.StringData(s), len(s))Best Practices for Unsafe Code
When unsafe is genuinely needed:
- Document invariants clearly: Explain why unsafe is necessary and what assumptions it makes
// readUint32LE reads a little-endian uint32 from b without bounds checks.
// Requires b to have at least 4 bytes of valid data at offset.
// UNSAFE: Bounds checking is the caller's responsibility.
func readUint32LE(b []byte, offset int) uint32 {
ptr := unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), offset)
return *(*uint32)(ptr)
}- Isolate unsafe into small packages: Contain risk
unsafe/
├── strings/ // String <-> []byte conversions only
├── binary/ // Struct serialization only
└── ...- Test thoroughly with -race and -checkptr:
go test -race -checkptr ./unsafe/...- Add integration tests: Ensure unsafe code works correctly
func TestStringSliceConversion(t *testing.T) {
original := "hello world"
slice := unsafeStringToSlice(original)
if string(slice) != original {
t.Fatalf("round-trip failed")
}
// Verify we didn't copy
if cap(slice) != 0 {
t.Fatal("unsafe conversion created a copy")
}
}- Profile impact: Verify the unsafe code actually improves performance
func BenchmarkLogicWithoutUnsafe(b *testing.B) { ... }
func BenchmarkLogicWithUnsafe(b *testing.B) { ... }If the improvement is under 5%, stick with safe code.
Summary
Unsafe pointers are powerful tools for performance optimization, but they require careful use. The Go specification defines six valid conversion patterns, and violating them causes undefined behavior.
The safest use case is zero-copy string/byte conversions with Go 1.20+ APIs like unsafe.String and unsafe.StringData. These provide the speed benefit (30-40x faster) with minimal risk when used correctly.
Key principles:
- Only use unsafe for performance-critical code where measurements show significant improvement
- Isolate unsafe code into well-tested, documented packages
- Use -race and -checkptr to catch errors
- Prefer safe alternatives - encoding/binary, reflect, generics
- Document assumptions - explain why unsafe is necessary
- Test thoroughly - unsafe bugs are subtle and dangerous
The Go runtime's memory safety model exists for good reason. Unsafe breaks those guarantees, so use it sparingly and carefully.