Go Module 5 – Structs, pointers and methods
Structs and struct literals, including Go 1.27
Define Go struct types, build values with keyed and positional literals, compare them, use anonymous structs and tags, and set promoted fields in Go 1.27.
What you will learn
- Define struct types and build values with keyed and positional literals
- Predict what assigning a struct, comparing two and using one as a map key do
- Use anonymous structs for one-off values, and field tags for encoding/json
- Set promoted fields directly in a struct literal, as Go 1.27 allows
- Predict how the order of the fields changes the size of a struct
Before you start
What you’ll learn · Before you start
What you will learn
- Define struct types and build values with keyed and positional literals
- Predict what assigning a struct, comparing two and using one as a map key do
- Use anonymous structs for one-off values, and field tags for encoding/json
- Set promoted fields directly in a struct literal, as Go 1.27 allows
- Predict how the order of the fields changes the size of a struct
Before you start
On this page
A booking has a PNR, a passenger, a coach and a berth: four values that travel together. Kept in four variables, they
need four parameters in every function that handles a booking, in the right order every time; a struct makes them one
value, Booking{PNR: "4521367890", Berth: 34}, with a name for each part.
You will define struct types, build values with keyed and positional literals, compare structs and use them as map keys, write one-off structs and field tags, set promoted fields the way Go 1.27 allows, and see why the order of the fields can double the size of a struct. All the programs were run with Go 1.27.1, and their real output follows each one.
A struct type names its fields, and a value holds all of them
type Booking struct { … } declares a struct: A type made of named fields, each with its own type, declared as struct { … }. A struct value holds all its fields together, so assigning or passing it copies every field. type with four fields, each with a name and a
type. A value of the type holds all four, and b.Berth reads or writes one of them:
// booking.go: a struct type, its zero value, a keyed literal and a copy.
package main
import "fmt"
type Booking struct {
PNR string
Passenger string
Coach string
Berth int
}
func main() {
var empty Booking // every field at its zero value
fmt.Printf("%+v\n", empty)
b := Booking{PNR: "4521367890", Passenger: "Asha", Coach: "B2", Berth: 34}
fmt.Println(b.Passenger, b.Coach, b.Berth)
moved := b // copies every field
moved.Berth = 36
fmt.Println(b.Berth, moved.Berth)
} Output
{PNR: Passenger: Coach: Berth:0}
Asha B2 34
34 36
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run booking.go
The first line shows the zero value: The value a variable holds when it is given none: 0 for numbers, false for booleans, the empty string for strings, nil for pointers, slices, maps, channels, functions and interfaces, and zeroed fields or elements for structs and arrays. More in “Variables, short declarations and zero values” of a struct: every field at the zero value of its own
type, three empty strings and a 0, which %+v prints with the field names. The literal on the next line names the
fields it sets. The last line holds the surprise for anyone who knows Java or Python: moved := b made a second
Booking, so changing moved.Berth left b.Berth at 34. Step through the run:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A zero Booking. var empty Booking gives every field its zero value, the empty string for the three strings and 0 for Berth. %+v prints the field names with nothing after the three strings.
- A keyed literal. The literal names each field it sets, so b holds the PNR 4521367890, Asha, coach B2 and berth 34. The program prints Asha B2 34.
- Assignment copies the fields. moved := b declares a second Booking and copies all four fields of b into it. From now on the two values share nothing.
- The copy changes. moved.Berth = 36 writes the Berth field of moved, and only of moved.
- The original stays. b.Berth is still 34, so the last line prints 34 36. A struct is its fields, with no reference behind it that two names could share.
A struct value is its fields, stored together, not a reference to an object kept somewhere else. Assigning a struct, passing it to a function and returning it all copy every field, as they do for arrays. Code that must share one Booking shares a pointer to it, which the next lesson covers.
Expecting an assignment to share a struct
After moved := b, the line moved.Berth = 36 changes only moved. In Java or Python the two names would point
at one object; in Go they are two values. Change b.Berth itself, or give the code that must change it a pointer,
&b.
Field names follow the rule of every Go name: a field whose name starts with a capital letter, such as Berth, can be
used by other packages, and one that starts with a lower-case letter only inside its own package.
Checkpoint
Check: A struct value and its copies
Keyed literals name the fields; positional literals list every one
A struct literal can name the fields it sets or list values in order. The first two literals below build the same Seat:
// literals.go: keyed and positional literals of one struct type.
package main
import "fmt"
type Seat struct {
Coach string
Berth int
Upper bool
}
func main() {
keyed := Seat{Berth: 34, Coach: "B2"} // any order; Upper is left at false
positional := Seat{"B2", 34, false} // every field, in declaration order
fmt.Println(keyed == positional)
fmt.Printf("%+v\n", keyed)
fmt.Printf("%+v\n", Seat{Coach: "S1"})
fmt.Printf("%+v\n", Seat{})
} Output
true
{Coach:B2 Berth:34 Upper:false}
{Coach:S1 Berth:0 Upper:false}
{Coach: Berth:0 Upper:false}
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run literals.go
A keyed literal, Seat{Berth: 34, Coach: "B2"}, may give its fields in any order and leave any of them out: Upper
was left at false, and Seat{Coach: "S1"} set one field of three. A positional literal, Seat{"B2", 34, false},
gives a value for every field, in the order the type declares them, and Seat{} is the zero value. The compiler holds
positional literals to that rule, and refuses a literal that mixes the two forms:
// too_few.go: a positional literal must list every field, and a literal cannot mix the two forms.
package main
import "fmt"
type Seat struct {
Coach string
Berth int
Upper bool
}
func main() {
a := Seat{"B2", 34}
b := Seat{Coach: "B2", 34, false}
fmt.Println(a, b)
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./too_few.go:13:20: too few values in struct literal of type Seat ./too_few.go:14:25: mixture of field:value and value elements in struct literal
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run too_few.go
too few values in struct literal of type Seat points at the literal that forgot Upper, and
mixture of field:value and value elements at the one that names only its first field. Writing & before a literal,
&Seat{Coach: "B2"}, builds a new Seat and gives its address, as the next lesson shows. The two forms side by side:
| Aspect | Keyed: Seat{Coach: "B2", Berth: 34} |
Positional: Seat{"B2", 34, false} |
|---|---|---|
| Fields | any order; the ones left out are zero | every field, in declaration order |
| A field added to the type later | still compiles | fails until every literal is updated |
| A reader sees | which value goes where | the values only |
A type of your own package changes together with your literals, but an imported type can gain a field in its next version. go vet’s composites check reports exactly that case, a literal without keys of a struct type from another package. This small module has one of each kind:
vetmod/main.go · vetmod/main.go
// The main package of vetmod: two literals without keys, one of an imported type.
package main
import (
"fmt"
"example.com/booking/rail"
)
type local struct {
coach string
berth int
}
func main() {
imported := rail.Seat{"B2", 34} // an imported struct type, without keys
own := local{"B2", 34} // a type of this package, without keys
keyed := rail.Seat{Coach: "B2", Berth: 34}
fmt.Println(imported == keyed, own)
} vetmod/rail/rail.go · vetmod/rail/rail.go
// Package rail holds a type that other packages import.
package rail
// Seat is one berth in one coach.
type Seat struct {
Coach string
Berth int
} from session import run
# go vet's composites check on a module whose main package uses an imported struct type without keys.
run(
"""
cd vetmod && go vet .
""",
expect_failure=["cd vetmod && go vet ."],
) Output
$ cd vetmod && go vet . main.go:16:14: example.com/booking/rail.Seat struct literal uses unkeyed fields [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vet_session.py
vet reported rail.Seat{"B2", 34} and stayed silent about local{"B2", 34}, the same kind of literal of a type in
the same package. Use keys for imported types, and wherever a reader would otherwise have to count fields.
Checkpoint
Check: Keyed and positional literals
Structs compare field by field, so they can be map keys
Two struct values are equal when all their fields are equal, so a struct whose fields are all comparable is itself a comparable type: A type whose values can be compared with == and !=: numbers, strings, booleans, pointers, channels, interfaces, and arrays and structs made of comparable types. Map keys need one; slices, maps and functions are not comparable. More in “Arrays are values: length, copies, comparison”. That makes it usable as a map key, here a Seat that finds the passenger in it:
// seats.go: structs compared with == and used as map keys.
package main
import "fmt"
type Seat struct {
Coach string
Berth int
}
func main() {
a := Seat{"B2", 34}
b := Seat{Coach: "B2", Berth: 34}
fmt.Println(a == b, a == Seat{"B2", 35})
taken := map[Seat]string{
{"B2", 34}: "Asha",
{"B2", 36}: "Ravi",
}
fmt.Println(taken[Seat{"B2", 36}], len(taken))
name, ok := taken[Seat{"A1", 7}]
fmt.Printf("%q %v\n", name, ok)
taken[b] = "Meera" // b equals the key {B2 34}, so that entry is replaced
fmt.Println(taken[a], len(taken))
} Output
true false Ravi 2 "" false Meera 2
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run seats.go
a and b were written differently, one positional and one keyed, and compare equal, because equality looks at the
fields and not at the literal. Follow the map’s lookups key by key:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Two entries. The map literal stores two entries, each under a Seat key. The keys can be written without the type name, because the map's type already says they are Seats.
- An equal key finds Ravi. taken[Seat{"B2", 36}] builds a new Seat. It is equal to the key {B2 36}, coach and berth alike, so the lookup returns Ravi.
- No equal key. {A1 7} equals neither key, so the lookup returns the zero value of string, and the two-value form sets ok to false: the program prints "" false.
- Writing through an equal key. b was written with keys, Seat{Coach: "B2", Berth: 34}, and b == {B2 34}. So taken[b] = "Meera" replaces Asha, and the map still holds 2 entries.
The lookup with a new Seat{"B2", 36} found Ravi, and taken[b] = "Meera" replaced Asha’s entry instead of adding a
third. A struct key suits anything that several values identify together: a seat by its coach and berth, a train run
by its number and date. A single field that is not comparable, such as a slice, rules out both uses:
// not_comparable.go: a struct with a slice field cannot be compared or used as a map key.
package main
import "fmt"
type Group struct {
PNR string
Names []string
}
func main() {
x := Group{PNR: "4521367890", Names: []string{"Asha"}}
y := x
fmt.Println(x == y)
byGroup := map[Group]int{}
fmt.Println(len(byGroup))
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./not_comparable.go:14:14: invalid operation: x == y (struct containing []string cannot be compared) ./not_comparable.go:15:17: invalid map key type Group
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run not_comparable.go
The compiler refused x == y because the struct contains []string, and refused the map type with
invalid map key type Group. Compare such structs with a function of your own, and key a map by their comparable
fields, here the PNR.
Key idea
Struct values are equal when all their fields are equal, however their literals were written, so a struct of comparable fields works as a map key made of several values.
Checkpoint
Check: Comparing structs and struct map keys
A struct type needs no name when it is used once
A struct type can be written where it is used, without a type declaration. Such an
anonymous struct: A struct type written where it is used, such as struct { in, want string }, without a type declaration. It suits a value needed in one place, like the cases of a table test. suits a value needed in one place, and above all a table of
cases, the shape of every Go table test:
// table.go: anonymous struct types for a table of cases and for a value built once.
package main
import (
"fmt"
"strings"
)
func main() {
cases := []struct {
in, want string
}{
{"b2", "B2"},
{" s1 ", "S1"},
{"a1\n", "A1"},
}
for _, c := range cases {
got := strings.ToUpper(strings.TrimSpace(c.in))
fmt.Printf("%q -> %q, as wanted: %v\n", c.in, got, got == c.want)
}
summary := struct {
Coach string
Free int
}{Coach: "B2", Free: 8}
fmt.Printf("%+v\n", summary)
} Output
"b2" -> "B2", as wanted: true
" s1 " -> "S1", as wanted: true
"a1\n" -> "A1", as wanted: true
{Coach:B2 Free:8}
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run table.go
cases is a slice of a struct type with two fields, in and want, written once in its declaration, and each case
is a positional literal of that type. The loop checked three inputs against what they should become, and printed true
for each. summary is a single value whose shape matters only to the line that prints it, so naming its type would
add a declaration that nothing else uses. Give the type a name as soon as a second function needs it, because a
function’s parameter list is no place to repeat a struct’s fields.
Tags tell encoding/json what to call a field
A field tag: A string literal after a struct field's type, by convention key:"value" pairs such as json:"pnr,omitempty". The language ignores it; packages such as encoding/json read it through reflection. is a string after a field’s type, by convention a list of key:"value"
pairs. The compiler does not interpret it; packages read it through package reflect, and encoding/json reads the
json key:
// tags.go: field tags that encoding/json reads, and one it cannot read.
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type Booking struct {
PNR string `json:"pnr"`
Passenger string `json:"passenger"`
Berth int `json:"berth,omitempty"`
note string // unexported: encoding/json never sees it
}
type Typo struct {
PNR string `json: "pnr"` // a space after the colon
}
func main() {
out, err := json.Marshal(Booking{PNR: "4521367890", Passenger: "Asha", note: "window"})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(string(out))
field, _ := reflect.TypeFor[Booking]().FieldByName("Berth")
fmt.Printf("%q\n", field.Tag.Get("json"))
typo, err := json.Marshal(Typo{PNR: "4521367890"})
fmt.Println(string(typo), err)
} Output
{"pnr":"4521367890","passenger":"Asha"}
"berth,omitempty"
{"PNR":"4521367890"} <nil>
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run tags.go
| Field | Tag | In the JSON |
|---|---|---|
PNR |
json:"pnr" |
"pnr":"4521367890" |
Passenger |
json:"passenger" |
"passenger":"Asha" |
Berth |
json:"berth,omitempty" |
left out, because it holds 0 |
note |
none, and unexported | never written |
omitempty leaves a field out when its value is empty, which for a number means 0. encoding/json writes exported
fields only, so note stayed out whatever it held. The second line read the tag the way encoding/json does:
Tag.Get("json") returned berth,omitempty, the name followed by its options. The last line is the trap. The tag of
Typo has a space after the colon, Get cannot parse it, and encoding/json fell back to the field name, PNR,
without an error. go vet’s structtag check finds it:
from session import run
# go vet's structtag check on tags.go, whose Typo type has a space after the colon of its json tag.
run(
"""
go vet tags.go
""",
expect_failure=["go vet tags.go"],
) Output
$ go vet tags.go tags.go:18:2: struct field tag `json: "pnr"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tag_vet_session.py
Checkpoint
Check: Anonymous structs · Field tags and encoding/json
Go 1.27: a literal can set a promoted field by name
A field declared with a type and no name, such as Audit in Order below, is an
embedded field: A struct field declared with a type and no name, such as Audit in struct { Audit; ID string }. The type's name serves as the field's name, and its fields and methods are promoted to the outer struct. More in “Embedding: composition instead of inheritance”, and its fields are promoted field: A field or method of an embedded field that can be used as if the outer struct declared it, such as o.CreatedBy for o.Audit.CreatedBy, unless a name at a shallower depth hides it. More in “Embedding: composition instead of inheritance”:
o.CreatedBy reads o.Audit.CreatedBy. Up to Go 1.26 a literal still had to spell the embedded struct out. Go 1.27
accepts the promoted name as a key:
// promoted.go: from Go 1.27 a struct literal may set a promoted field by its own name.
package main
import "fmt"
type Audit struct {
CreatedBy string
Version int
}
type Order struct {
Audit // an embedded field: its fields are promoted to Order
ID string
Total int
}
func main() {
nested := Order{Audit: Audit{CreatedBy: "asha", Version: 2}, ID: "A-1", Total: 499}
direct := Order{ID: "A-1", CreatedBy: "asha", Version: 2, Total: 499} // Go 1.27
fmt.Printf("%+v\n", direct)
fmt.Println(nested == direct, direct.CreatedBy, direct.Audit.Version)
} Output
{Audit:{CreatedBy:asha Version:2} ID:A-1 Total:499}
true asha 2
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run promoted.go
Both literals built the same Order, so nested == direct printed true, and %+v shows where the values went: inside
Audit. Go 1.27 widened what a key can be, from the names of the type’s own fields to every field its selectors
reach. The specification sets two limits, which this figure walks through on two types:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Order embeds Audit. Audit is a field of Order named after its type. Its fields are promoted, so o.CreatedBy is a shorter way to write o.Audit.CreatedBy.
- A promoted key, accepted. Order{ID: "A-1", CreatedBy: "asha"} compiles in Go 1.27. The path from Order to CreatedBy passes through Audit, a struct value, so the literal can set the field inside it.
- Two keys for one field. A literal that also sets Audit: Audit{Version: 1} would set CreatedBy twice, once inside that value and once by its own key. The compiler refuses the pair.
- Draft holds a pointer. Draft embeds *Audit. In a new Draft that pointer is nil, so there is no Audit for a key to write into.
- Through a pointer, refused. Draft{CreatedBy: "ravi"} would have to follow the pointer, and the compiler reports invalid implicit pointer indirection to reach CreatedBy. Write Audit: &Audit{CreatedBy: "ravi"} instead.
// promoted_rules.go: the two limits of promoted-field keys.
package main
import "fmt"
type Audit struct {
CreatedBy string
Version int
}
type Order struct {
Audit
ID string
}
type Draft struct {
*Audit // embedded through a pointer
ID string
}
func main() {
o := Order{Audit: Audit{Version: 1}, CreatedBy: "asha"}
d := Draft{ID: "D-7", CreatedBy: "ravi"}
fmt.Println(o, d)
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./promoted_rules.go:22:39: cannot specify promoted field CreatedBy and enclosing embedded field Audit ./promoted_rules.go:23:24: invalid implicit pointer indirection to reach CreatedBy
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run promoted_rules.go
cannot specify promoted field CreatedBy and enclosing embedded field Audit is the first limit: one literal may not
set the embedded struct and a field inside it. invalid implicit pointer indirection to reach CreatedBy is the
second: Draft embeds *Audit, and a key cannot follow a pointer. The feature also depends on the language version of
the file, not on the toolchain. Built as Go 1.26, with a //go:build go1.26 line, the literal of promoted.go fails:
//go:build go1.26
// promoted_old.go: the literal of promoted.go, built as Go 1.26.
package main
import "fmt"
type Audit struct {
CreatedBy string
Version int
}
type Order struct {
Audit
ID string
Total int
}
func main() {
direct := Order{ID: "A-1", CreatedBy: "asha", Version: 2, Total: 499}
fmt.Printf("%+v\n", direct)
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./promoted_old.go:20:29: use of promoted field Audit.CreatedBy in struct literal of type Order requires go1.27 or later (file declares //go:build go1.26) ./promoted_old.go:20:48: use of promoted field Audit.Version in struct literal of type Order requires go1.27 or later (file declares //go:build go1.26)
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run promoted_old.go
Go 1.27
Promoted fields as keys need code built as Go 1.27 or later, which the go line of the module’s go.mod decides. go fix gained an analyzer for them, embedlit, which rewrites the nested literals of older code.
Here embedlit runs on a module at go 1.27 whose literals spell Audit out, with -diff so that it only shows its
changes:
// The main package of fixmod: literals written before Go 1.27, with the embedded struct spelled out.
package main
import "fmt"
type Audit struct {
CreatedBy string
Version int
}
type Order struct {
Audit
ID string
Total int
}
func main() {
o := Order{Audit: Audit{CreatedBy: "asha", Version: 2}, ID: "A-1", Total: 499}
p := Order{Audit: Audit{CreatedBy: "ravi"}, ID: "A-2"}
fmt.Println(o.CreatedBy, p.CreatedBy)
} from session import run
# go fix's embedlit analyzer, shown as a diff (-diff changes no file). The sed only shortens the file names in the
# diff's header to main.go.
run(
"""
cd fixmod && go run .
cd fixmod && go fix -embedlit -diff . | sed 's#[^ ]*/main[.]go#main.go#'
""",
) Output
$ cd fixmod && go run .
asha ravi
$ cd fixmod && go fix -embedlit -diff . | sed 's#[^ ]*/main[.]go#main.go#'
--- main.go (old)
+++ main.go (new)
@@ -15,7 +15,7 @@
}
func main() {
- o := Order{Audit: Audit{CreatedBy: "asha", Version: 2}, ID: "A-1", Total: 499}
- p := Order{Audit: Audit{CreatedBy: "ravi"}, ID: "A-2"}
+ o := Order{CreatedBy: "asha", Version: 2, ID: "A-1", Total: 499}
+ p := Order{CreatedBy: "ravi", ID: "A-2"}
fmt.Println(o.CreatedBy, p.CreatedBy)
}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fix_session.py
The program printed asha ravi, and the diff turned both literals into the short form without changing what they
build. Review such a diff like any other change: a reader who has not met embedding may find
Order{CreatedBy: "ravi"} harder to follow than the nested form.
Checkpoint
Check: Promoted fields as keys (Go 1.27)
The order of the fields decides how much padding a struct carries
The two types in this program hold the same five fields in different orders, and one takes twice the memory of the other:
// layout.go: the same five fields in two orders, and the bytes each order takes.
package main
import (
"fmt"
"unsafe"
)
type Loose struct {
Paid bool
Fare int64 // in paise
Veg bool
Berth int32
Tier byte
}
type Tight struct {
Fare int64
Berth int32
Paid bool
Veg bool
Tier byte
}
func main() {
var l Loose
var t Tight
fmt.Println("Loose:", unsafe.Sizeof(l), "bytes, Tight:", unsafe.Sizeof(t), "bytes")
fmt.Println("Loose offsets:", unsafe.Offsetof(l.Paid), unsafe.Offsetof(l.Fare),
unsafe.Offsetof(l.Veg), unsafe.Offsetof(l.Berth), unsafe.Offsetof(l.Tier))
fmt.Println("Tight offsets:", unsafe.Offsetof(t.Fare), unsafe.Offsetof(t.Berth),
unsafe.Offsetof(t.Paid), unsafe.Offsetof(t.Veg), unsafe.Offsetof(t.Tier))
const n = 1_000_000
fmt.Println("a million of each:", n*unsafe.Sizeof(l), "and", n*unsafe.Sizeof(t), "bytes")
} Output
Loose: 32 bytes, Tight: 16 bytes Loose offsets: 0 8 16 20 24 Tight offsets: 0 8 12 13 14 a million of each: 32000000 and 16000000 bytes
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run layout.go
unsafe.Sizeof gave 32 bytes for Loose and 16 for Tight, and unsafe.Offsetof shows where each field starts. Look
at the gaps in the drawing of those offsets:
The same five fields take 32 or 16 bytes
Text description of the diagram
Memory is drawn as rows of 8 bytes, one box per byte, with the offset of each row's first byte on its left. Dashed boxes are padding, bytes that hold no field.
Loose, 32 bytes in 4 rows:
- Offset 0: Paid, 1 byte, then 7 bytes of padding, because Fare must start at a multiple of 8.
- Offset 8: Fare, 8 bytes, the whole row.
- Offset 16: Veg, 1 byte, then 3 bytes of padding, then Berth, 4 bytes, at offset 20, a multiple of 4.
- Offset 24: Tier, 1 byte, then 7 bytes of padding, which round the size up to 32, a multiple of 8.
Tight, 16 bytes in 2 rows:
- Offset 0: Fare, 8 bytes.
- Offset 8: Berth, 4 bytes, then Paid, Veg and Tier, 1 byte each, at offsets 12, 13 and 14, then 1 byte of padding.
The note under the rows says that Loose carries 17 bytes of padding and Tight 1, and that a million values take 32 MB in the loose order and 16 MB in the tight one.
On this 64-bit machine an int64 starts at a multiple of 8 and an int32 at a multiple of 4. In Loose, Fare
follows the 1-byte Paid, so the compiler leaves 7 unused bytes, padding: Unused bytes the compiler leaves between struct fields, or after the last one, so that each field starts at a multiple of its alignment, such as an int64 at a multiple of 8., to reach offset
8, and the size is rounded up to a multiple of 8 at the end. The specification gives the rule in outline: a struct’s
alignment is the largest alignment of its fields. Tight puts the 8-byte field first and the 1-byte fields together,
and needs 1 byte of padding instead of 17.
Order fields for readers first
Group fields the way a reader thinks about them, and reorder for size only in a type with millions of values, where 32 MB against 16 MB matters. The fieldalignment analyzer of golang.org/x/tools lists structs that would be smaller with their fields sorted; go vet does not run it.
Checkpoint
Check: Field order and padding
Practise
Seven ideas, from a struct’s copy to its padding, one question at a time: practice picks your weakest idea first, and each choice tells you why it is right or what it misses.
Practice that adapts to you
Practise this lesson
14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is the difference between a keyed and a positional struct literal?
A keyed literal names the fields it sets, Seat{Coach: "B2", Berth: 34}, in any order, and every field it leaves out
gets its zero value. A positional literal, Seat{"B2", 34, false}, must give a value for every field, in the order
the type declares them, so it stops compiling when someone adds a field. Prefer keys for any struct of another
package; go vet’s composites check reports positional literals of imported types.
Three longer questions of the module bank build on this lesson:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
Your team upgrades a shared package, and three services stop compiling with too few values in struct literal of type rail.Seat. The package only added a field to Seat. Explain why, how you fix the services, and how you keep it from happening again.
-
Mid to senior
A booking service must spot duplicate bookings: the same passenger, train, travel day and berth. A teammate keys a map by one string joined from the four values with a | between them. Would you use a struct key instead? Design the key and say what can go wrong with each choice.
-
Mid to senior
A service keeps 20 million fare records in memory, each a struct of a bool, an int64, a bool, an int32 and a byte, in that order. Memory use is higher than the fields suggest. How do you measure what the struct costs, what do you change, and what do you check before changing it?
Key takeaways
- A struct value holds all its fields, starts with each at its zero value, and is copied whole by assignment, by a call and by a return.
- A keyed literal names the fields it sets and leaves the rest at zero; a positional literal lists every field in order and breaks when the type gains one, which go vet reports for imported types.
- Structs whose fields are all comparable compare field by field and can be map keys; one slice field rules out both.
- An anonymous struct suits a value used in one place, such as a table of test cases, and a field tag such as
json:"pnr,omitempty"tells encoding/json what to call a field and when to leave it out. - Go 1.27 lets a literal set a promoted field by its own name, except through an embedded pointer or next to a key for the embedded struct itself.
- Field order sets the padding: the same five fields took 32 bytes in one order and 16 in another.
Terms in this lesson
- anonymous struct
- A struct type written where it is used, such as struct { in, want string }, without a type declaration. It suits a value needed in one place, like the cases of a table test.
- field tag
- A string literal after a struct field's type, by convention key:"value" pairs such as json:"pnr,omitempty". The language ignores it; packages such as encoding/json read it through reflection.
- padding
- Unused bytes the compiler leaves between struct fields, or after the last one, so that each field starts at a multiple of its alignment, such as an int64 at a multiple of 8.
- struct
- A type made of named fields, each with its own type, declared as struct { … }. A struct value holds all its fields together, so assigning or passing it copies every field.
References
- The Go Programming Language Specification: Struct types (The Go Authors)
- The Go Programming Language Specification: Composite literals (The Go Authors)
- The Go Programming Language Specification: Comparison operators (The Go Authors)
- The Go Programming Language Specification: Size and alignment guarantees (The Go Authors)
- Go 1.27 Release Notes: Changes to the language (The Go Authors)
- Command vet: composites and structtag (The Go Authors)
- Command go: Apply fixes suggested by static checkers (go fix) (The Go Authors)
- Package encoding/json: Marshal (The Go Authors)
- Package reflect: StructTag (The Go Authors)
- Package unsafe: Sizeof, Alignof and Offsetof (The Go Authors)
- Package fieldalignment (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress