#Go Unsafe Package Tutorial
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.
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.
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.
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.Pointercan convert to any pointer typeuintptrcan be converted to and fromunsafe.Pointer
Let’s see a practical example - inspecting the raw bytes of an integer by reinterpreting it as a float64.
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:
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
Continue Learning
Go Encryption and Decryption using AES - Tutorial
In this tutorial, we are going to look at how you can do both encryption and decryption using AES in Go
Writing a Frontend Web Framework with WebAssembly And Go
In this tutorial, we are going to look at building a really simple frontend web framework using WebAssembly and Go
Building a Solid Continuous Integration Pipeline with TravisCI for Your Go Projects
In this tutorial, we look at how you can build a solid CI pipeline with Travis for your Go Projects
Go Face Recognition Tutorial - Part 1
In this tutorial, we are going to look at building a face recognition system using Go