An overview of Go structs
A Go struct, short for structure, is a collection of related data.
1
2
3
4
type Person struct {
Name string
Age int
}
Each item in a struct is called a field. Fields can be public, that is, accessible by code outside of the package they are defined in, or private.
Visibility of fields, like visibility elsewhere in Go, is determined by whether the field name begins with a capital letter. Public fields are capitalized.
Structs with private fields will not have those private fields exposed in their documentation. However, their presence is acknowledged by a special comment.
1
2
3
type Person struct {
// contains filtered or unexported fields
}
Fields may optionally define tags. These tags are optional metadata that can be used by other packages.
Tags are strings. Since these strings often contain double quotes, the back quotes syntax is used to avoid needing to escape quotes.
encoding/json
, for example, uses tags to correlate JSON keys with struct fields.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"encoding/json"
"fmt"
"log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
data := `{"name": "Alice", "age": 30}`
var p Person
if err := json.Unmarshal([]byte(data), &p); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", p)
}
You can find other well-known struct tags on the Go wiki.
Empty structs
Empty structs are the most common way to express something of zero size.
1
var empty struct{}
These are useful in situations where a value needs to exist, but does not need to contain any data, such as in a done channel.
The spec says:
A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.
This leads to some interesting consequences, such as an array of empty structs taking up zero memory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
import (
"fmt"
"unsafe"
)
func main() {
var empty struct{}
fmt.Println("size of empty struct:", unsafe.Sizeof(empty))
var empty2 struct{}
fmt.Println("same address?", addr(&empty) == addr(&empty2))
var arr [1000]struct{}
fmt.Println("size of 1000 empty structs:", unsafe.Sizeof(arr))
var arrb [1000]bool
fmt.Println("size of 1000 bools:", unsafe.Sizeof(arrb))
}
func addr[T any](p *T) uintptr { return uintptr(unsafe.Pointer(p)) }
// Output:
// size of empty struct: 0
// same address? true
// size of 1000 empty structs: 0
// size of 1000 bools: 1000
In the gc
Go compiler, all zero-sized entities point to the same location: runtime.zerobase
. This is an implementation detail that may vary from compiler to compiler and should not be relied upon. Since struct{}
taking up no memory is defined in the Go specification, that behavior can be relied upon.
This makes the empty struct the natural choice when using a map to store unique values as keys. In those situations, prefer map[T]struct{}
over map[T]bool
.
Struct initialization
Like other values in Go, structs can be initialized with their zero value by declaration.
1
var p Person // Inits a Person with p.Name == "" and p.Age == 0.
They can also be initialized via new()
, which returns a pointer.
1
p := new(Person) // Inits a Person with zero values and returns *Person.
Structs can also be defined by composite literals. Structs can be initialized with their zero value using a composite literal.
1
p := Person{} // Equivalent to: var p Person
A composite literal can be preceded by a &
to return a pointer.
1
p := &Person{} // Equivalent to: p := new(Person)
Composite literals are often used to define some or all of the fields at initialization time. Field values can be provided as a comma-separated list.
1
p := Person{"Alice", 30}
Note, however, in practice, it is rare to see structs initialized in this way if they have more than one field. This is because it makes code sensitive to the addition of new fields and the re-ordering of existing fields.
This means that, technically, changing the order or number of public fields is a breaking API change. In practice, however, most code defines fields with keys.
1
p := Person{Name: "Alice", Age: 30}
Doing so makes the code no longer sensitive to field order or additional fields.
Size optimization
Because the order of struct fields is user-defined, it is possible to define struct types whose memory usage is not optimal. Go provides a tool for field optimization called fieldalignment.
You can install it as a standalone tool and run it on your code to report on structs whose fields could be optimized.
1
2
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
fieldalignment ./...
Note, however, as the documentation says:
Be aware that the most compact order is not always the most efficient. In rare cases it may cause two variables each updated by its own goroutine to occupy the same CPU cache line, inducing a form of memory contention known as “false sharing” that slows down both goroutines.
Anonymous structs
An anonymous struct is a struct type defined without a name. These are frequently used in conjunction with table-driven tests, where the test data type is unlikely to be reused outside of its specific test.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Example from Stupid Gopher Tricks: https://go.dev/talks/2015/tricks.slide#12
func TestIndex(t *testing.T) {
var tests = []struct {
s string
sep string
out int
}{
{"", "", 0},
{"", "a", -1},
{"fo", "foo", -1},
{"foo", "foo", 0},
{"oofofoofooo", "f", 2},
// etc
}
for _, test := range tests {
actual := strings.Index(test.s, test.sep)
if actual != test.out {
t.Errorf("Index(%q,%q) = %v; want %v", test.s, test.sep, actual, test.out)
}
}
}
Empty structs are usually declared anonymously with the type struct{}
.
1
2
done := make(chan struct{})
done <- struct{}{} // Anonymous empty struct.
Anonymous structs can be arbitrarily deeply nested. This is useful when dealing with nested structures that are only used once, such as when decoding JSON.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Example from Stupid Gopher Tricks: https://go.dev/talks/2015/tricks.slide#9
var data struct {
ID int
Person struct {
Name string
Job string
}
}
const s = `{"ID":42,"Person":{"Name":"George Costanza","Job":"Architect"}}`
err := json.Unmarshal([]byte(s), &data)
if err != nil {
log.Fatal(err)
}
fmt.Println(data.ID, data.Person.Name, data.Person.Job)
Struct embedding
Types can be embedded inside structs. The inner types’ methods and fields will be promoted to the outer struct so long as there is no naming conflict. In the event of a name conflict, the outermost type’s methods and fields win; in the event two names conflict at the same level, attempting to access anything by that name will result in a compiler error.
You can use this to combine multiple types together that you want to associate, but without needing to name and reference each type separately. Here is an example that associates an exec.Cmd
with a sync.Once
. The example code ensures that the command will only be executed one time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
import (
"log"
"os/exec"
"sync"
)
var cmd = struct {
sync.Once
*exec.Cmd
}{
Cmd: exec.Command("whoami"),
}
var wg sync.WaitGroup
func main() {
for range 5 {
wg.Add(1)
go func() {
cmd.Do(func() {
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatal(err)
}
println(string(out))
})
wg.Done()
}()
}
wg.Wait()
}
// Output:
// root
In another example, we associate a number with its mutex guard.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Example from Stupid Gopher Tricks: https://go.dev/talks/2015/tricks.slide#15
package main
import "sync"
var viewCount struct {
sync.Mutex
n int64
}
func main() {
var wg sync.WaitGroup
for range 5 {
wg.Add(1)
go func() {
viewCount.Lock()
viewCount.n++
viewCount.Unlock()
wg.Done()
}()
}
wg.Wait()
println(viewCount.n)
}
// Output:
// 5
If two embedded types share a name, they will need to be disambiguated to avoid compiler errors. This can be done via type aliasing.
1
2
3
4
5
6
7
8
9
// Contrived example: bufio.Reader and io.Reader together
// in one struct would create a name conflict at Reader.
// Type aliasing resolves this conflict and allows the code to compile.
type bufReader = bufio.Reader
type reader struct {
bufReader
io.Reader
}
Types can be embedded to fulfill interfaces. For example, in a situation that calls for an io.ReadWriteCloser
, you can use an io.NopCloser
to add a Close()
method that does nothing onto another type.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import (
"bytes"
"io"
"log"
)
var closeableBuffer = struct {
io.ReadWriter
io.Closer
}{
bytes.NewBufferString("Hello, world!"),
io.NopCloser(nil),
}
func main() {
f(closeableBuffer)
}
func f(r io.ReadWriteCloser) {
defer r.Close()
buf, err := io.ReadAll(r)
if err != nil {
log.Fatal(err)
}
println(string(buf))
}