Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 15 minutes
  • Examples run with Go 1.27.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

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 zero value, a literal and a copy Go · booking.go
// 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:

Interactive · step by step A struct value and its copy Step through booking.go and watch which Booking each line changes.
A struct value and its copy, step 5 of 5. Showing empty, b, moved (Asha, B2, berth 36). In focus: b, moved.moved := bemptyevery field at zerobAsha, B2, berth 34movedAsha, B2, berth 36

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. The copy changes. moved.Berth = 36 writes the Berth field of moved, and only of moved.
  5. 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

  1. Question 1 of 2 Booking has the fields PNR string and Berth int. What does this print?

    Read the code, then choose one answer.

    a := Booking{PNR: "1", Berth: 10}
    b := a
    b.Berth = 20
    fmt.Println(a.Berth, b.Berth)
    Show the answer: Question 1 of 2

    Answer: 10 20

    Assignment copies every field of a struct, as moved := b did in booking.go, which printed 34 36.

  2. Question 2 of 2 What does var empty Booking hold, for the Booking of booking.go?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Three empty strings and a Berth of 0

    A struct's zero value is every field at its own zero value, so var empty Booking is ready to use.

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 Go · literals.go
// 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 value short, and both forms in one literal Go · too_few.go
// 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:

A main package that builds an imported rail.Seat and a local type without keys

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
}
go vet on the module Python · vet_session.py
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

  1. Question 1 of 2 Seat has the fields Coach string, Berth int and Upper bool, in that order. What does this print?

    Read the code, then choose one answer.

    s := Seat{Berth: 7}
    fmt.Printf("%+v\n", s)
    Show the answer: Question 1 of 2

    Answer: {Coach: Berth:7 Upper:false}

    Fields a keyed literal leaves out get their zero values, as Seat{Coach: "S1"} did in literals.go.

  2. Question 2 of 2 A package you import adds a fourth field to its struct type Seat. Which of your literals stops compiling?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: rail.Seat{"B2", 34, false}, the positional one

    This is the case go vet's composites check reports: a literal without keys of a struct type from another package.

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: == and a map with Seat keys Go · seats.go
// 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:

Interactive · step by step A map with struct keys Step through the map of seats.go; every lookup compares its Seat with the keys, field by field.
A map with struct keys, step 4 of 4. Showing {B2 34}, Asha (Meera), {B2 36}, Ravi. In focus: {B2 34}, Asha.{B2 34}a Seat keyAshaMeera{B2 36}a Seat keyRaviits value

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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 Go · not_comparable.go
// 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

  1. Question 1 of 2 Seat has the fields Coach string and Berth int. What does this print?

    Read the code, then choose one answer.

    m := map[Seat]int{{"B2", 1}: 5}
    m[Seat{Berth: 1, Coach: "B2"}]++
    fmt.Println(m, len(m))
    Show the answer: Question 1 of 2

    Answer: map[{B2 1}:6] 1

    Struct keys compare field by field, however the literal was written, as taken[b] = "Meera" did in seats.go.

  2. Question 2 of 2 Why does the compiler refuse map[Group]int, where Group has a field Names []string?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A slice is not comparable, so neither is a struct that contains one

    not_comparable.go stopped with invalid map key type Group; key the map by a comparable field such as the PNR.

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: a table of cases and a value built once Go · table.go
// 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: a Booking as JSON, and a tag with a typo Go · tags.go
// 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:

go vet on tags.go Python · tag_vet_session.py
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

JSON to Go Struct Paste a JSON document and get Go structs with json tags and omitempty, then check them against the rules above.

Checkpoint

