What Is uintptr in Go and When to Use It

uintptr is an integer type holding a pointer's bit pattern, used for C interop but not for direct memory access.

The number that breaks the rules

You are building a high-performance cache that indexes objects by their memory address. Or you are writing a serialization library that needs to walk through a struct layout without reflection. You grab a pointer, cast it to an integer, add an offset, and cast back. It works on your laptop. You deploy to production, the garbage collector runs, and the cache returns garbage data. Or the program crashes with a segmentation fault because the address is misaligned.

This is the trap of uintptr. It is the type that bridges Go's safe memory model and the raw reality of hardware addresses. It is powerful, but it breaks the safety guarantees that make Go reliable. uintptr is not a pointer. It is an integer that happens to hold the bit pattern of a pointer. The runtime does not know this. The garbage collector ignores it. If you hold a uintptr and nothing else holds the object, the object dies. The number becomes a ghost.

What uintptr actually is

uintptr is an integer type. Its size matches the size of a pointer on your machine. On a 64-bit system, it is 64 bits. On a 32-bit system, it is 32 bits. You can store the address of any object in a uintptr. You can do arithmetic on it. You can cast it back to a pointer.

The critical distinction is that uintptr is invisible to the garbage collector. The runtime treats it as a plain integer. When the GC scans memory, it looks for pointers. It follows pointers to mark objects as live. It sees a uintptr and sees only a number. It does not follow it. If you hold a uintptr and the original pointer goes out of scope, the GC marks the object as dead. The memory is reclaimed. The uintptr still holds the address, but the address now points to freed memory or a different object.

Think of a pointer as a lease on an apartment. As long as you hold the lease, the landlord cannot evict the tenant. uintptr is like writing the apartment number on a napkin. You can do math on the number. You can pass the napkin around. But the landlord does not see the napkin. If the lease expires, the tenant moves out. The number on the napkin still points to the apartment, but the apartment is now occupied by someone else. Walking in based on the napkin leads to trouble.

Go enforces a strict boundary between pointers and integers. You cannot cast a pointer directly to uintptr. You must go through unsafe.Pointer. The compiler rejects addr := uintptr(ptr) with cannot convert ptr to type uintptr. You must write addr := uintptr(unsafe.Pointer(ptr)). This extra step is deliberate. It forces you to acknowledge that you are crossing a safety boundary. The unsafe package is a warning label. Use it sparingly.

Minimal conversion and arithmetic

Here's the basic pattern for converting a pointer to uintptr, doing math, and converting back. The original pointer must stay alive to keep the object reachable.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// Create a slice of integers. The backing array lives on the heap.
	nums := []int{10, 20, 30}
	// Get a pointer to the first element.
	ptr := &nums[0]
	// Convert to uintptr to enable arithmetic.
	// The runtime no longer tracks nums through this value.
	addr := uintptr(unsafe.Pointer(ptr))
	// Calculate the address of the second element.
	// Sizeof returns the size of int in bytes.
	step := unsafe.Sizeof(nums[0])
	nextAddr := addr + step
	// Cast back to pointer to read the value.
	// Safe here because nums keeps the backing array alive.
	nextPtr := (*int)(unsafe.Pointer(nextAddr))
	fmt.Println(*nextPtr) // Prints 20
}

When you compile this, the compiler sees unsafe.Pointer. It allows the conversion to uintptr. It emits code to load the address into an integer register. At runtime, addr holds the raw address. The GC runs. It scans the stack. It sees ptr. It follows ptr and marks nums as live. It sees addr. It sees an integer. It ignores it. If you removed ptr from scope, the GC would not see nums and would free it. The nextAddr calculation is just integer addition. The cast back to *int tells the compiler to treat the address as a pointer to an int. The CPU dereferences it. If nums was freed, this reads garbage.

The convention in Go is to keep uintptr lifetimes as short as possible. Convert to uintptr only when you need to do math or pass to C. Convert back to unsafe.Pointer immediately after. Never store a uintptr in a long-lived data structure unless you have a separate mechanism to keep the object alive.

Realistic usage: walking struct layouts

Here's how uintptr appears in libraries that need to access struct fields by offset. This pattern avoids reflection and is faster, but it breaks if the struct layout changes.

package main

import (
	"fmt"
	"unsafe"
)

type Record struct {
	ID   int32
	Name string
	Age  int32
}

// GetAge reads the Age field using raw offset math.
// This avoids reflection overhead but ties the code to the struct layout.
func GetAge(r *Record) int32 {
	// Get the base address of the struct instance.
	base := uintptr(unsafe.Pointer(r))
	// Compute the byte offset of Age relative to the struct start.
	// Offsetof respects compiler padding and alignment rules.
	offset := unsafe.Offsetof(r.Age)
	// Add offset to base address to get the field address.
	// Pointer arithmetic requires uintptr; add offset to base.
	addr := base + offset
	// Cast back to *int32 to read the field value.
	return *(*int32)(unsafe.Pointer(addr))
}

func main() {
	r := &Record{ID: 1, Name: "Alice", Age: 30}
	fmt.Println(GetAge(r))
}

The unsafe.Offsetof function is the safe way to get offsets. It computes the offset at compile time. It respects padding. The compiler may insert padding between fields to satisfy alignment requirements. Hardcoding offsets is dangerous. Offsetof ensures the math matches the actual memory layout.

The convention for receiver names in Go is one or two letters matching the type. Use (r *Record) not (this *Record). This keeps the code concise and follows community style. The gofmt tool enforces formatting, but naming is a human choice. The community expects short receiver names.

Pitfalls and runtime crashes

The biggest danger is the garbage collector. If you hold a uintptr across a GC cycle and the object has no other references, the object dies. The address becomes a dangling reference. Dereferencing it reads garbage or crashes. This is undefined behavior. The program might work for a while and then fail randomly.

Alignment is another trap. Some architectures require addresses to be multiples of 4 or 8 bytes. If you cast a uintptr to *int64 and the address is misaligned, the CPU crashes with a segmentation fault or alignment error. The compiler does not check alignment. This is a runtime error.

If you try to do math on a pointer directly, the compiler rejects it with invalid operation: operator + not defined on pointer. You must convert to uintptr first. This restriction exists because pointer arithmetic makes GC hard. If pointers can be derived from integers, the GC has to scan all integers. That is slow and prone to false positives. Go chooses to make GC fast by restricting pointers. uintptr is the controlled leak.

Another pitfall is passing uintptr to cgo. cgo has its own rules. You can pass uintptr to C functions that expect integer handles. C expects integers for file descriptors or opaque handles. uintptr is the bridge type. The size of uintptr matches unsigned long on most platforms. If you pass a uintptr to C and C treats it as a pointer, C must not store it across calls that might trigger Go GC. cgo calls can trigger GC. The object must stay alive.

The worst uintptr bug is the one that never logs. The program reads garbage data and produces wrong results. It does not crash. Debugging this requires understanding GC timing and memory layout. Isolate unsafe code. Document why you use it. The community expects unsafe to be rare in application code. It is acceptable in low-level libraries.

Decision: when to use uintptr

Use unsafe.Pointer when you need to cast between different pointer types or pass a pointer to the runtime.

Use uintptr when you need to perform arithmetic on an address or pass a raw address to a C function.

Use unsafe.Offsetof when you need the byte offset of a struct field relative to the struct start.

Use standard indexing and methods when you can; unsafe code is harder to maintain and breaks across compiler versions.

Use a slice or array when you need to iterate over memory; pointer arithmetic is rarely necessary in safe Go code.

Uintptr is a number, not a reference. The GC doesn't track numbers. Keep the pointer alive. Do math on uintptr. Dereference pointers. Never mix the two. Unsafe is a bridge, not a destination. Cross it and get back to safety.

Where to go next