Skip to content

Structs

A struct groups related fields under a single named type. Each field has a name and a type:

type Point struct {
X, Y int
}
type User struct {
Name string
Email string
Age int
}

Fields whose names begin with an uppercase letter are exported (visible outside the package). Lowercase names are unexported.

A struct tag is a plain string literal attached to a field, written right after its type:

type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age,omitempty"`
}

The compiler does nothing with a tag except store it — it has zero effect on compilation or field access. A tag only becomes meaningful when something reads it via reflection at runtime, which is exactly what encoding/json, most ORMs, and validation libraries do:

t := reflect.TypeOf(User{})
f, _ := t.FieldByName("Name")
fmt.Println(f.Tag.Get("json")) // name
fmt.Println(f.Tag.Get("db")) // user_name

Because a tag is just a string until something parses it, a typo in the key name compiles perfectly and produces no warning — it just silently stops doing what you meant:

type Broken struct {
Name string `jso:"name"` // typo: "jso" instead of "json"
Age int `json:"age"`
}
b, _ := json.Marshal(Broken{Name: "Ada", Age: 30})
fmt.Println(string(b)) // {"Name":"Ada","age":30} — Name fell back to the Go field name

Verified on 1.26.5: this compiles, go vet ./... reports nothing, and it runs and marshals Name under its Go field name because encoding/json never found a tag under the key "json". What go vet’s structtag check does catch is a tag that is not valid reflect.StructTag syntax — for example a missing space between key:"value" pairs:

type Bad struct {
Name string `json:"name",omitempty`
}
main.go:6:2: struct field tag `json:"name",omitempty` not compatible with reflect.StructTag.Get: key:"value" pairs not separated by spaces

So go vet protects you from malformed tag syntax, but not from a tag using the wrong key name for the library that will read it — that mistake is only caught by actually running the marshal/unmarshal path (a test).

You can create a struct value in two ways:

// Positional — order must match the field declaration order
p := Point{3, 4}
// Named fields — order does not matter; unset fields get zero values
u := User{Name: "Alice", Email: "[email protected]"}

Named-field form is strongly preferred: it is self-documenting and resilient to field reordering.

Use a dot to read or write any field:

fmt.Println(u.Name) // read
u.Age = 30 // write

For one-off shapes — configuration, test fixtures, local grouping — you can define a struct type inline without naming it:

cfg := struct {
Host string
Port int
}{Host: "localhost", Port: 8080}

A well-designed struct should work correctly the moment it is declared, with no constructor call — var b bytes.Buffer or var mu sync.Mutex are already ready to use. This is called making the zero value useful, and it is idiomatic Go, not an accident: bytes.Buffer’s zero value is an empty buffer, sync.Mutex’s zero value is an unlocked mutex. Compare that to a type that panics or misbehaves until some Init() or New() runs first — every caller now has to remember an extra step, and forgetting it is a runtime bug instead of a compile error.

When you design your own structs, prefer field types whose zero value already means something sensible: a nil slice that is safe to append to, a sync.Mutex (never a *sync.Mutex that needs allocating), a time.Time whose zero value IsZero() reports true instead of a *time.Time that needs a nil check everywhere. Reserve a New/constructor function for the genuinely necessary cases (required fields, a value that must be validated, or a field like a map that you know will be written to immediately).

struct{} is a real, singleton type with a specific property: it occupies zero bytes.

var s struct{}
fmt.Println(unsafe.Sizeof(s)) // 0

Because it costs nothing to store, struct{} is the idiomatic value type for two patterns: a set (map[K]struct{}, when you only care whether a key is present, not any associated value) and a signal-only channel (chan struct{}, when a send/receive means “this happened,” not “here is a payload”):

set := map[string]struct{}{"a": {}, "b": {}}
_, ok := set["a"]
fmt.Println(len(set), ok) // 2 true
done := make(chan struct{})
close(done)
_, open := <-done
fmt.Println(open) // false — closed and drained, the classic "done" signal

A map[K]bool also works as a set, but map[K]struct{} documents the intent more precisely (there is no meaningful false entry to accidentally check for) and uses less memory at scale.

Two struct values are comparable with == if all their fields are comparable types. The comparison is field-by-field:

a := Point{1, 2}
b := Point{1, 2}
fmt.Println(a == b) // true

Structs containing slices, maps, or functions are not comparable with == — that is a compile error, not a runtime one, so the compiler catches it immediately. Two subtler comparability traps are runtime-only:

NaN never equals itself, even the exact same variable, because floating-point comparison follows IEEE 754, not value identity:

