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]bool when 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

Copyright © 2026 by Michal Przybylowicz