Structs and Embedding
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18; every go.mod shown has the line go 1.27.
A struct is Go’s only way to group named values into one type, and together with methods and interfaces it
replaces the classes of other languages. Go has no inheritance: reuse is done by embedding one type in another, and
this page explains exactly what that does, what it does not do, and what changed in Go 1.27 (struct literals may now
use promoted-field keys). It also covers literals, comparison and conversion, tags, and memory layout.
The next sections of the reference build on it:
Methods and Receivers,
Interfaces and
Composition and Object-Oriented Design.
For the other composite types see Arrays and Slices,
Maps and
Strings, Runes and UTF-8.
For comparison with another language’s records and inheritance model see the
Rust Reference, whose shelf domain this page shares.
Every example was compiled, vetted with go vet and run with Go 1.27.2; the output shown is the real output (the
compiler and vet messages are likewise copied from real runs). The go.mod of the example module is:
module example.com/shelf
go 1.27
Struct types and fields
A struct type is a sequence of named fields, each with a name and a type. The specification (section Struct types) states the rules:
-
field names must be unique within the struct, except for the blank identifier
_, which may repeat and cannot be read or written; -
a field starts with a lowercase letter (unexported, visible only inside the declaring package) or an uppercase letter (exported). Visibility is per field, not per struct. This is how packages such as
catalogkeep their invariants: other packages cannot set an unexported field, so they must go through a constructor function such asParseISBN; -
a struct may not contain itself, directly or through other struct or array fields. It may refer to itself through a pointer, slice, map, function or channel;
-
a field can carry a tag, a string literal that is metadata for tools (see below);
-
a field declared without a name is an embedded field, the subject of most of this page.
The zero value of a struct has every field set to its own zero value, so a usable Book needs no constructor. Design
types so that their zero value is useful, as strings.Builder, sync.Mutex and bytes.Buffer do.
Fields are selected with a dot, whether the left-hand side is a struct or a pointer to a struct: the compiler inserts
the dereference, so p.Pages is shorthand for (*p).Pages and there is no -> operator. new(T) and &T{...} both
return a *T to a zero (or initialized) struct, and, unlike in C, returning the address of a local struct is safe,
because escape analysis moves it to the heap if needed.
package main
import "fmt"
// ISBN is a validated ISBN-13 (see the catalog package).
type ISBN string
type Author struct {
Name string
Born int
}
type Book struct {
ISBN ISBN
Title string
Authors []Author
Pages int
}
func main() {
// Keyed literal: fields may be omitted and reordered; omitted fields are zero.
dune := Book{
Title: "Dune",
ISBN: "9780441172719",
Authors: []Author{{Name: "Frank Herbert", Born: 1920}},
}
fmt.Printf("%v\n%+v\n", dune, dune)
fmt.Printf("%#v\n", Author{"Ursula K. Le Guin", 1929})
// Zero value: every field has its own zero value.
var empty Book
fmt.Printf("%+v %t\n", empty, empty.Authors == nil)
// Pointers: new, &T{} and automatic dereference in selectors.
p := &dune
p.Pages = 412
q := new(Book)
q.Title = "Emma"
r := &Book{Title: "Ulysses"}
fmt.Println(dune.Pages, (*p).Pages, q.Title, r.Title)
// Assignment copies the struct (but not what a slice field points to).
cp := dune
cp.Title = "Dune (copy)"
cp.Authors[0].Name = "F. Herbert"
fmt.Println(dune.Title, dune.Authors[0].Name)
}
{9780441172719 Dune [{Frank Herbert 1920}] 0}
{ISBN:9780441172719 Title:Dune Authors:[{Name:Frank Herbert Born:1920}] Pages:0}
main.Author{Name:"Ursula K. Le Guin", Born:1929}
{ISBN: Title: Authors:[] Pages:0} true
412 412 Emma Ulysses
Dune F. Herbert
The last two lines show the central rule of value semantics: assigning a struct copies it. The copy shares whatever its
fields point to, though, so cp.Authors[0].Name = … changed the original, because both slice headers refer to the same
backing array. A struct is copied shallowly. A deep copy needs explicit code, such as a Clone method that uses
slices.Clone and a map copy:
package main
import (
"fmt"
"slices"
)
type Author struct{ Name string }
type Book struct {
Title string
Authors []Author
Tags map[string]bool
}
// Clone returns a copy that shares no mutable state with b.
func (b Book) Clone() Book {
b.Authors = slices.Clone(b.Authors)
if b.Tags != nil {
tags := make(map[string]bool, len(b.Tags))
for k, v := range b.Tags {
tags[k] = v
}
b.Tags = tags
}
return b
}
func main() {
a := Book{Title: "Dune", Authors: []Author{{"Frank Herbert"}}, Tags: map[string]bool{"sf": true}}
shallow := a
shallow.Authors[0].Name = "changed"
shallow.Tags["classic"] = true
fmt.Println(a.Authors[0].Name, len(a.Tags))
a.Authors[0].Name = "Frank Herbert"
deep := a.Clone()
deep.Authors[0].Name = "changed"
deep.Tags["new"] = true
fmt.Println(a.Authors[0].Name, len(a.Tags), len(deep.Tags))
}
changed 2
Frank Herbert 2 3
A self-referential struct is only legal through an indirection; embedding the type by value is an error because the struct would have infinite size:
package main
type Node struct {
Value int
Next Node
}
func main() {}
./main.go:3:6: invalid recursive type: Node refers to itself
Composite literals: keyed or positional
A composite literal creates a struct value. There are two forms, and they cannot be mixed in one literal:
-
the keyed form
Book{Title: "Dune", Pages: 412}names the fields: fields may be omitted (they get zero values) and listed in any order, and each name must be a field of the type; -
the positional form
Book{"9780441172719", "Dune", nil, 412}lists a value for every field, in declaration order.
Prefer the keyed form for every non-trivial struct. A positional literal stops compiling the day someone adds a field
to the type, and it silently changes meaning if two fields of the same type are reordered. It is also unreadable
(Point{3, 4} is fine; Book{"9780441172719", "Dune", nil, 412} is not). The compiler reports misuse of either form:
package main
import "fmt"
type Book struct {
Title string
Pages int
}
func main() {
b := Book{"Dune"}
c := Book{Title: "Dune", "412"}
d := Book{Titel: "Dune"}
fmt.Println(b, c, d)
}
./main.go:11:18: too few values in struct literal of type Book
./main.go:12:27: mixture of field:value and value elements in struct literal
./main.go:13:12: unknown field Titel in struct literal of type Book
For a struct type declared in another package the problem is worse: adding a field is a breaking change for every
importer that used a positional literal. go vet reports this as the composites analyzer ("unkeyed fields"), and
it is why standard-library structs are meant to be used with keys:
package main
import (
"fmt"
"os"
)
func main() {
err := fmt.Errorf("denied")
// Positional literal: every field, in declaration order. Breaks when a field is added.
e := os.PathError{"open", "/shelf/books.json", err}
fmt.Println(e.Error())
}
main.go:11:7: os.PathError struct literal uses unkeyed fields
Three more literal forms are worth knowing. An element type can be elided inside a slice, array or map literal
([]Author{{Name: "Frank Herbert"}}, as in the examples above). A pointer element type can be elided too
([]*Author{{Name: "x"}} means &Author{Name: "x"}). And an & in front of a literal, &Book{...}, yields a
pointer to a fresh value, the idiomatic way to write a constructor’s return.
Anonymous structs and the empty struct
A struct type literal can be used without declaring a named type. This is common for one-off data: configuration
read once, table-driven test cases, and decoded JSON fragments. The empty struct struct{} has no fields and size
zero, which makes it the idiomatic element of a set (map[string]struct{}) and the signal-only element of a channel
(chan struct{}):
package main
import "fmt"
type Author struct {
Name string
Born int
}
func main() {
// Anonymous struct: a type literal used once.
cfg := struct {
Addr string
Verbose bool
}{Addr: ":8080", Verbose: true}
fmt.Printf("%+v\n", cfg)
// A table of test-like cases.
cases := []struct {
isbn string
valid bool
}{
{"9780441172719", true},
{"9780441172710", false},
}
for _, c := range cases {
fmt.Println(c.isbn, c.valid)
}
// Anonymous structs with identical fields are assignable to a named type.
var a Author = struct {
Name string
Born int
}{"Jane Austen", 1775}
fmt.Println(a)
// The empty struct occupies no memory.
var e struct{}
seen := map[string]struct{}{"Dune": {}}
_, ok := seen["Dune"]
done := make(chan struct{})
go func() { close(done) }()
<-done
fmt.Println(e, ok, len(seen))
}
{Addr::8080 Verbose:true}
9780441172719 true
9780441172710 false
{Jane Austen 1775}
{} true 1
An anonymous struct and a named struct with identical underlying types are assignable to each other, as the
var a Author = struct{…}{…} line shows, because at least one of the two types is not a named (defined) type.
Comparison and conversion
Two struct values are comparable with == and != if all their fields are comparable; the values are equal when the
corresponding non-blank fields are equal. That makes such structs usable as map keys and as type arguments constrained
by comparable. A struct with a slice, map or function field (or a field that is itself such a struct) is not
comparable, and == on it does not compile; slices, maps and functions can only be compared to nil:
package main
import "fmt"
type Book struct {
Title string
Authors []string
}
func main() {
a := Book{Title: "Dune"}
b := Book{Title: "Dune"}
fmt.Println(a == b)
}
./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)
The usual replacements are slices.Equal and maps.Equal for the fields, reflect.DeepEqual (slow, for tests), or
github.com/google/go-cmp/cmp for readable test diffs. The blank-field idiom _ [0]func() makes a struct
deliberately incomparable, so that a future change cannot make it a map key by accident.
package main
import "fmt"
type Point struct{ X, Y int }
type Edition struct {
Year int
Name string
}
type Shelf struct {
Edition
Row int
}
type Pair struct {
A int `json:"a"`
B int `json:"b"`
}
type Alt struct {
A, B int
}
func main() {
p, q := Point{1, 2}, Point{1, 2}
fmt.Println(p == q, p != Point{2, 1})
// Comparable structs are usable as map keys.
where := map[Point]string{{0, 0}: "origin", {3, 4}: "shelf A"}
fmt.Println(where[Point{3, 4}])
// Conversion ignores tags when the underlying field sequences match.
pair := Pair{1, 2}
alt := Alt(pair)
fmt.Println(alt)
// Comparison of nested comparable fields.
fmt.Println(Shelf{Edition{1965, "first"}, 1} == Shelf{Edition{1965, "first"}, 1})
var x any = Point{1, 2}
fmt.Println(x == any(Point{1, 2}))
}
true true
shelf A
{1 2}
true
true
A struct value can be converted to another struct type if both have identical underlying types, ignoring tags (the
field names, types and order must match). Alt(pair) above compiles although the two types have different tags. Two
named struct types with different field names or order cannot be converted, and conversion never invokes methods.
Embedding
A struct field declared with a type but no name is embedded; its field name is the unqualified type name (Audit
for both Audit and *Audit). The fields and methods of the embedded type are promoted: they can be selected on the
outer struct as if they were declared there. b.By is shorthand for b.Audit.By, and b.Describe() calls
b.Audit.Describe().
package main
import (
"fmt"
"time"
)
// Audit records when a record was created and by whom.
type Audit struct {
Created time.Time
By string
}
func (a Audit) Describe() string {
return fmt.Sprintf("created %s by %s", a.Created.Format(time.DateOnly), a.By)
}
func (a *Audit) Touch(by string) { a.By = by }
type Book struct {
Audit // embedded field: the field name is the type name, Audit
Title string
}
func main() {
b := Book{
Audit: Audit{Created: time.Date(2026, 3, 14, 0, 0, 0, 0, time.UTC), By: "ana"},
Title: "Dune",
}
// Promoted field and methods: b.By is shorthand for b.Audit.By.
fmt.Println(b.By, b.Audit.By)
fmt.Println(b.Describe())
b.Touch("ben") // addressable b, so (&b.Audit).Touch
fmt.Println(b.Describe())
fmt.Printf("%+v\n", b.Audit.By)
}
ana ana
created 2026-03-14 by ana
created 2026-03-14 by ben
ben
Promotion is syntactic sugar for forwarding, and it extends to the method sets of the outer type: all methods with
receiver Audit are in the method set of Book, and the methods with receiver *Audit are in the method set of
*Book (and of Book when the embedded field is itself a pointer). That is what lets a struct satisfy an interface
through its embedded field. The same addressability rule as everywhere applies: b.Touch(…) works because the variable
b is addressable, so the compiler takes &b.Audit, but a value of type Page that embeds a Counter does not
implement an interface that needs Inc when Inc has a pointer receiver:
package main
import "fmt"
// noCopy is the blank-field idiom: it takes no space and carries no data.
type Shelf struct {
_ [0]func() // makes Shelf incomparable
Label string
_ int // padding or reserved field; cannot be read or set by name
}
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func (c Counter) Value() int { return c.n }
type Page struct {
Counter // embedded value: *Page has Inc and Value, Page only Value
}
type Incer interface{ Inc() }
type Valuer interface{ Value() int }
func main() {
var s Shelf
s.Label = "A1"
fmt.Printf("%+v\n", s)
p := Page{}
p.Inc() // fine: p is addressable, so the compiler takes &p
var _ Valuer = p
var _ Incer = &p
// var _ Incer = p // compile error: Page does not implement Incer (method Inc has pointer receiver)
fmt.Println(p.Value())
}
{_:[] Label:A1 _:0}
1
./main.go:14:16: cannot use Page{} (value of struct type Page) as Incer value in variable declaration: Page does not implement Incer (method Inc has pointer receiver)
Embedding is not inheritance
The most important thing to understand is that embedding is composition with automatic forwarding. The outer type
has an embedded value; it is not a subtype of it, and the embedded value knows nothing about the outer one. The
diagram contrasts the two models: on the left, a Go struct holding an Audit; on the right, the inheritance
hierarchy that other languages would draw.
The consequences are:
-
no polymorphism through the outer type: a
Dogthat embeds anAnimalcannot be passed where anAnimalis expected. Only an interface provides polymorphism in Go. -
the receiver is always the embedded value. A method promoted from the embedded type runs with that value as its receiver, so it cannot see, and cannot be overridden by, the methods of the outer type (there is no virtual dispatch).
-
selectors are resolved at the shallowest depth, which is what makes "shadowing" and "ambiguity" below work.
./main.go:15:11: cannot use d (variable of struct type Dog) as Animal value in argument to describe
Shadowing and ambiguity
The selector rules find the field or method at the shallowest depth in the embedding tree. A name declared in the outer struct (depth 0) hides a promoted name (depth 1), and the hidden one is still reachable by writing the embedded field name explicitly:
package main
import "fmt"
type Base struct{ ID string }
func (b Base) Name() string { return "base " + b.ID }
func (b Base) Hello() string { return "hello from " + b.Name() }
type Derived struct {
Base
ID string // shadows Base.ID at a shallower depth
}
// Name shadows the promoted Base.Name for callers of Derived...
func (d Derived) Name() string { return "derived " + d.ID }
func main() {
d := Derived{Base: Base{ID: "inner"}, ID: "outer"}
fmt.Println(d.ID, d.Base.ID)
fmt.Println(d.Name())
// ...but not for methods promoted from Base: the receiver is the embedded Base.
fmt.Println(d.Hello())
}
outer inner
derived outer
hello from base inner
d.ID is the outer field and d.Base.ID the inner one. d.Name() is the method declared on Derived, but d.Hello() is a
method promoted from Base, and it runs with a Base receiver, so it prints base inner, not derived outer. This
is the "no virtual dispatch" rule in action, and the most common surprise for people used to class inheritance.
If two embedded fields at the same depth promote the same name, the name is ambiguous. This is only an error when the ambiguous selector is used; the struct itself is legal, and the fields stay accessible through the qualified names:
package main
import "fmt"
type Audit struct{ ID string }
type Tag struct{ ID string }
type Book struct {
Audit
Tag
Title string
}
func main() {
b := Book{Audit{"a-1"}, Tag{"t-1"}, "Dune"}
fmt.Println(b.Audit.ID, b.Tag.ID)
// b.ID would be ambiguous: both embedded fields are at depth 1.
fmt.Println(b.Title)
}
a-1 t-1
Dune
./main.go:15:16: ambiguous selector b.ID
Embedding pointers and interfaces
An embedded field can be a pointer type *T (where T is a type name, not itself a pointer or an interface) or an
interface type:
-
embedding a pointer shares one value between several outer values and lets the embedded type be allocated lazily. The promoted fields and methods are reached through the pointer, so a
nilembedded pointer panics at run time on first use (the example below compiles, and reading the promoted fieldb.Bypanics); -
embedding an interface is the decorator idiom: the outer type inherits the interface’s method set and overrides the methods it wants to change, delegating to the embedded value for the rest.
sort.Reverseand manyiowrappers are written this way. The embedded interface isniluntil it is set, and calling a promoted method on it panics.
package main
import (
"fmt"
"io"
"os"
"strings"
)
// CountingWriter embeds an interface to decorate any io.Writer.
type CountingWriter struct {
io.Writer
N int
}
func (c *CountingWriter) Write(p []byte) (int, error) {
n, err := c.Writer.Write(p)
c.N += n
return n, err
}
type Logger struct{ Prefix string }
func (l *Logger) Logf(format string, args ...any) {
fmt.Printf(l.Prefix+format+"\n", args...)
}
// Service embeds a pointer: the Logger is shared, and may be nil.
type Service struct {
*Logger
Name string
}
func main() {
cw := &CountingWriter{Writer: os.Stdout}
fmt.Fprintln(cw, "written through the decorator")
io.Copy(cw, strings.NewReader("and again\n"))
fmt.Println(cw.N)
s := Service{Logger: &Logger{Prefix: "[svc] "}, Name: "shelfd"}
s.Logf("starting %s", s.Name)
var bare CountingWriter // embedded interface is nil
defer func() { fmt.Println("recovered:", recover()) }()
bare.Write([]byte("x"))
}
written through the decorator
and again
40
[svc] starting shelfd
recovered: runtime error: invalid memory address or nil pointer dereference
package main
import "fmt"
type Audit struct{ By string }
type Book struct {
*Audit
Title string
}
func main() {
b := Book{Title: "Dune"}
fmt.Println(b.Title)
fmt.Println(b.By)
}
Dune
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=...]
Promoted-field keys in composite literals (Go 1.27)
Until Go 1.26, the keys of a keyed struct literal had to be fields declared directly in the struct. Promoted fields could be read and assigned with selectors, but a literal had to spell out the embedded value, which is verbose when embedding is used for reuse:
// Go 1.26 and earlier: the embedded value had to be written explicitly.
b := Book{Audit: Audit{Created: created, By: "ana"}, Edition: Edition{Year: 1965}, Title: "Dune"}
Go 1.27 changes this. In the words of the release notes, "A key in a struct literal may now be any valid field selector for the struct type, not just a (top-level) field name of the struct." Promoted fields can be used as keys directly:
package main
import (
"fmt"
"time"
)
type Audit struct {
Created time.Time
By string
}
type Edition struct {
Year int
Name string
}
type Book struct {
Audit
Edition
Title string
}
func main() {
created := time.Date(2026, 3, 14, 0, 0, 0, 0, time.UTC)
// Before Go 1.27 the promoted fields had to be wrapped in the embedded type.
old := Book{
Audit: Audit{Created: created, By: "ana"},
Edition: Edition{Year: 1965},
Title: "Dune",
}
// Go 1.27: keys may name promoted fields directly.
b := Book{Created: created, By: "ana", Year: 1965, Title: "Dune"}
fmt.Println(old == b, b.By, b.Year, b.Title)
// Different embedded fields may be initialised in different styles.
c := Book{Audit: Audit{By: "ben"}, Year: 1815, Title: "Emma"}
fmt.Println(c.By, c.Year, c.Created.IsZero())
}
true ana 1965 Dune
ben 1815 true
The rules, checked against Go 1.27.2:
-
each key must be a field the selector rules would accept, so a name that is ambiguous at its depth is an unknown field in the literal (
unknown field ID in struct literal of type Bookwhen two embedded structs both promoteID). Promoted methods are not fields and are not accepted as keys; -
you cannot name both a promoted field and its enclosing embedded field in the same literal:
./main.go:13:37: cannot specify promoted field By and enclosing embedded field Audit
-
a key cannot reach a field through an embedded pointer, because a literal cannot allocate the pointee; spell the pointer out instead (
Book{Audit: &Audit{By: "ben"}}):
./main.go:13:12: invalid implicit pointer indirection to reach By
-
the feature is gated by the module’s
goline. Withgo 1.26ingo.modthe same program is rejected, andgo vetrepeats the message:
use of promoted field Audit.Created in struct literal of type Book requires go1.27 or later (-lang was set to go1.26; check go.mod)
A positional literal still has to list the embedded fields themselves, unchanged. The change makes literals of embedded structs more readable; it does not make embedding into inheritance. Source: Go 1.27 release notes: Language changes and Composite literals.
Struct tags
A field can be followed by a tag, a string literal that is part of the type. By convention it is a list of
key:"value" pairs separated by spaces; the compiler ignores it, but encoding/json, database/sql mappers,
validators and ORMs read it at run time with the reflect package. Use a raw (backquoted) string to avoid escaping
the inner quotes. reflect.StructTag has two accessors: Get(key) returns the value or the empty string, and
Lookup(key) also reports whether the key was present, which distinguishes a missing key from an empty value.
reflect.TypeFor[T]() (Go 1.22) obtains the reflect.Type without needing a value.
package main
import (
"fmt"
"reflect"
)
type Book struct {
ISBN string `json:"isbn" db:"isbn,primary"`
Title string `json:"title,omitempty" validate:"required"`
Secret string `json:"-"`
Pages int `json:"pages,omitzero"`
hidden int
}
func main() {
t := reflect.TypeFor[Book]()
for i := range t.NumField() {
f := t.Field(i)
fmt.Printf("%-6s exported=%-5t tag=%q\n", f.Name, f.IsExported(), f.Tag)
}
f, _ := t.FieldByName("ISBN")
fmt.Println(f.Tag.Get("json"), f.Tag.Get("db"), f.Tag.Get("xml") == "")
v, ok := f.Tag.Lookup("xml")
fmt.Printf("%q %t\n", v, ok)
f, _ = t.FieldByName("Secret")
v, ok = f.Tag.Lookup("json")
fmt.Printf("%q %t\n", v, ok)
}
ISBN exported=true tag="json:\"isbn\" db:\"isbn,primary\""
Title exported=true tag="json:\"title,omitempty\" validate:\"required\""
Secret exported=true tag="json:\"-\""
Pages exported=true tag="json:\"pages,omitzero\""
hidden exported=false tag=""
isbn isbn,primary true
"" false
"-" true
Source: reflect.StructTag and reflect.StructTag.Lookup
Tags of unexported fields are legal but pointless for tools that cannot set them; encoding/json ignores
unexported fields entirely, and a tag value "-" means "skip this field". The meaning of the options (omitempty,
and omitzero, added in Go 1.24) belongs to the packages that read them; see
encoding/json and
Reflection. go vet checks the syntax of tags with the structtag
analyzer, because a malformed tag silently becomes invisible to every tool:
package main
import "fmt"
type Book struct {
Title string `json: "title"`
}
func main() { fmt.Println(Book{}) }
main.go:6:2: struct field tag `json: "title"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
The same analyzer reports duplicate json or xml names within one struct, which would make encoded data collide.
(For a tag whose key has the right syntax but a typo, no tool can help.)
Source: reflect.StructTag and cmd/vet
Memory layout, alignment and padding
A struct’s fields are laid out in memory in declaration order. The compiler inserts padding so that each field is
aligned to a multiple of its alignment (a bool to 1, an int32 to 4, an int64 to 8 on 64-bit platforms), and rounds
the whole struct up to a multiple of the largest field alignment. The order of fields therefore affects size:
package main
import (
"fmt"
"structs"
"unsafe"
)
type Loose struct {
Active bool
ID int64
Flag bool
Pages int32
Rare bool
}
type Tight struct {
ID int64
Pages int32
Active bool
Flag bool
Rare bool
}
// Header has the memory layout the host (C) ABI would give the same fields.
type Header struct {
_ structs.HostLayout
Magic uint32
Size uint16
Kind uint8
}
type Empty struct{}
type WithEmpty struct {
A int32
E Empty
}
func main() {
fmt.Println("Loose:", unsafe.Sizeof(Loose{}), unsafe.Alignof(Loose{}))
fmt.Println("Tight:", unsafe.Sizeof(Tight{}), unsafe.Alignof(Tight{}))
var l Loose
fmt.Println(unsafe.Offsetof(l.Active), unsafe.Offsetof(l.ID), unsafe.Offsetof(l.Flag),
unsafe.Offsetof(l.Pages), unsafe.Offsetof(l.Rare))
var t Tight
fmt.Println(unsafe.Offsetof(t.ID), unsafe.Offsetof(t.Pages), unsafe.Offsetof(t.Active),
unsafe.Offsetof(t.Flag), unsafe.Offsetof(t.Rare))
fmt.Println("Header:", unsafe.Sizeof(Header{}), "Empty:", unsafe.Sizeof(Empty{}), "WithEmpty:", unsafe.Sizeof(WithEmpty{}))
}
Loose: 32 8
Tight: 16 8
0 8 16 20 24
0 8 12 13 14
Header: 8 Empty: 0 WithEmpty: 8
Loose and Tight hold exactly the same five fields, but Loose is 32 bytes and Tight 16: in Loose the two
bool`s sit between 8-byte and 4-byte fields and force 7, 3 and 7 bytes of padding. Ordering fields from largest to
smallest alignment removes the holes. This rarely matters for a handful of values but is worth doing for types stored
by the million. The `fieldalignment analyzer in golang.org/x/tools/go/analysis/passes/fieldalignment reports
structs that could be smaller (it is not part of go vet); the size, alignment and offsets above come from the
unsafe functions Sizeof, Alignof and Offsetof, which are constants evaluated at compile time. See
unsafe and Low-Level Programming.
Three details of layout:
-
a zero-size field (
struct{}or[0]T) at the end of a struct gets padding so that taking its address cannot point past the object, which is why a struct made of anint32followed by an empty-struct field is 8 bytes and not 4; put empty-struct fields first, or avoid them; -
the layout is a property of the implementation: the language guarantees only the alignment rules, and the Go compiler is allowed to reorder fields in the future. It does not today;
-
since Go 1.23 the package
structsprovides the marker typestructs.HostLayout. A struct that has astructs.HostLayoutfield (usually the blank field_ structs.HostLayout) promises that its layout follows the host platform’s ABI conventions, which matters when the struct is shared with C code or the operating system (cgo, syscalls). Without the marker Go reserves the right to choose a different order. The marker takes no space (Headerabove is 8 bytes:uint32anduint16anduint8plus one byte of tail padding).
Pitfalls and checks
-
Copying a struct that contains a lock (
sync.Mutex,sync.WaitGroup, or anything with anoCopyfield) copies the lock state and defeats it. Thecopylocksanalyzer ofgo vetreports passing and returning such structs by value. Use pointers for types that own locks, as the shelf’sStoredoes:
package main
import (
"fmt"
"sync"
)
type Store struct {
mu sync.Mutex
books map[string]int
}
func count(s Store) int { return len(s.books) }
func main() {
s := Store{books: map[string]int{"Dune": 1}}
fmt.Println(count(s))
}
main.go:13:14: count passes lock by value: example.com/errs/f4.Store contains sync.Mutex
main.go:17:20: call of count copies lock value: example.com/errs/f4.Store contains sync.Mutex
Source: copylock analyzer
-
Struct values in maps are not addressable:
m[k].Pages+` does not compile. Read the value, change it and store it back, or store pointers (`+map[string]*Book). -
Range copies the element: in
for _, b := range books,bis a copy, sob.Pages` has no effect on the slice. Index (`books[i].Pages) or range over pointers. Since Go 1.22 each iteration has its ownb, so taking&bis no longer an aliasing bug, but it still points to the copy, not to the slice element. -
A nil struct pointer panics on field access, whether the field is direct or promoted:
package main
import "fmt"
type Book struct{ Title string }
func main() {
var b *Book
fmt.Println(b.Title)
}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=...]
-
A struct that embeds
sync.MutexexposesLockandUnlockto its callers, which is rarely wanted. Prefer a named, unexported field (mu sync.Mutex) so the locking stays an implementation detail.
Where the books are outdated or wrong
Bodner’s chapter 3 describes structs, anonymous structs, comparison and conversion accurately, and chapter 7 explains
that embedding is not inheritance, which is the central point of this page. What neither book covers is the Go 1.27
extension to composite literals (promoted-field keys), structs.HostLayout (Go 1.23), reflect.TypeFor (Go 1.22) and the
omitzero tag option (Go 1.24); the helper functions that books use to obtain a pointer to a literal value are replaced
by new(expr) since Go 1.26.
Vivien’s chapter 7 covers structs, embedding, promotion and field tags. Its examples predate per-iteration loop
variables (Go 1.22), so copies of the loop variable shown in struct-slice code are no longer needed, and
the discussion of tags predates omitzero and the standard structtag vet check in its current form. Chapter 8
(methods and composition) and chapter 4 (memory layout with unsafe.Sizeof) remain correct, but the alignment output
in the book depends on the platform; the examples above were run on a 64-bit target.
Version notes
-
Go 1.18: generic struct types, see Generics.
-
Go 1.22:
reflect.TypeFor; per-iteration loop variables. -
Go 1.23:
structs.HostLayout. -
Go 1.24: the
omitzerotag option forencoding/json. -
Go 1.26:
new(expr). -
Go 1.27: promoted-field keys in struct literals (needs
go 1.27ingo.mod).