type WithNaN struct{ F float64 }
w := WithNaN{F: math.NaN()}
fmt.Println(w == w) // false — NaN != NaN, even the same variable

A struct field of interface type defers comparability to whatever is stored inside it. The struct type-checks as comparable at compile time (an interface field is always statically comparable), but comparing it can still panic at runtime if the dynamic value it holds turns out to be a slice, map, or function:

var sx, sy any = []int{1}, []int{1}
s3 := struct{ V any }{V: sx}
s4 := struct{ V any }{V: sy}
fmt.Println(s3 == s4) // panics: comparing uncomparable type []int

Verified on 1.26.5:

panic: runtime error: comparing uncomparable type []int

An any field holding an int compares fine; the same field holding a []int panics. The compiler cannot tell which case you are in — it only knows the static type is any, which is always comparable. This is a real risk in any struct with an any/interface{} field that gets compared with == or used as a map key.

A positional (unkeyed) struct literal — Point{3, 4} instead of Point{X: 3, Y: 4} — is valid Go, but it silently breaks if the struct’s author ever reorders or inserts a field, because the values are matched to fields by position, not by name. go vet’s composites check flags unkeyed literals of struct types that come from another package:

r := shapes.Rect{10, 20} // shapes is a different package

Verified on 1.26.5 (go vet ./...):

main.go:10:7: example.com/t09b/shapes.Rect struct literal uses unkeyed fields

There is a catch worth knowing about: this check exempts a hardcoded list of well-known, historically-stable stdlib types — image.Point{1, 2} is idiomatic Go and go vet says nothing about it, even though it is exactly the same pattern. Do not treat “go vet is silent” as proof a literal is safe; treat unkeyed literals from any other package as a habit to avoid, vet exemption or not. Positional literals for a struct declared in the same file you are editing are common and fine — you would notice immediately if you reordered its fields.

Go lays out a struct’s fields in declaration order, but not necessarily back-to-back: each field must start at an address that is a multiple of its own alignment (int64 aligns to 8 bytes, bool to 1), so the compiler inserts invisible padding bytes between fields when needed. Field order can change a struct’s total size:

type Bad struct {
A bool // 1 byte, then 7 bytes of padding to align B
B int64 // 8 bytes
C bool // 1 byte, then 7 bytes of trailing padding (struct size rounds up to 8)
}
type Good struct {
B int64 // 8 bytes
A bool // 1 byte
C bool // 1 byte, then 6 bytes of trailing padding
}
flowchart TD
  subgraph Bad["Bad: 24 bytes"]
    direction LR
    BA["A (1B)"] --- BP1["padding (7B)"] --- BB["B (8B)"] --- BC["C (1B)"] --- BP2["padding (7B)"]
  end
  subgraph Good["Good: 16 bytes"]
    direction LR
    GB["B (8B)"] --- GA["A (1B)"] --- GC["C (1B)"] --- GP["padding (6B)"]
  end
Reordering fields from largest to smallest removes 8 bytes of padding per struct value

Verified on 1.26.5 (unsafe.Sizeof/unsafe.Offsetof):

fmt.Println(unsafe.Sizeof(Bad{}), unsafe.Sizeof(Good{})) // 24 16
fmt.Println(unsafe.Offsetof(Bad{}.B), unsafe.Offsetof(Good{}.B)) // 8 0

Bad and Good have the exact same three fields — only the declaration order differs, and it costs 8 bytes per value. The rule of thumb: order fields from largest to smallest (or group same-sized fields together) to minimize padding. This matters most for structs allocated in bulk — a slice of a million Bad values wastes 8 million bytes for nothing. unsafe.Sizeof and unsafe.Offsetof are the tools to check it; reach for them when a hot-path struct’s size seems larger than the sum of its fields suggests.

Composition through embedding — promoting an embedded type’s fields and methods, and why that is composition rather than inheritance — is covered in depth in Embedding.

