Structs
Defining a struct
Section titled “Defining a struct”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.
Struct tags
Section titled “Struct tags”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")) // namefmt.Println(f.Tag.Get("db")) // user_nameBecause 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 nameVerified 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 spacesSo 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).
Struct literals
Section titled “Struct literals”You can create a struct value in two ways:
// Positional — order must match the field declaration orderp := Point{3, 4}
// Named fields — order does not matter; unset fields get zero valuesNamed-field form is strongly preferred: it is self-documenting and resilient to field reordering.
Field access
Section titled “Field access”Use a dot to read or write any field:
fmt.Println(u.Name) // readu.Age = 30 // writeAnonymous structs
Section titled “Anonymous structs”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}Zero-value-useful design
Section titled “Zero-value-useful design”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).
The empty struct struct{}
Section titled “The empty struct struct{}”struct{} is a real, singleton type with a specific property: it occupies zero bytes.
var s struct{}fmt.Println(unsafe.Sizeof(s)) // 0Because 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 := <-donefmt.Println(open) // false — closed and drained, the classic "done" signalA 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.
Struct comparison
Section titled “Struct comparison”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) // trueStructs 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 variableA 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 []intVerified on 1.26.5:
panic: runtime error: comparing uncomparable type []intAn 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.
Unkeyed literals and go vet
Section titled “Unkeyed literals and go vet”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 packageVerified on 1.26.5 (go vet ./...):
main.go:10:7: example.com/t09b/shapes.Rect struct literal uses unkeyed fieldsThere 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.
Size, alignment, and padding
Section titled “Size, alignment, and padding”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 Verified on 1.26.5 (unsafe.Sizeof/unsafe.Offsetof):
fmt.Println(unsafe.Sizeof(Bad{}), unsafe.Sizeof(Good{})) // 24 16fmt.Println(unsafe.Offsetof(Bad{}.B), unsafe.Offsetof(Good{}.B)) // 8 0Bad 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.
Struct embedding (composition)
Section titled “Struct embedding (composition)”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} Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| value semantics | predictable behavior, no aliasing | copy cost for large structs |
| zero-value-useful design | no constructor call needed, fewer nil checks | not every type can have a meaningful zero value (e.g. one with a required field) |
| struct tags | declarative metadata for JSON/DB/validation | runtime-only — no compile-time check that a tag’s key name is correct |
| field ordered by size (padding-aware) | smaller struct, less memory at scale | slightly less readable than “logical” field grouping |
| exported vs unexported fields | clear control over the API surface | exposing a new field later is a breaking change |
Common Misconceptions
Section titled “Common Misconceptions”- 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 veteither, only by actually exercising the code path that reads the tag. go vetcatches every unkeyed-literal risk — it only flags unkeyed literals of types from other packages, and it exempts a hardcoded list of stable stdlib types likeimage.Point.- Any two equal-looking struct values compare equal — true field-by-field, except a
float64field holdingNaN, which never equals itself, and anany/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.Bufferis immediately ready toWriteandReadwith no constructor call, exactly the zero-value-useful pattern this lesson recommends.
Reading AI-generated code
Section titled “Reading AI-generated code”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 ofjson:"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 isany/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.