The Power of Nothing: Optimizing Go with Empty Structs
In the world of systems programming, every byte counts. Go, being a language designed for efficiency and scalability, offers a unique primitive that takes "lightweight" to the absolute extreme: the empty struct, struct{}.
It has no fields, stores no data, and most importantly, occupies zero bytes of memory.
In this post, we'll explore the mechanics of struct{}, verify its zero-size property, and look at practical patterns where "nothing" gives you everything.
The Zero-Size Guarantee
Let's start with the basics. In Go, the size of a struct is the sum of the size of its fields, plus padding for alignment. If a struct has no fields, its size is 0.
We can verify this using the unsafe package:
package main
import (
"fmt"
"unsafe"
)
func main() {
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // Output: 0
}This isn't just a compiler trick; it's a fundamental property of the runtime.
The zerobase Optimization
You might wonder: if a variable has 0 size, does it have an address?
package main
import "fmt"
func main() {
var a, b struct{}
fmt.Printf("%p\n", &a)
fmt.Printf("%p\n", &b)
fmt.Println(&a == &b) // Output: true
}In Go, all zero-sized variables can point to the same memory address. The Go runtime defines a special variable called zerobase.
// runtime/malloc.go
var zerobase uintptrWhen you allocate a struct{}, the runtime doesn't need to allocate new memory. It simply returns a pointer to zerobase (or an address derived from it that effectively points to nothing). This means allocating 1 million struct{}s creates virtually no garbage collection pressure for the values themselves.
Practical Use Cases
Knowing that struct{} is free, how can we use it to optimize our applications?
1. Implementing Sets
Go doesn't have a built-in Set type. The idiomatic way to implement a set is using a map. A common mistake is to use map[string]bool.
// Uses 1 byte per boolean value (plus map overhead)
set := make(map[string]bool)
set["item1"] = trueWhile a bool is small (1 byte), it's not zero. For massive datasets, this adds up. Instead, use map[string]struct{}:
// Uses 0 bytes for the value
set := make(map[string]struct{})
set["item1"] = struct{}{}
// Checking existence
if _, exists := set["item1"]; exists {
// ...
}Why it matters: The map bucket structure in Go stores keys and values. If the value size is 0, the bucket doesn't need to allocate space for values at all, leading to better cache locality and lower memory usage.
2. Signal Channels
Channels are often used for signaling—telling a goroutine "stop" or "I'm done" without sending actual data.
// Common but slightly ambiguous
done := make(chan bool)
done <- trueUsing bool implies the value (true or false) might matter. Using struct{} makes it explicit that only the event matters.
// Idiomatic signal channel
done := make(chan struct{})
// Sending a signal
done <- struct{}{}
// Closing to broadcast to all listeners
close(done)Since struct{} is zero-sized, passing it through a channel is theoretically cheaper, though the main benefit here is semantic clarity.
3. Semaphores / Token Buckets
You can use a buffered channel of empty structs to limit concurrency (a semaphore pattern).
// Allow up to 10 concurrent jobs
sem := make(chan struct{}, 10)
func process() {
sem <- struct{}{} // Acquire token
defer func() { <-sem }() // Release token
// Do heavy work...
}Here, the channel acts purely as a counter. We don't care about what's inside the channel, only that it has space.
4. Method Receivers for Namespaces
Sometimes you want to group utility functions without creating a package or holding state.
type Util struct{}
func (Util) Log(msg string) {
fmt.Println(msg)
}
func main() {
var u Util
u.Log("Hello")
}Since Util is size 0, creating an instance of it is free. This is useful for mocking or satisfying interfaces without data overhead.
Conclusion
The empty struct is a powerful tool in the Go developer's kit. It represents the absence of information, which in itself is information.
- Use
map[K]struct{}for sets to save memory. - Use
chan struct{}for signals to clarify intent. - Remember
zerobase: "Nothing" is efficient because it's shared.
Next time you need to signal an event or check for existence, remember: sometimes, nothing is better than something.