package main
import (
"fmt"
"math"
)
type Address struct {
City string
Country string
}
type Person struct {
Name string
Age int
}
type WithNaN struct{ F float64 }
func main() {
// Named-field literal
p1 := Person{Name: "Alice", Age: 30}
fmt.Println(p1.Name, p1.Age)
// Struct copy: p2 is an independent value
p2 := p1
p2.Name = "Bob"
fmt.Println(p1.Name, p2.Name) // Alice Bob
// Anonymous struct
cfg := struct {
Host string
Port int
}{Host: "localhost", Port: 8080}
fmt.Printf("%s:%d\n", cfg.Host, cfg.Port)
// Struct comparison (all fields comparable)
a1 := Address{City: "Bangkok", Country: "Thailand"}
a2 := Address{City: "Bangkok", Country: "Thailand"}
fmt.Println(a1 == a2) // true
// Unkeyed vs keyed literal: same value, but unkeyed breaks silently on reorder
unkeyed := Address{"Bangkok", "Thailand"}
keyed := Address{City: "Bangkok", Country: "Thailand"}
fmt.Println(unkeyed == keyed) // true
// NaN never equals itself, even the same variable
w := WithNaN{F: math.NaN()}
fmt.Println(w == w) // false
}
What you getBenefitCost
value semanticspredictable behavior, no aliasingcopy cost for large structs
zero-value-useful designno constructor call needed, fewer nil checksnot every type can have a meaningful zero value (e.g. one with a required field)
struct tagsdeclarative metadata for JSON/DB/validationruntime-only — no compile-time check that a tag’s key name is correct
field ordered by size (padding-aware)smaller struct, less memory at scaleslightly less readable than “logical” field grouping
exported vs unexported fieldsclear control over the API surfaceexposing a new field later is a breaking change
  • Struct copies are always expensive — a small struct (2-4 fields) copies faster than using a pointer, which requires a dereference and can make the object escape to the heap.
  • An unexported field is private to the type — unexported means package-level visibility — any code in the same package can access it.
  • Struct tags have an effect at compile time — struct tags are string literals that reflection reads at runtime — a typo in a tag’s key name compiles fine and is not caught by go vet either, only by actually exercising the code path that reads the tag.
  • go vet catches every unkeyed-literal risk — it only flags unkeyed literals of types from other packages, and it exempts a hardcoded list of stable stdlib types like image.Point.
  • Any two equal-looking struct values compare equal — true field-by-field, except a float64 field holding NaN, which never equals itself, and an any/interface field, which can panic at runtime if its dynamic type is a slice, map, or function.

💡 Real-world example

bytes.Buffer (stdlib) has a useful zero value — var b bytes.Buffer is immediately ready to Write and Read with no constructor call, exactly the zero-value-useful pattern this lesson recommends.

Struct-related bugs tend to hide in what looks like boilerplate. Check for these:

  • Does a struct field use a tag key that does not match what the library actually expects (jso:"name" instead of json:"name", a field the ORM’s docs call something else)? This compiles and often “half-works” — some fields serialize, one silently doesn’t.
  • Is a positional (unkeyed) literal used for a struct defined in a different package or file the author didn’t just write? A field reorder upstream silently scrambles the values.
  • Does anything compare two structs with ==, or use a struct as a map key, when one of its fields is any/interface{}? That is a latent panic waiting for the first slice or map value to flow through it.
  • Are the struct’s fields ordered for readability with no thought to size — small fields (bool, int8) scattered between large ones (int64, pointers, strings)? Not a bug, but worth flagging in a hot-path or bulk-allocated type.
  • Does a type require a New()/Init() call before it is safe to use, when its zero value could have been made to just work? Every caller who forgets that call has a latent bug, and nothing enforces the call at compile time.

🐛 Spot the bug

type Metrics struct {
Name string `json:"name"`
Count int `jso:"count"`
}
func main() {
m := Metrics{Name: "requests", Count: 42}
b, _ := json.Marshal(m)
fmt.Println(string(b))
}
Show the answer

The second field’s tag has a typo: `jso:"count"` instead of `json:"count"`. This is valid tag syntax — go vet ./... reports nothing, because the structtag check only validates the key:"value" grammar, not that "jso" is a key encoding/json recognizes. The struct compiles and runs, but Count marshals under its Go field name instead of the intended key: {"name":"requests","Count":42}. Nothing short of a test asserting the exact JSON shape would catch this — it will not fail a build, a lint pass, or even a casual glance at the struct definition.

You write p2 := p1 where p1 is a Person struct. You then set p2.Name = "Bob". What is p1.Name?
Which struct literal style is preferred in idiomatic Go?
What does this print? type User struct { Name string jso:"name" } b, _ := json.Marshal(User{Name: "Ada"}) fmt.Println(string(b))
What does unsafe.Sizeof report for these two structs, and why do they differ? type Bad struct { A bool; B int64; C bool } type Good struct { B int64; A bool; C bool }
Does this compile, and if so what does it print? var x, y any = []int{1}, []int{1} s1 := struct{ V any }{V: x} s2 := struct{ V any }{V: y} fmt.Println(s1 == s2)