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.
Translations
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.
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.
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.
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.
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.
This is scary for me.
If I create 2 different named aliases of the empty struct, & each are the receivers of methods, does it mean that the method set of each named type are equal ?
http://play.golang.org/p/3EERnxsbrl
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.
Thanks Dave. That clears it up
Quite interesting! I was thinking if this is a possible use of an empty struct (and if this is idiomatic) http://play.golang.org/p/TxcQXgLuKI
Some people also use map[T]struct{} to implement sets, but I think map[T]bool is better.
Here the boolean version wins, but not by much.
Nobody wins.
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.
The boolean version wins, the zero value makes things nice.
When I needed sets, I often had to write code like this:
But this becomes so much nicer with booleans, when using the second
reset form:
As a relative newcomer to Go, this is a really interesting article! Great stuff. :)
One of the consequences is that each element of an array of empty structs also at the same address:
true:
(&a[9] == &b[0])
as shown on the playground.