The Bridge Between Types
You are writing a Go program that interfaces with a C library. The C code hands you a raw memory address wrapped in a *byte. Your Go code needs to treat that memory as a *MyStruct. You try to cast it directly. The compiler stops you. Go refuses to let you pretend a byte pointer is a struct pointer. The type system protects you from misinterpreting memory, but sometimes you know better than the compiler. You need to bridge the gap between incompatible pointer types. unsafe.Pointer is the bridge. It allows you to convert any pointer type to any other pointer type by passing through a raw, untyped intermediate.
Stripping the Type Label
unsafe.Pointer is a type that represents a raw memory address without any type information. Think of a pointer as a sticky note attached to a memory address. The note says "This is an int" or "This is a struct". The compiler reads the note to decide what operations are allowed. unsafe.Pointer is the act of peeling off the sticky note. You are left with just the address. You can then stick a new note on it that says "This is a byte" or "This is a float". The compiler stops checking if the new note matches the reality of the memory. You are telling the compiler, "I promise this memory layout matches the new type." The compiler accepts the promise. If the promise is false, the program crashes or corrupts data.
The Minimal Conversion
Here's the basic conversion loop: take a typed pointer, strip the type to unsafe.Pointer, then cast to a new type.
package main
import (
"fmt"
"unsafe"
)
func main() {
// Start with a typed value and its address.
var value int = 42
pInt := &value
// Strip the type to get a raw pointer.
// unsafe.Pointer holds the address but forgets it points to an int.
rawPtr := unsafe.Pointer(pInt)
// Reinterpret the raw pointer as a *byte.
// This is safe because every pointer can be viewed as a byte address.
pByte := (*byte)(rawPtr)
// Print the addresses to show they are identical.
// The address hasn't changed, only the type label.
fmt.Printf("pInt: %p, pByte: %p\n", pInt, pByte)
}
The address stays the same. The type label changes. Trust your layout.
What Happens at Compile and Runtime
At compile time, Go enforces strict type rules. You cannot assign a *int to a *byte variable. The compiler rejects this with cannot use pInt (variable of type *int) as *byte value in assignment. This prevents accidental misinterpretation of data. When you introduce unsafe.Pointer, you opt out of this check. The conversion unsafe.Pointer(pInt) is allowed from any pointer type. The conversion (*byte)(rawPtr) is allowed from unsafe.Pointer to any pointer type. The compiler trusts you.
At runtime, the pointer value is just a number representing a memory address. The conversion changes how the Go runtime interprets that address. If you read through the new pointer, the runtime fetches bytes from that address and treats them as the new type. If the size or alignment doesn't match, you get garbage data or a segmentation fault.
Bridging C Structs
Here's a realistic scenario: bridging a C struct into Go using CGo. You get a pointer to a C struct, but you want to read it using a Go struct definition.
package main
/*
typedef struct { int id; double score; } CRecord;
CRecord* get_record() { static CRecord r = {1, 99.5}; return &r; }
*/
import "C"
import (
"fmt"
"unsafe"
)
// GoRecord mirrors the C struct.
// Field order and types must match the C definition exactly.
type GoRecord struct {
ID int32
Score float64
}
func main() {
// Get the pointer from C.
cPtr := C.get_record()
// Bridge the C pointer to unsafe.Pointer.
raw := unsafe.Pointer(cPtr)
// Cast to the Go struct pointer.
// This tells Go to interpret the C memory as a GoRecord.
goPtr := (*GoRecord)(raw)
// Access fields directly.
// Mismatched layout causes undefined behavior.
fmt.Printf("ID: %d, Score: %f\n", goPtr.ID, goPtr.Score)
}
CGo bridges are common. Keep the struct definitions in sync.
Pitfalls and Silent Failures
The compiler won't save you from logic errors. If you cast a *int to *string, the compiler allows it. At runtime, Go treats the integer bits as a string header. You get a string pointing to random memory. Accessing it causes a panic with runtime error: invalid memory address or nil pointer dereference or garbage output.
Alignment is another trap. If you cast a pointer to a type that requires stricter alignment, the hardware may crash. For example, casting a misaligned *byte to *int64 on some architectures triggers a bus error. The compiler doesn't check alignment during pointer casts. You must ensure the source address satisfies the alignment requirements of the target type. Alignment requirements vary by architecture. On x86-64, the CPU is forgiving and handles misaligned accesses with a performance penalty. On ARM or RISC-V, misaligned accesses trigger a hardware exception. Your code might work on your laptop but crash on a Raspberry Pi or in a cloud container. Use unsafe.Alignof to check requirements.
Garbage collection is the silent killer. The Go runtime tracks pointers to manage memory. If you hide a pointer inside an integer or a byte slice, the GC might not see it. The object gets collected while you still hold the raw address. Dereferencing it later reads freed memory. Use unsafe.Pointer conversions carefully to ensure the GC can still trace your pointers.
unsafe operations do not return errors. This is a fundamental difference from safe Go code. Functions like json.Unmarshal return an error if the data doesn't match. unsafe casts never return an error. They either work or they corrupt memory. The corruption might not manifest immediately. You might cast a pointer, read a field, and get a value that looks plausible but is actually garbage from adjacent memory. The bug propagates through your logic. Hours later, a calculation fails. Debugging requires tools like go vet or race detector, and even then, unsafe bugs can be invisible. Isolate unsafe code to make it easier to audit. The community convention is to keep unsafe usage in small, well-tested helper functions, not scattered across business logic.
The GC doesn't read your mind. Keep pointers visible.
uintptr vs unsafe.Pointer
There is a second type involved in pointer arithmetic: uintptr. uintptr is an integer type large enough to hold a pointer. Converting unsafe.Pointer to uintptr turns the pointer into a plain number. The garbage collector stops tracking the memory behind that number. If the only reference to an object is held as a uintptr, the GC may reclaim the memory. You end up with a dangling pointer. Use uintptr only for arithmetic or storage in non-pointer fields. Convert back to unsafe.Pointer before dereferencing.
// BAD: Storing as uintptr breaks GC tracking.
// The GC sees a number, not a pointer.
addr := uintptr(unsafe.Pointer(pInt))
// If pInt is the only reference, the object may be collected here.
// Dereferencing addr later is undefined behavior.
uintptr is for math. unsafe.Pointer is for memory. Don't mix them up.
When to Use unsafe.Pointer
Use unsafe.Pointer when you need to bridge Go pointers with C pointers in CGo bindings. Use unsafe.Pointer when you are implementing low-level data structures that require direct memory manipulation. Use unsafe.Pointer when you need to bypass type restrictions for performance-critical code that has been profiled and verified. Use standard type assertions or conversions when the types are compatible or can be made compatible with interfaces. Use encoding/binary or unsafe-free serialization when you need to convert data between formats without risking memory safety. Use reflection via reflect.Value when you need dynamic type inspection, even though it is slower than unsafe.
unsafe is a hammer. Use it only when the nail is metal and the wall is concrete.