Structs, Embedding & Field Promotion
Structs group named fields into a single type.
Embedding lets you compose behavior and fields from other types without inheritance, promoting exported members to the outer type.
Summary
A struct literal sets fields by name or position (if unambiguous).
Embedded fields must be unnamed and their type name becomes a field name for promotion.
Promoted fields and methods are callable on the outer value as if declared there, unless shadowed.
Recipe
Quick-reference recipe card - copy-paste ready.
package main
import "fmt"
type Logger struct{}
func (Logger) Log(msg string) { fmt.Println(msg) }
type Server struct {
Logger
Addr string
}
func main() {
s := Server{Addr: ":8080"}
s.Log("listening") // promoted method
fmt.Println(s.Addr)
}When to reach for this:
- Model records with named fields (
User,Order,Config). - Compose small capabilities via embedding (
io.Reader,sync.Mutex). - Expose promoted API only when the outer type truly is-a usage of the embedded type.
- Prefer explicit fields when names would collide or confuse readers.
Working Example
package main
import "fmt"
type Person struct {
Name string
Age int
}
type Employee struct {
Person
ID int
Dept string
}
func (p Person) Greet() string {
return "hello, " + p.Name
}
func describe(e Employee) {
fmt.Println(e.Name, e.ID, e.Greet())
}
func main() {
e := Employee{
Person: Person{Name: "Ada", Age: 36},
ID: 1001,
Dept: "research",
}
describe(e)
// Explicit outer field wins over promoted if names collide
type Shadow struct {
Person
Name string
}
sh := Shadow{Person: Person{Name: "Grace"}, Name: "Override"}
fmt.Println(sh.Name, sh.Person.Name)
}What this demonstrates:
- Nested initialization with named struct literals.
- Method promotion from embedded
PersontoEmployee. - Field promotion:
e.Nameaccesses embeddedPerson.Name. - Outer fields shadow promoted names - use qualified access when both exist.
Deep Dive
How It Works
- Struct types are comparable when all fields are comparable.
- Embedding is syntactic sugar: the embedded type's exported fields and methods promote one level.
- Methods attach to the embedded type's method set; outer type gains those methods in its method set.
- Pointer receivers on embedded type still promote for addressable outer values.
Struct Literal Forms
| Form | Example | When |
|---|---|---|
| Named fields | Person{Name: "Ada"} | Default - clear and order-free |
| Positional | Person{"Ada", 36} | Small stable structs only |
| Pointer literal | &Person{Name: "Ada"} | APIs expecting *Person |
| Partial | Person{Name: "Ada"} | Other fields zero |
Promotion Rules
| Situation | Result |
|---|---|
Single embedded T | outer.Field if T.Field exported |
Method on T | Callable as outer.Method() |
| Name collision on outer | Outer field shadows; use outer.T.Field |
| Multiple embeddings conflict | Ambiguous access - compiler error without qualifier |
Go Notes
// Embed interface to forward behavior
type ReadCloser struct {
io.Reader
io.Closer
}
// Embed mutex - zero value usable
type SafeMap struct {
sync.Mutex
m map[string]int
}Gotchas
- Embedding only for composition - Promoted APIs leak embedded type's surface unexpectedly. Fix: embed unexported helper structs or delegate explicitly.
- Nil embedded pointer - Embedded
*Twith nil panics on promoted method call. Fix: ensure initialization or guard calls. - JSON tags on embedded fields - Anonymous fields flatten in encoding/json by default. Fix: name fields or adjust tags when nesting matters.
- Comparable struct with slice field - Struct becomes non-comparable. Fix: do not use
==; compare fields manually. - Shadowed field confusion - Readers may not know which
Nameapplies. Fix: avoid duplicate field names across outer and embedded types. - Embedding concrete type for "inheritance" - Go has no subclass polymorphism via embedding alone. Fix: use interfaces for behavior contracts.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Named field person Person | Clear ownership, no promotion | You want promoted methods |
| Embedding | True is-a composition (Server has logging) | Simple data grouping only |
| Interfaces | Behavior abstraction | You only need data fields |
| Separate packages | Hide embedded implementation | Tight local helpers |
FAQs
What is field promotion?
Exported fields and methods of an embedded type appear on the outer type without a qualifier.
e.Name works when Person is embedded and has Name.
Can I embed multiple types?
Yes, if promoted names do not conflict.
Ambiguous promotions require explicit selectors like e.Person.Name.
Does embedding copy the embedded struct?
The embedded field is a real field stored inside the outer struct by value (or pointer if *T).
It is not a reference link.
How do methods promote with pointer receivers?
If T has func (t *T) M(), outer O with embedded T gets M when O or *O is addressable per receiver rules.
Can I embed primitive types?
You can embed only named types, not bare int.
Define type UserID int and embed UserID if needed.
How does encoding/json handle embedded structs?
Anonymous struct fields marshal inline (fields promoted to top level) unless tagged json:",inline" changes behavior for nested structs.
Named fields nest normally.
What is the empty struct struct{} for?
Zero-size marker type used for set keys map[K]struct{} and signaling channels.
It allocates no additional bytes for the value.
Are struct field tags compile-time only?
Tags are string metadata read via reflection (reflect.StructTag).
Packages like encoding/json and ORMs interpret them at runtime.
Can I compare structs with ==?
Only if all fields are comparable.
Slices, maps, and functions inside break comparability.
How do I copy a struct?
Assignment copies value types field-wise.
For pointer fields, copy duplicates pointers - deep copy manually if needed.
Should I use pointers to structs in APIs?
Use *T when mutating, avoiding copy cost for large structs, or representing optional presence.
Small immutable records can stay values.
What is an unexported embedded field?
Embedding unexportedType promotes only to types in the same package.
External packages cannot access promoted unexported members.
Related
- Pointers & the Address-of Operator - Pointer receivers and struct mutation
- Zero Values and Initialization - Struct literals and defaults
- Arrays, Slices & Maps at a Glance - Fields that hold collections
- Go Fundamentals Basics - Embedding quick example
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).