Check: Anonymous structs · Field tags and encoding/json

  1. Question 1 of 3 Where does an anonymous struct type fit best?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: The cases of a table test, written and used in one function

    table.go used one for its cases and one for a value printed once.

  2. Question 2 of 3 What does json.Marshal produce for Bag{Name: "trunk", label: "x"}, with Kg and Fee left at 0?

    Read the code, then choose one answer.

    type Bag struct {
    	Name  string `json:"name"`
    	Kg    int    `json:"kg,omitempty"`
    	Fee   int    `json:"fee"`
    	label string
    }
    Show the answer: Question 2 of 3

    Answer: {"name":"trunk","fee":0}

    The program printed {"name":"trunk","fee":0}: tags give the names, omitempty drops empty values, and only exported fields are written.

  3. Question 3 of 3 A field is tagged json: "pnr", with a space after the colon. What does encoding/json do with it?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: It cannot read the tag and writes the field under its Go name, without an error

    The structtag check of go vet reported the tag as not compatible with reflect.StructTag.Get.

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: two literals of one Order Go · promoted.go
// 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:

Interactive · step by step Which promoted fields a literal may set Step through two types that embed Audit, one as a value and one through a pointer, and the keys Go 1.27 accepts.
Which promoted fields a literal may set, step 5 of 5. Showing Order, Audit, embedded, CreatedBy, Version (CreatedBy set to asha), Draft, *Audit, embedded, CreatedBy, Version. In focus: *Audit, embedded, p-f.embedsholdsembedspoints toOrderAudit, ID, TotalAudit, embeddeda value inside OrderCreatedBy, VersionCreatedBy set to ashaDraft*Audit, ID*Audit, embeddeda pointer, nil at firstCreatedBy, Versionbehind the pointerrefused: a key cannot follow a pointer

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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: two literals the rules refuse Go · promoted_rules.go
// 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:

promoted_old.go: the same literal, built as Go 1.26 Go · promoted_old.go
//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:

fixmod/main.go: two Order literals in the older form Go · fixmod/main.go
// 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)
}
go fix -embedlit on fixmod Python · fix_session.py
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)

  1. Question 1 of 2 Order embeds Audit, which has the fields CreatedBy and Version. Which literal compiles in a module at go 1.27?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Order{ID: "A-1", CreatedBy: "asha"}

    promoted.go built Order{ID: "A-1", CreatedBy: "asha", Version: 2, Total: 499} and printed it with the values inside Audit.

  2. Question 2 of 2 Why is Draft{CreatedBy: "ravi"} refused when Draft embeds *Audit?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The key would have to follow a pointer, which is nil in a new Draft

    Set the embedded pointer itself instead: Draft{ID: "D-7", Audit: &Audit{CreatedBy: "ravi"}}.

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: five fields in two orders Go · layout.go
// 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 fields of Loose and Tight drawn byte by byte: Loose takes 32 bytes with 17 of padding, Tight 16 bytes with 1.One record, two field ordersOne box per byte; a row is 8 bytesLoose: 32 bytes0Paid7 bytes of padding8Fare, 8 bytes16Veg3 pad bytesBerth, 424Tier7 bytes of paddingTight: 16 bytes0Fare, 8 bytes8Berth, 4PaidVegTierpadDashed boxes are padding: 17 bytes in Loose,1 in Tight. A million values: 32 MB or 16 MB.

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:

  1. Offset 0: Paid, 1 byte, then 7 bytes of padding, because Fare must start at a multiple of 8.
  2. Offset 8: Fare, 8 bytes, the whole row.
  3. Offset 16: Veg, 1 byte, then 3 bytes of padding, then Berth, 4 bytes, at offset 20, a multiple of 4.
  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:

  1. Offset 0: Fare, 8 bytes.
  2. 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

  1. Question 1 of 2 How many bytes does unsafe.Sizeof report for struct { A bool; B int64; C bool } on the machine of layout.go?

    Type a number.

    A hint

    B must start at a multiple of 8, and the size is rounded up to a multiple of 8.

    Show the answer: Question 1 of 2

    Answer: 24 bytes

    A at 0, 7 bytes of padding, B at 8 to 15, C at 16, then 7 bytes of padding to reach 24, a multiple of 8. The order { B; A; C } needs 16.

  2. Question 2 of 2 When is reordering a struct's fields for size worth doing?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: When the program keeps millions of values of the type, and measuring shows the memory matters

    Order fields for readers first, and let measurement and tools such as fieldalignment point at the types worth reordering.

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.

  1. Question 1 of 14 What does this print?

    Read the code, then choose one answer.

    func shift(b Booking) { b.Berth++ }
    
    a := Booking{PNR: "1", Berth: 10}
    shift(a)
    fmt.Println(a.Berth)
    Show the answer: Question 1 of 14

    Answer: 10

    Passing a struct copies every field, as assignment does; to change the caller's value, pass &a to a *Booking parameter.

  2. Question 2 of 14 bs is []Booking{{"1", 1}, {"2", 2}}. What does fmt.Println(bs) print after this loop?

    Read the code, then choose one answer.

    for _, x := range bs {
    	x.Berth += 10
    }
    Show the answer: Question 2 of 14

    Answer: [{1 1} {2 2}]

    The range value is a copy of each element, a struct like any other; index the slice to change its elements.

  3. Question 3 of 14 What must a positional literal such as Seat{"B2", 34, false} give?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: A value for every field, in the order the type declares them

    A positional literal lists every field by position, so it changes whenever the type gains or loses a field.

  4. Question 4 of 14 What does the compiler say about Seat{Coach: "B2", 34, false}?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: mixture of field:value and value elements in struct literal

    too_few.go reported the mixture for exactly this literal.

  5. Question 5 of 14 A program must count tickets per train and date. Which key type fits best?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: A struct { Train string; Date string } used as the map key

    A struct of comparable fields is a key made of several values, as Seat was in seats.go.

  6. Question 6 of 14 A map is keyed by *Seat. Two different variables both hold Seat{"B2", 34}. How many entries do m[&s1] = 1 and m[&s2] = 2 make?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: Two, because pointers compare by address and &s1 differs from &s2

    Key by the struct value when equal values must find the same entry.

  7. Question 7 of 14 What is an anonymous struct?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: A struct type written where it is used, without a type declaration

    An anonymous struct type has fields but no type name, which suits a value used in one place.

  8. Question 8 of 14 What does this print?

    Read the code, then choose one answer.

    var z struct{ A, B int }
    fmt.Printf("%+v\n", z)
    Show the answer: Question 8 of 14

    Answer: {A:0 B:0}

    Anonymous or named, a struct starts with each field at its zero value.

  9. Question 9 of 14 What does the tag json:"pnr" on the field PNR change?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: The name encoding/json uses for the field, pnr instead of PNR

    A tag is a note for the packages that read fields, such as encoding/json.

  10. Question 10 of 14 A Berth tagged json:"berth,omitempty" should appear in the JSON when a booking really has berth 0. What is wrong?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: omitempty drops every 0, so a real berth 0 and an unset one look the same

    Drop omitempty when 0 is a meaningful value, or make the field a pointer, where nil means not set; the next lesson covers pointers.

  11. Question 11 of 14 In a module at go 1.27, with Order and Audit as in promoted.go, what does this print?

    Read the code, then choose one answer.

    o := Order{ID: "A-2", Version: 3}
    fmt.Println(o.Audit.Version, o.CreatedBy == "")
    Show the answer: Question 11 of 14

    Answer: 3 true

    A promoted key writes into the embedded struct, as %+v of direct showed {Audit:{CreatedBy:asha Version:2} …}.

  12. Question 12 of 14 Your module's go.mod says go 1.26, and a teammate builds it with Go 1.27.1. Does Order{CreatedBy: "asha"} compile?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: No; the go line decides the language version, and promoted keys need 1.27

    Raise the go line to 1.27 to use the new keys, then run go fix -embedlit to shorten older literals.

  13. Question 13 of 14 How many bytes does struct { A bool; B int32; C bool } take on the machine of layout.go?

    Type a number.

    A hint

    B must start at a multiple of 4, and the size is rounded up to a multiple of 4.

    Show the answer: Question 13 of 14

    Answer: 12 bytes

    A at 0, 3 bytes of padding, B at 4 to 7, C at 8, and 3 more bytes to reach 12. The order { B; A; C } takes 8.

  14. Question 14 of 14 A cache holds 10 million values of a 32-byte struct that would take 16 bytes sorted. What does reordering save?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: About 160 MB

    At this size the saving is worth a reorder; for a type with a few values it would not be.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.