The empty struct

Introduction

This post explores the properties of my favourite Go data type, the empty struct.

The empty struct is a struct type that has no fields. Here are a few examples in named and anonymous forms

type Q struct{}
var q struct{}

So, if an empty struct contains no fields, contains no data, what use is it ? What can we do with it ?

Width

Before diving into the empty struct itself, I wanted to take a brief detour to talk about width.

The term width comes, like most terms, from the gc compiler, although its etymology probably goes back decades.

Width describes the number of bytes of storage an instance of a type occupies. As a process’s address space is one dimensional, I think width is a more apt than size.

Width is a property of a type. As every value in a Go program has a type, the width of the value is defined by its type and is always a multiple of 8 bits.

We can discover the width of any value, and thus the width of its type using the unsafe.Sizeof() function.

var s string
var c complex128
fmt.Println(unsafe.Sizeof(s))	 // prints 8
fmt.Println(unsafe.Sizeof(c))	 // prints 16

http://play.golang.org/p/4mzdOKW6uQ

The width of an array type is a multiple of its element type.

var a [3]uint32
fmt.Println(unsafe.Sizeof(a)) // prints 12

http://play.golang.org/p/YC97xsGG73

Structs provide a more flexible way of defining composite types, whose width is the sum of the width of the constituent types, plus padding

type S struct {
        a uint16
        b uint32
}
var s S
fmt.Println(unsafe.Sizeof(s)) // prints 8, not 6

The example above demonstrates one aspect of padding, that a value must be aligned in memory to a multiple of its width. In this case there are two bytes of padding added by the compiler between a and b.

Update Russ Cox has kindly written to explain that width is unrelated to alignment. You can read his comment below.

An empty struct

Now that we’ve explored width it should be evident that the empty struct has a width of zero. It occupies zero bytes of storage.

var s struct{}
fmt.Println(unsafe.Sizeof(s)) // prints 0

As the empty struct consumes zero bytes, it follows that it needs no padding. Thus a struct comprised of empty structs also consumes no storage.

type S struct {
        A struct{}
        B struct{}
}
var s S
fmt.Println(unsafe.Sizeof(s)) // prints 0

http://play.golang.org/p/PyGYFmPmMt

What can you do with an empty struct

True to Go’s orthogonality, an empty struct is a struct type like any other. All the properties you are used to with normal structs apply equally to the empty struct.

You can declare an array of structs{}s, but they of course consume no storage.

var x [1000000000]struct{}
fmt.Println(unsafe.Sizeof(x)) // prints 0

http://play.golang.org/p/0lWjhSQmkc

Slices of struct{}s consume only the space for their slice header. As demonstrated above, their backing array consumes no space.

var x = make([]struct{}, 1000000000)
fmt.Println(unsafe.Sizeof(x)) // prints 12 in the playground

http://play.golang.org/p/vBKP8VQpd8

Of course the normal subslice, len, and cap builtins work as expected.

var x = make([]struct{}, 100)
var y = x[:50]
fmt.Println(len(y), cap(y)) // prints 50 100

http://play.golang.org/p/8cO4SbrWVP

You can take the address of a struct{} value, when it is addressable, just like any other value.

var a struct{}
var b = &a

Interestingly, the address of two struct{} values may be the same.

var a, b struct{}
fmt.Println(&a == &b) // true

http://play.golang.org/p/uMjQpOOkX1

This property is also observable for []struct{}s.

a := make([]struct{}, 10)
b := make([]struct{}, 20)
fmt.Println(&a == &b)       // false, a and b are different slices
fmt.Println(&a[0] == &b[0]) // true, their backing arrays are the same

http://play.golang.org/p/oehdExdd96

Why is this? Well if you think about it, empty structs contain no fields, so can hold no data. If empty structs hold no data, it is not possible to determine if two struct{} values are different. They are in effect, fungible.

a := struct{}{} // not the zero value, a real new struct{} instance
b := struct{}{}
fmt.Println(a == b) // true

http://play.golang.org/p/K9qjnPiwM8

note: this property is not required by the spec, but it does note that Two distinct zero-size variables may have the same address in memory.

struct{} as a method receiver

Now we’ve demonstrated that empty structs behave just like any other type, it follows that we may use them as method receivers.

type S struct{}

func (s *S) addr() { fmt.Printf("%p\n", s) }

func main() {
        var a, b S
        a.addr() // 0x1beeb0
        b.addr() // 0x1beeb0
}

http://play.golang.org/p/YSQCczP-Pt

In this example it shows that the address of all zero sized values is 0x1beeb0. The exact address will probably vary for different versions of Go.

Wrapping up

Thank you for reading this far. At close to 800 words this article turned out to be longer than expected, and there was still more I was planning to write.

While this article concentrated on language obscura, there is one important practical use of empty structs, and that is the chan struct{} construct used for signaling between go routines

I’ve talked about the use of chan struct{} in my Curious Channels article.


Update Damian Gryski pointed out that I had omitted Brad Fitzpatrick’s iter package. I’ll leave it as an exercise to the reader to explore the profound implications of Brad’s contribution.

11 thoughts on “The empty struct

  1. Russ Cox

    It’s not true that “a value must be aligned in memory to a multiple of its width.” Each type has another property, its alignment. Alignments are always powers of two. The alignment of a basic type is usually equal to its width, but the alignment of a struct is the maximum alignment of any field, and the alignment of an array is the alignment of the array element. The maximum alignment of any value is therefore the maximum alignment of any basic type. Even on 32-bit systems this is often 8 bytes, because atomic operations on 64-bit values typically require 64-bit alignment.

    To be concrete, a struct containing 3 int32 fields has alignment 4 but width 12.

    It is true that a value’s width is always a multiple of its alignment. One implication is that there is no padding between array elements.

  2. Russ Cox

    Also, the address 0x1beeb0 in your example does indeed vary. It is the address of the (non-zero width) global variable runtime.zerobase, which you can see if you run ‘go tool nm’ on your binary.

    1. Dave Cheney Post author

      Thanks for your comments Russ. I probably didn’t do a good enough job explaining that just because today’s playground places runtime.zerobase at 0x1beeb0, nobody should rely on that being a constant.

  3. Andy Balholm

    You missed my favorite use for the empty struct: singletons!

    Where an OOP programmer would use some variant of the Singleton design pattern, to ensure that only one instance of his class is created, in Go you can use an empty struct, and store all your data in global variables. There will only be one instance of the type, since all empty structs are interchangeable.

    Of course you could just use global variables and functions instead, unless you need to fulfill an interface. But if you need to fulfill an interface, it’s quite useful. You see this pattern a lot in the go.text/encoding packages.

      1. Dave Cheney Post author

        Yes and No. Types with different names have different methods sets even if they share the same base type. Instances of type S struct{} and type T struct will have the same value for their receiver, but then so will two instances of S.

  4. Aram

    Some people also use map[T]struct{} to implement sets, but I think map[T]bool is better.

        // creation
        m1 := make(map[T]struct{})
        var empty struct{}	// to avoid ugly struct{}{}
        
        m2 := make(map[T]bool)

    Here the boolean version wins, but not by much.

        // set
        m1[t] = empty
        m2[t] = true

    Nobody wins.

        // reset
        delete(m1[t])
        
        delete(m2[t])
        m2[t] = false

    Some people claim the boolean version loses here, because the second
    form (explicit assignment to false) is wrong. I disagree. The
    second form is very useful as we’ll see later.

        // test
        if _, ok := m1[t]; ok { ...
        
        if m2[t] { ...

    The boolean version wins, the zero value makes things nice.

    When I needed sets, I often had to write code like this:

        func foo(..., b bool) {
            ...
            
            if b {
                    delete(m1[t])
            }

    But this becomes so much nicer with booleans, when using the second
    reset form:

        func foo(..., b bool) {
            ...
            
            m2[t] = b

Comments are closed.