Borrowed Fields, Not Inherited Types
Three server types — HTTP, gRPC, and a test double — all need a mutex, a start time, and a logger. Copy those three fields into every declaration and every method that touches them repeats srv.mu.Lock() three times across three types. Embedding says: hold one instance of the shared type, and let its members appear as if they were ours.
type Server struct {
sync.Mutex // embedded — no field name
Name string
Started time.Time
}
srv := Server{Name: "api"}
srv.Lock() // promoted: == srv.Mutex.Lock()
srv.Name = "worker" // promoted field
srv.Unlock()
An embedded field is written as its type with no field name, and it compiles to exactly that — a field named after the type, plus sugar that lets you skip the qualification:
// this is what the compiler sees
type Server struct {
Mutex sync.Mutex
Name string
Started time.Time
}
// srv.Mutex.Lock() still works — promotion adds a shortcut, not a new path
Promotion applies to fields and methods alike: every exported and unexported member of the embedded type that is visible from here becomes callable on the outer type (unexported members promote only within their own package). The pointer-receiver method rule follows automatically — with sync.Mutex embedded by value, srv.Lock() works whenever srv is addressable, because the compiler takes &srv.Mutex for you.
Embedding a pointer works too, with the aliasing consequence you would expect:
type Server struct {
*Config // shared pointer: every Server sees edits through this pointer
}
What embedding deliberately does not give you: no runtime dispatch, no super, no overriding in the inheritance sense. Conflicts are resolved statically — an outer declaration shadows an embedded one at greater depth, and the rule is simply shallowest wins:
type Base struct{}
func (Base) String() string { return "base" }
type Derived struct {
Base
}
func (Derived) String() string { return "derived" }
fmt.Println(Derived{}.String()) // derived — outer method shadows Base.String
If two embedded types at the same depth both provide the member, nothing shadows: c.Name becomes an ambiguous selector and fails at the use site (not at the type declaration), with the fix spelled out — c.A.Name. That error-at-use behavior is the compiler telling you the sugar ran out; spell the path and the design keeps working.
Interfaces That Stack
Interfaces embed the same way structs do — except the member list is all methods, so embedding one interface inside another unions their method sets:
type ReadCloser interface {
io.Reader // Read([]byte) (int, error)
io.Closer // Close() error
}
// identical to declaring both methods by hand
The standard library is built from these blocks: io.ReadCloser, io.ReadWriteCloser, and bufio.Reader's neighbors all stack two or three one-method primitives. This is why io.Reader being one method long matters — a small interface is a Lego brick; a 12-method interface is a wall.
Your own designs compose the same way, always upward from narrow to wide:
type Named interface {
Name() string
}
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
type Registrable interface {
Handler // embedded: contributes ServeHTTP
Named // contributes Name
Mount(path string) error
}
// a registrable handler is usable wherever a plain Handler is needed:
func route(h Handler) { /* ... */ }
Registrable satisfies Handler structurally, because its method set contains Handler's — interfaces never declare what they implement, they just accumulate shapes (Interfaces and Implicit Satisfaction). A function that accepts Handler accepts anything with ServeHTTP, and widening is free.
Two composition habits keep this healthy. Compose in the consumer's vocabulary: if your service needs Get, declare Get; embed the standard pieces (io.Reader plus your Close()) rather than re-listing their methods, so the stdlib signature cannot drift from yours. Embed for interfaces, explicit fields for structs when identity matters — an interface has no fields to collide and no serialization shape, so unioning method sets is always safe. A struct brings storage, copying, and a JSON representation along, and those are where embedding earns its two known surprises — the next chapter's subject.
Also note the direction that does not exist: an interface cannot embed a struct, and a struct embedding an interface gains only a field of that interface type — no promotion of interest. The composition idioms are struct-embeds-struct for concrete reuse and interface-embeds-interface for capability stacking. Together with implicit satisfaction they cover every case inheritance would have been proposed for.
The Two Surprises Hiding in Promotion
Embedding reads like inheritance, so code that would be a bug in a class hierarchy compiles here with a different bug. Both surprises come from promoted members belonging to a value the outer struct holds.
Surprise one: the embedded value is copied with the struct.
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
type Agg struct {
Counter
total int
}
func touch(a Agg) { a.Inc() } // promotes to a.Counter.Inc — on the copy
x := Agg{}
touch(x)
fmt.Println(x.n) // 0 — touch incremented its parameter's embedded Counter
a.Inc() compiles because a is addressable — but the address is the copy's embedded field. The promoted method did its job perfectly on the wrong object. The fix depends on intent: take *Agg so the embedded Counter is shared through the pointer, or accept that Agg is a value type and return the modified copy. The same rule explains embedding sync.Mutex: the outer struct owns that mutex, and passing the struct by value copies it — go vet flags this as copylocks, because a copied mutex is two independent locks. The convention is that mutex-holding structs travel as pointers, always.
Embedding *Counter instead shares one counter across every copy — which is usually what you wanted from "composition": one embedded instance, many references.
Surprise two: promotion changes serialized shape. encoding/json flattens embedded fields into the parent object:
type Timestamped struct {
CreatedAt time.Time `json:"created_at"`
}
type Post struct {
Timestamped
Title string `json:"title"`
}
// marshalled Post -> {"created_at": "...", "title": "..."}
// not {"Timestamped": {"created_at": ...}, "title": ...}
Usually this is exactly what you want — a shared Auditable base that disappears into the wire format. It becomes a bug when the API contract expected a nested object, or when two embedded types contribute colliding JSON keys: at marshal time the shallowest tag wins, silently, where the compiler's ambiguous-selector check never runs (reflection does not compile). Read your marshalled output after introducing an embed — the JSON shape is part of your API, and promotion just edited it.
The Case for Keeping the Field Named
Embedding is a tool, not a virtue. When each condition below applies, the honest declaration is a named field with explicit forwarding.
| Situation | Prefer | Why |
|---|---|---|
| you want a nested JSON/YAML shape or a different key | named field | embedded fields flatten automatically |
| only some members should be reachable | named field + explicit methods | embedding promotes everything visible |
| the outer type must not be copyable carelessly (holds a mutex) | named field or pointer embed | value embed invites by-value copies (vet copylocks) |
| two candidates at equal depth could collide later | named fields | ambiguous selectors break at the use site, not the declaration |
| reuse is behavioral, not structural | interface | one method set, any implementation |
| you are tempted by a deep hierarchy | stop: compose two things side by side | depth makes shadowing rules do design's job |
The delegation pattern is what "keep it named" looks like in practice — it forwards on purpose, and only what it chooses:
type Handler struct {
next http.Handler // named: unexported, not promoted, not in the JSON
}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logBefore(r)
h.next.ServeHTTP(w, r) // explicit — this is where middleware hooks live
}
Everything is visible and intentional: one forwarding method, room to wrap behavior around the call, nothing else leaking through.
Why Go stopped at promotion instead of building inheritance: the failure modes of class hierarchies — fragile base classes where a parent's change silently breaks children, implicit overrides that hide which implementation runs, diamond ambiguity that needs C3 linearization — all come from runtime resolution. Go resolves promotion statically at compile time: shallowest wins, ambiguity errors point at the use site, and there is no super to misuse. Stacking interfaces upward (Interfaces and Implicit Satisfaction) and embedding concrete structs for shared state is the whole story of reuse in Go. The discipline is knowing which of the two you are reaching for — capability (interface) or storage and methods (embedding) — and choosing a named field the moment identity, shape, or visibility has to stay explicit.
Quiz
1. What does `type Server struct { sync.Mutex; Name string }` compile into, and what does srv.Lock() mean?
2. Two embedded structs both define `Name string` at the same depth. When does the compiler complain?
3. A struct embeds `Timestamped` (field tag `json:"created_at"`) and is marshalled with encoding/json. What shape comes out?
4. `type Agg struct { Counter }` with pointer-receiver `Inc`, and `func touch(a Agg) { a.Inc() }` called on `x := Agg{}`. What is x.n afterward?
Flashcards
Question
What is the difference between embedding a struct and declaring it as a named field?
Click to reveal answer
Answer
They are the same storage: `type S struct { T }` is a field named T of type T. Embedding adds promotion sugar — T's members are selectable on S without qualification — plus flattening in encoding/json. The named field requires s.T.Member at every use and keeps the field out of automatic promotion.
Question
How does interface embedding differ from struct embedding?
Click to reveal answer
Answer
An interface has only methods, so embedding unions the method sets with no storage, copying, or serialization involved — io.ReadCloser is literally Reader + Closer. Struct embedding brings value semantics with it: embedded values are copied with the outer struct and flatten into JSON.
Question
What is the resolution rule when an outer member and an embedded member share a name?
Click to reveal answer
Answer
Shallowest wins: a method or field declared on the outer type shadows anything promoted from depth 1, which shadows depth 2. Equal depth is not resolved — selecting it is an ambiguous-selector compile error at the use site, fixed by spelling the full path (c.A.Name).
Question
Why do structs embedding sync.Mutex need to travel as pointers?
Click to reveal answer
Answer
The mutex is embedded by value, so copying the outer struct copies the mutex — two independent locks, one of them never held by the original's users (go vet calls this copylocks). A pointer keeps one mutex instance that every reference shares.
Question
When should you choose a named field over embedding?
Click to reveal answer
Answer
When you need a nested serialization shape, want only some members reachable, hold a lock that must not be copied, or want to prevent future equal-depth collisions. Also when forwarding is intentional — middleware wrapping next http.Handler spells it out and hooks behavior around the call.
Revision Notes
Key Takeaways
- 1. Embedding is an unnamed field plus compile-time promotion — desugar it mentally and every selector question answers itself
- 2. Interface embedding unions method sets, which is how small stdlib interfaces stack into ReadCloser and friends
- 3. Conflicts resolve shallowest-wins; equal depth is an ambiguous-selector error at the use site, and a silent JSON collision at marshal time
- 4. Embedded values are copied with the struct — promoted mutations on a by-value copy never reach the caller, and copied mutexes are a vet error
- 5. Composition replaces inheritance here: static promotion, no fragile base class; choose a named field whenever identity or shape must stay explicit
Interview Tips
- • "Explain Go embedding versus inheritance" — promotion is static sugar over a named field, resolved at compile time; no override, no super, conflicts are errors rather than runtime surprises
- • "What does `type S struct { T }` mean?" — a field named T plus member promotion; be ready to desugar s.M as s.T.M on the spot
- • "Why did my mutation disappear?" — promoted method ran on a by-value copy of the embedded struct; show the *Agg fix or the returned-value pattern
- • "Embedded struct in JSON — what shape?" — flattened into the parent; mention it as a feature for Auditable bases and a hazard when the contract expected nesting
Cheat Sheet
Embedding and Composition
Embedding desugars to: struct { T } = field named T + promotion sugar; interface { R; C } = method-set union. Promotion is static — no runtime dispatch, no super.
Conflict rules:
| Situation | Result |
|---|---|
| outer declares same member as embedded | outer wins (shallowest) |
| equal-depth embeds both provide it | ambiguous selector — error at use; spell c.A.Member |
| two embedded structs, same JSON tag | shallowest tag wins silently at marshal time |
Copy rules: embedded value copies with the outer struct (mutating a promoted method on a by-value copy changes nothing upstream); embedded pointer shares. Mutex embeds require pointer receivers of the outer type — vet copylocks guards the mistake.
Choose: capability -> interface; shared storage/methods -> embed; explicit shape/visibility -> named field; deep hierarchy -> compose two things side by side.
Code template:
type Auditable struct {
CreatedAt time.Time `json:"created_at"`
}
type Post struct {
Auditable
Title string `json:"title"`
}
type Registrable interface {
http.Handler
Name() string
}
Continue Learning
Before You Start
- No prerequisites listed.