Nothing means something
In the Go programming language we can define our own structures. We can add both exported and unexported fields 1 .
The idea of grouping data together is nothing new in the programming world, but Go language has a unique feature called
empty struct. An empty struct{} is a structure that contains no fields. It is guaranteed by the Go specificaton
that the size of such a structure is zero bytes 2.
“Make the zero value useful.”, Go Proverb
All instances of an empty struct{} have zero size and share the same memory address. This is because if an
allocated object has a size of 0-bytes, it returns a zerobase pointer 3.
var a struct{}
var b struct{}
var c struct{}
fmt.Printf("a: %v %p\n", unsafe.Sizeof(a), &a)
fmt.Printf("b: %v %p\n", unsafe.Sizeof(b), &b)
fmt.Printf("c: %v %p\n", unsafe.Sizeof(c), &c)
// outputs: a: 0 0x85d7a0
// outputs: b: 0 0x85d7a0
// outputs: c: 0 0x85d7a0
Note: the unsafe.Sizeof() actually shows the width of the type struct{} 4.
We can use the feature to our advantage. Let’s say we want to gather if certain words appear in the text. We don’t want
to count their occurrences just if they are present or not. We can use a map with bool as values or struct{} as values.
We are creating two maps here to indicate the difference:
var before runtime.MemStats
var after runtime.MemStats
const n = 10_000_000
runtime.ReadMemStats(&before)
isPresentBool := make(map[int]bool)
for i := 0; i < n; i++ {
var b bool
if i % 2 == 0 {
b = true
}
isPresentBool[i] = b
}
runtime.ReadMemStats(&after)
fmt.Printf("Allocated %d bytes\n", after.TotalAlloc-before.TotalAlloc)
runtime.ReadMemStats(&before)
isPresentStruct := make(map[int]struct{})
for i := 0; i < n; i++ {
isPresentStruct[i] = struct{}{}
}
runtime.ReadMemStats(&after)
fmt.Printf("Allocated %d bytes\n", after.TotalAlloc-before.TotalAlloc)
// output: Allocated 605353608 bytes
// output: Allocated 605306648 bytes
There is a meaningful difference between these two approaches:
- Use
map[int]boolwhen the false value is meaningful - Use
map[int]struct{}when you only care about presence (like in a set)
In practice the saving per-entry is small due to bucket alignment, but at a large scale (millions of keys) it adds up.
We can also use the empty struct in channels to indicate that something happened, not what happened:
done := make(chan struct{})
go func() {
// do work...
done <- struct{}{} // signaling that we are done
}()
<-done
Additionally, we can use an empty struct to implement an interface:
type Codec interface {
Encode(w io.Writer, v interface{}) error
Decode(r io.Reader, v interface{}) error
}
type jsonCodec struct{}
func (jsonCodec) Encode(w io.Writer, v interface{}) error {
return json.NewEncoder(w).Encode(v)
}
func (jsonCodec) Decode(r io.Reader, v interface{}) error {
return json.NewDecoder(r).Decode(v)
}
To summarize:
- An empty struct: Is the smallest building block in Go. Its size is literally 0-bytes.
- Zero memory: Takes up no space. All instances share zerobase address.
- Expresses intent: Signals “no data, only signal” clearly
- No allocation cost: Compiler optimizes these away
- Use as method receiver: An empty struct is used as a type to implement an interface.
- Idiomatic: The standard Go way to build sets and signal channels
That leaves us with my take on the Go Proverb:
“interface{} says nothing, but struct{} says something”, Go Proverb