Skip to content
intermediate Phase 4 · Structs, Methods, and Interfaces

Embedding and Composition

Promote fields and methods with embedding instead of class-style inheritance.

45m
0 problems
Topic Progress 0%

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?

Question 1 options

2. Two embedded structs both define `Name string` at the same depth. When does the compiler complain?

Question 2 options

3. A struct embeds `Timestamped` (field tag `json:"created_at"`) and is marshalled with encoding/json. What shape comes out?

Question 3 options

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?

Question 4 options

Flashcards

Question

What is the difference between embedding a struct and declaring it as a named field?

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?

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?

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?

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?

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.

Next Topics

Related Reading