Why Go’s Empty Struct Takes Zero Bytes and How to Leverage It
This article explains the zero‑byte nature of Go’s empty struct, how the runtime treats zero‑size allocations, why multiple instances share the same address, and demonstrates practical patterns such as set maps, signal channels, and worker coordination that improve memory efficiency and performance.
1. What Is an Empty Struct?
An empty struct in Go is declared as struct{}. It contains no fields and occupies zero bytes. Example code shows that unsafe.Sizeof(Empty{}) prints 0 bytes, while typical types like int, string, and map[string]int consume 8, 16, and 8 bytes respectively on a 64‑bit system.
2. Same‑Address Phenomenon
Multiple instances of an empty struct share the same memory address. The following snippet prints identical addresses for e1 and e2, confirming that the Go runtime deliberately points all zero‑size values to a single location.
type Empty struct{}
var e1, e2 Empty
fmt.Printf("e1 address: %p, e2 address: %p, same: %t
", &e1, &e2, &e1 == &e2)
// Output: e1 address: 0x123456, e2 address: 0x123456, same: true3. How the Runtime Handles Zero‑Size Allocation
The Go runtime contains a special mechanism for zero‑byte allocations in runtime/malloc.go. A global variable zerobase serves as the base address for all such allocations. The core allocation function mallocgc short‑circuits when size == 0 and returns unsafe.Pointer(&zerobase), avoiding any real heap allocation.
// base address for all 0‑byte allocations
var zerobase uintptr
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ... other code ...
// Short‑circuit zero‑sized allocation requests.
if size == 0 {
return unsafe.Pointer(&zerobase)
}
// ... remaining allocation logic ...
}This design yields three important effects:
No actual memory allocation: Zero‑size requests do not consume heap memory.
Consistent address: All empty struct instances point to the same zerobase address.
Fast allocation: The runtime bypasses the normal allocation path entirely.
4. Real‑World Use Cases
4.1 Using a Map as a Set (map[K]struct{})
Because an empty struct occupies no space, map[K]struct{} is the idiomatic way to implement a set in Go. The example below tracks processed orders without storing extra data:
// Create a set of processed orders
processedOrders := make(map[string]struct{})
// Mark an order as processed
processedOrders["ORD001"] = struct{}{}
// Check if an order exists
if _, exists := processedOrders["ORD001"]; exists {
fmt.Println("Order ORD001 has been processed")
}Compared with map[string]bool, the empty‑struct set uses less memory, conveys clear intent (only existence matters), and avoids the ambiguity of true/false values.
4.2 Signal Channels (chan struct{})
When only a notification is needed, a channel of empty structs provides a lightweight signaling mechanism. The following code shows a timer that signals completion via a closed channel:
// Timer task completion signal
timerDone := make(chan struct{})
go func() {
fmt.Println("Timer task started...")
time.Sleep(2 * time.Second)
fmt.Println("Timer task completed!")
close(timerDone)
}()
fmt.Println("Waiting for timer to complete...")
<-timerDone
fmt.Println("Received timer completion signal")Typical scenarios include task‑completion notifications, graceful shutdown signals, and coordination between goroutines.
4.3 Multi‑Worker Coordination
Empty‑struct channels can coordinate many workers efficiently. Each worker sends a struct{} value when finished, and the main goroutine waits for all signals:
workers := 3
workerDone := make(chan struct{}, workers)
for i := 1; i <= workers; i++ {
go func(id int) {
fmt.Printf("Worker %d working...
", id)
time.Sleep(time.Duration(id) * 500 * time.Millisecond)
fmt.Printf("Worker %d done!
", id)
workerDone <- struct{}{}
}(i)
}
for i := 0; i < workers; i++ {
<-workerDone
}
fmt.Println("All workers completed!")This pattern avoids transmitting unnecessary data and minimizes memory and runtime overhead.
5. Performance Considerations
Using empty structs yields measurable performance benefits:
Reduced memory pressure: map[K]struct{} consumes far less memory than alternatives.
Better cache locality: Smaller footprints improve CPU cache utilization.
Faster GC cycles: Fewer objects need to be scanned, shortening garbage‑collection pauses.
In high‑throughput services handling millions of entries or signals, these savings accumulate into lower hardware costs and more stable systems, especially in micro‑service architectures.
6. Common Patterns in Production Code
Deduplication
seen := make(map[string]struct{})
for _, item := range items {
if _, exists := seen[item.ID]; exists {
continue
}
seen[item.ID] = struct{}{}
process(item)
}Rate Limiting
type RateLimiter struct {
requests map[string]struct{}
mu sync.Mutex
}
func (r *RateLimiter) Allow(key string) bool {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.requests[key]; exists {
return false
}
r.requests[key] = struct{}{}
return true
}Fan‑Out with Completion Tracking
func processItems(items []Item) {
done := make(chan struct{}, len(items))
for _, item := range items {
go func(i Item) {
process(i)
done <- struct{}{}
}(item)
}
for range items {
<-done
}
}These patterns illustrate how the zero‑size property of struct{} can be exploited for memory‑efficient data structures and lightweight synchronization primitives in production Go code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
