#Go Unsafe Package Tutorial

Elliot Forbes Elliot Forbes · Oct 6, 2018 · 5 min read

In this tutorial, we’re going to explore Go’s unsafe package - a powerful but dangerous set of tools that let you work directly with memory. Before we dive in, let’s be clear: the unsafe package bypasses Go’s type safety and memory safety guarantees. You should rarely need it in your day-to-day code, but when you do, it’s invaluable. Let’s see what we can do with it.

Introduction to unsafe

The unsafe package exists because sometimes you need to break the rules. Maybe you’re interfacing with C code via cgo, implementing a high-performance serialization library, or optimizing a critical path. The unsafe package gives you access to the underlying memory layout of your data structures.

Here’s the golden rule: use unsafe sparingly. If you find yourself reaching for unsafe in regular application code, there’s probably a better way.

unsafe.Sizeof

Sizeof tells you how many bytes a type occupies in memory. It’s straightforward - pass a variable of any type, and you get back its size in bytes.

main.go
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var b bool
	var i int
	var f float64
	var s string
	var sl []int
	var iface interface{}

	fmt.Printf("bool: %d bytes\n", unsafe.Sizeof(b))
	fmt.Printf("int: %d bytes\n", unsafe.Sizeof(i))
	fmt.Printf("float64: %d bytes\n", unsafe.Sizeof(f))
	fmt.Printf("string: %d bytes\n", unsafe.Sizeof(s))
	fmt.Printf("[]int: %d bytes\n", unsafe.Sizeof(sl))
	fmt.Printf("interface{}: %d bytes\n", unsafe.Sizeof(iface))
}

Notice that Sizeof returns the size of the type itself, not the data it points to. A string header is typically 16 bytes (pointer plus length), but the actual string data isn’t counted. This is crucial to understand when optimizing memory usage.

unsafe.Alignof

Every type has alignment requirements - the number of bytes at which it should start in memory. CPUs fetch data most efficiently when it’s properly aligned. Alignof reveals these requirements.

main.go
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var b bool
	var i8 int8
	var i16 int16
	var i32 int32
	var i64 int64
	var f64 float64

	fmt.Printf("bool alignment: %d\n", unsafe.Alignof(b))
	fmt.Printf("int8 alignment: %d\n", unsafe.Alignof(i8))
	fmt.Printf("int16 alignment: %d\n", unsafe.Alignof(i16))
	fmt.Printf("int32 alignment: %d\n", unsafe.Alignof(i32))
	fmt.Printf("int64 alignment: %d\n", unsafe.Alignof(i64))
	fmt.Printf("float64 alignment: %d\n", unsafe.Alignof(f64))
}

Proper alignment lets CPUs load data in single operations rather than multiple memory accesses. Misaligned data can slow down your program significantly.

unsafe.Offsetof

Structs are where alignment really matters. Offsetof shows you the byte offset of each field within a struct. Go may add padding between fields to satisfy alignment requirements.

main.go
package main

import (
	"fmt"
	"unsafe"
)

type Person struct {
	Name string
	Age  int32
	Tall bool
}

func main() {
	fmt.Printf("Person size: %d bytes\n", unsafe.Sizeof(Person{}))
	fmt.Printf("Name offset: %d\n", unsafe.Offsetof(Person{}.Name))
	fmt.Printf("Age offset: %d\n", unsafe.Offsetof(Person{}.Age))
	fmt.Printf("Tall offset: %d\n", unsafe.Offsetof(Person{}.Tall))
}

Run this and you’ll see that fields are padded with extra bytes to align properly. This is automatic alignment, but it costs memory. By reordering fields, we can eliminate unnecessary padding.

unsafe.Pointer

This is the workhorse of the unsafe package. unsafe.Pointer is a generic pointer type that any pointer can be converted to, and vice versa. Here are the conversion rules:

  • Any pointer type can convert to unsafe.Pointer
  • unsafe.Pointer can convert to any pointer type
  • uintptr can be converted to and from unsafe.Pointer

Let’s see a practical example - inspecting the raw bytes of an integer by reinterpreting it as a float64.

main.go
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// Create an int64 with a specific bit pattern
	var num int64 = 1234567890

	// Convert the pointer to the int64 to an unsafe.Pointer,
	// then cast it to a *float64
	floatPtr := (*float64)(unsafe.Pointer(&num))

	fmt.Printf("Original int64: %d\n", num)
	fmt.Printf("Interpreted as float64: %v\n", *floatPtr)

	// We can also work with the raw memory address
	addr := uintptr(unsafe.Pointer(&num))
	fmt.Printf("Memory address: 0x%x\n", addr)
}

This is powerful but dangerous - you’re telling Go to ignore type information and interpret memory however you want. Use this only when you know exactly what you’re doing.

Struct Field Reordering for Memory Optimization

Let’s see how field ordering impacts memory usage. Consider these two versions of the same struct:

main.go
package main

import (
	"fmt"
	"unsafe"
)

// Poorly ordered struct - creates unnecessary padding
type PersonBad struct {
	Name   string
	Active bool
	Age    int32
	Score  float64
}

// Optimized struct - fields ordered by size
type PersonGood struct {
	Score  float64
	Name   string
	Age    int32
	Active bool
}

func main() {
	fmt.Printf("PersonBad size: %d bytes\n", unsafe.Sizeof(PersonBad{}))
	fmt.Printf("PersonGood size: %d bytes\n", unsafe.Sizeof(PersonGood{}))

	fmt.Printf("\nPersonBad field offsets:\n")
	fmt.Printf("  Name: %d\n", unsafe.Offsetof(PersonBad{}.Name))
	fmt.Printf("  Active: %d\n", unsafe.Offsetof(PersonBad{}.Active))
	fmt.Printf("  Age: %d\n", unsafe.Offsetof(PersonBad{}.Age))
	fmt.Printf("  Score: %d\n", unsafe.Offsetof(PersonBad{}.Score))

	fmt.Printf("\nPersonGood field offsets:\n")
	fmt.Printf("  Score: %d\n", unsafe.Offsetof(PersonGood{}.Score))
	fmt.Printf("  Name: %d\n", unsafe.Offsetof(PersonGood{}.Name))
	fmt.Printf("  Age: %d\n", unsafe.Offsetof(PersonGood{}.Age))
	fmt.Printf("  Active: %d\n", unsafe.Offsetof(PersonGood{}.Active))
}

By ordering fields from largest alignment requirement to smallest, we minimize wasted padding. This is especially important when you have thousands of these structures in memory.

When Should You Use unsafe?

Most Gophers will never need the unsafe package. But there are legitimate use cases:

  • CGO interop: Calling C libraries requires unsafe conversions
  • High-performance serialization: Zero-copy operations on binary data
  • Memory-mapped I/O: Working with hardware-mapped memory
  • Custom data structures: Implementing specialized collections that need precise memory control

If you’re reaching for unsafe anywhere else, pause and ask yourself if there’s a safer alternative. There usually is.

Conclusion

The unsafe package is called “unsafe” for a reason - it removes Go’s protective guarantees and puts the responsibility on you. Used correctly, it unlocks performance and enables system-level programming. Used carelessly, it causes mysterious bugs that are incredibly hard to track down.

Master the fundamentals of Go first. Learn pointers, understand memory allocation, and build solid applications. Only when you genuinely need low-level memory access should you reach for unsafe.

Further Reading