Embedding
Composition over inheritance
Section titled “Composition over inheritance”Go has no extends, no base class, and no virtual dispatch. Instead it has embedding: you place one type inside another without a field name, and the outer type gains all of the inner type’s fields and methods as if they were its own.
type Named struct { Name string}
func (n Named) Describe() string { return "Name: " + n.Name }
type Employee struct { Named // embedded — no field name Department string}Now Employee has a Name field and a Describe() method promoted from Named:
e := Employee{Named: Named{Name: "Alice"}, Department: "Engineering"}fmt.Println(e.Name) // promoted fieldfmt.Println(e.Describe()) // promoted methodHow promotion works
Section titled “How promotion works”When you access e.Name, Go looks for a field or method named Name directly on Employee first. If none is found, the compiler searches by depth: a field or method at a shallower embedding level wins. Two candidates at the same depth are ambiguous, and using that selector is a compile error:
type A struct{ Name string }type B struct{ Name string }
type E struct { A B}
e := E{A{"a"}, B{"b"}}e.Name // compile error: ambiguous selector e.NamePromotion is resolved at compile time, and promoted methods join the outer type’s method set — that is why Employee satisfies Describer below. The long form e.Named.Name is always valid too, even when the short form would be ambiguous.
flowchart TD E["Employee (depth 0)"] N["Named (depth 1, embedded)"] E -->|embeds| N E -.promoted.-> Name["e.Name"] E -.promoted.-> Desc["e.Describe()"] N --> NameField["Name field"] N --> DescMethod["Describe() method"] NameField -.-> Name DescMethod -.-> Desc
You can override a promoted method by declaring a method with the same name on the outer type:
func (e Employee) Describe() string { return fmt.Sprintf("%s in %s", e.Name, e.Department)}Now e.Describe() calls the outer method. The inner method is still reachable via e.Named.Describe().
Interface embedding
Section titled “Interface embedding”Interfaces can also embed other interfaces:
type Stringer interface { String() string}
type Describer interface { Stringer // embeds Stringer Describe() string}Any type that satisfies Describer also satisfies Stringer automatically. The standard library uses this extensively: io.ReadWriter embeds io.Reader and io.Writer.
Embedding is not inheritance
Section titled “Embedding is not inheritance”A critical distinction:
| Inheritance (Java/C++) | Embedding (Go) |
|---|---|
| Virtual dispatch: calling a method through the base type dispatches to the derived override | No virtual dispatch: calling the inner method always calls that method, regardless of what is on the outer type |
instanceof / type hierarchy | No hierarchy — types are unrelated |
| Base class can reference derived class via virtual methods | Embedded type has no knowledge of the outer type |
Because there is no virtual dispatch, embedding does not allow polymorphism on the outer type. To get polymorphism, you still need an interface.
Embedding *T vs T
Section titled “Embedding *T vs T”Embedding a pointer instead of a value changes two things: whether the embedded type is shared or copied, and whether it can be nil. Its zero value is nil until something allocates it — and calling a promoted method through that nil pointer dereferences it:
type Named struct{ Name string }
func (n *Named) Hello() string { return "hi " + n.Name }
type Employee struct { *Named // embedded pointer — zero value is nil Dept string}
func main() { e := Employee{Dept: "eng"} // Named is nil, not allocated fmt.Println(e.Dept) fmt.Println(e.Hello()) // panics: promoted method dereferences a nil embedded pointer}Running this on Go 1.26.5:
engpanic: runtime error: invalid memory address or nil pointer dereferenceEmbedding Named by value instead avoids the nil case entirely, at the cost of copying the whole Named struct into every Employee. Embedding *Named avoids the copy and lets several outer values share one Named, but every promoted method call is one nil check away from this panic until something assigns the pointer.
Embedding an interface for partial mocks
Section titled “Embedding an interface for partial mocks”Struct embedding works with interface types too, not just concrete ones. Embedding an interface field gives the outer type every method of that interface for free — including the ones you never implement yourself, which forward straight to whatever the field holds. This is a common trick for a partial test double: override only the method the test cares about, and inherit the rest.
type Store interface { Get(key string) string Put(key, val string)}
// mockStore embeds the interface to get a partial implementation for free —// only override the method the test cares about.type mockStore struct { Store // nil — any unoverridden method panics if called}
func (m *mockStore) Get(key string) string { return "stub-" + key }
func main() { var s Store = &mockStore{} fmt.Println(s.Get("x")) // stub-x — overridden, fine
s.Put("x", "y") // panics: Put is not overridden, falls through to the nil embedded Store}stub-xpanic: runtime error: invalid memory address or nil pointer dereferencemockStore satisfies Store the moment it embeds it — the compiler is happy because the embedded interface’s method set covers everything Store requires — but the embedded field itself is nil until you assign a real implementation. Calling any method you did not override reaches into that nil field and panics. This pattern is deliberate and useful for stubbing exactly one method, but every unoverridden call is a live landmine, not a safe default.
JSON flattening through embedding
Section titled “JSON flattening through embedding”encoding/json promotes embedded struct fields into the parent’s JSON object when the embedded field has no tag of its own — this is often exactly what an API needs, and easy to trigger by accident when it is not:
type Address struct { City string `json:"city"`}
type Person struct { Address // embedded, no json tag — fields flatten into the parent object Name string `json:"name"`}
func main() { p := Person{Address: Address{City: "Bangkok"}, Name: "Ada"} b, _ := json.Marshal(p) fmt.Println(string(b))}{"city":"Bangkok","name":"Ada"}Address’s fields appear directly in Person’s JSON object, with no "address": {...} nesting — the same field-promotion rule that lets you write p.City in Go also flattens the JSON. Give the embedded field a tag (Address \json:“address”`) to get a nested object instead; that also switches it from an *embedded* field to a *named* one, so p.Citystops compiling and becomesp.Address.City`.
Method sets when embedding T vs *T
Section titled “Method sets when embedding T vs *T”Whether embedding by value or by pointer, promoted methods join the outer type’s method set according to the usual value/pointer rule — but which methods those are depends on the embedded type’s receivers, and the result can be surprising. Embedding Named by value into Employee, when Named’s only method has a pointer receiver:
type Named struct{ Name string }
func (n *Named) Hello() string { return "hi " + n.Name }
type Sayer interface{ Hello() string }
type Employee struct { Named // embedded by value Dept string}
func main() { var _ Sayer = Employee{} // Employee's method set has only value-receiver promoted methods}./main.go:15:16: cannot use Employee{} (value of struct type Employee) as Sayer value in variable declaration: Employee does not implement Sayer (method Hello has pointer receiver)Employee (value) does not satisfy Sayer, because Hello needs *Named, and an embedded-by-value Named field only promotes its pointer-receiver methods to *Employee, not to Employee. var _ Sayer = &Employee{} compiles fine — the same rule that governs a type’s own methods (T’s method set vs *T’s) applies recursively through embedding.
A leaked Lock/Unlock from an embedded mutex
Section titled “A leaked Lock/Unlock from an embedded mutex”Embedding sync.Mutex is a popular shortcut to get lock/unlock without naming a field — but embedding promotes Lock and Unlock into the exported method set of the outer type, handing every caller direct access to the internal lock:
// Bad: embedding sync.Mutex promotes Lock/Unlock into Cache's exported API —// any caller can lock/unlock the internal mutex directly.type Cache struct { sync.Mutex data map[string]string}
func main() { c := &Cache{data: map[string]string{}} c.Lock() // Lock is part of Cache's public method set — leaked c.data["a"] = "1" c.Unlock()}This compiles and runs without a race here, but it is a design bug: nothing stops a caller outside Cache from calling c.Lock() and never unlocking, or locking around code that also calls a Cache method that locks internally — an instant deadlock. Use a named, unexported field instead — mu sync.Mutex — so only Cache’s own methods can reach the lock, and the locking discipline stays entirely inside the type that owns it.
package main
import ( "encoding/json" "fmt")
type Named struct { Name string}
func (n Named) String() string { return "Named(" + n.Name + ")"}
func (n Named) Describe() string { // Describe calls String on itself — always Named's String, never an outer override return "Name: " + n.String()}
type Employee struct { Named Department string}
// Employee's own String — NOT used by Named.Describe() abovefunc (e Employee) String() string { return fmt.Sprintf("%s (Dept: %s)", e.Name, e.Department)}
// Interface embeddingtype Stringer interface { String() string}
type Describer interface { Stringer Describe() string}
func print(d Describer) { fmt.Println(d.String()) // Employee's own String, reached through the interface fmt.Println(d.Describe()) // promoted Describe still calls Named's String, not Employee's}
// Store is a minimal dependency; mockStore embeds it to get a partial fake for free.type Store interface { Get(key string) string Put(key, val string)}
type mockStore struct { Store // only Get is overridden below; calling Put would panic on this nil embedded Store}
func (m *mockStore) Get(key string) string { return "stub-" + key }
// Address is embedded without a json tag, so its fields flatten into Person's JSON object.type Address struct { City string `json:"city"`}
type Person struct { Address Name string `json:"name"`}
func main() { e := Employee{ Named: Named{Name: "Alice"}, Department: "Engineering", } // promoted field and method fmt.Println(e.Name) // Alice fmt.Println(e.Describe()) // Name: Named(Alice) — Named's own String, not Employee's override print(e)
var s Store = &mockStore{} fmt.Println(s.Get("x")) // stub-x — overridden, safe to call
p := Person{Address: Address{City: "Bangkok"}, Name: "Ada"} b, _ := json.Marshal(p) fmt.Println(string(b)) // {"city":"Bangkok","name":"Ada"} — embedded fields flatten into the parent object} Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Struct embedding | Reuses fields and methods without inheritance | Field shadowing if the outer type declares a field with the same name as the embedded type |
| Interface embedding | Composes a large interface from small ones | The bigger the interface gets, the harder it is to implement |
| Method promotion | Calls an embedded type’s methods directly | Confusing which type a method comes from once embedding is nested |
Common Misconceptions
Section titled “Common Misconceptions”- Embedding is inheritance. The embedded type has no knowledge of the outer type; there is no virtual dispatch and no IS-A relationship.
- Embedding creates a subtype relationship. A
Personthat embedsAddressis not a subtype ofAddress— it is only composition. - A promoted method runs against the outer type. A promoted method always calls the embedded type’s own receiver, never the outer type’s.
💡 Real-world example
io.ReadWriterembedsio.Readerandio.Writer— a type implementing both automatically satisfiesio.ReadWriterwith no extra declaration.
bufio.ReadWriterembeds*bufio.Readerand*bufio.Writerdirectly, promoting theirRead/Writemethods (and others) so callers reach both through a singleReadWritervalue, with no duplicated code.
Reading AI-generated code
Section titled “Reading AI-generated code”When generated code embeds a type, check for these before trusting it:
- Is the embedded field a pointer (
*T)? If so, is it guaranteed to be non-nil before any promoted method is called? A zero-value struct with an embedded*Tfield has a nilTuntil something assigns it. - Does the embedded field’s type include
sync.Mutex,sync.RWMutex, or similar? If it is not a named, unexported field (mu sync.Mutex),Lock/Unlockleak into the outer type’s exported API. - Is an interface embedded to build a partial mock or decorator? Every method not explicitly overridden forwards to the embedded field, which panics if that field was never assigned.
- Does an embedded struct field lack a JSON tag when the intent was a nested
"address": {...}object? Without a tag, its fields flatten into the parent object instead. - When embedding by value, does the embedded type’s only relevant method have a pointer receiver? If so, the value form of the outer type may not satisfy an interface that the pointer form does — check which one the code actually uses.
🐛 Spot the bug
type Cache struct { sync.Mutex data map[string]int}
func NewCache() *Cache { return &Cache{data: make(map[string]int)}}
func (c *Cache) Inc(key string) { c.Lock() defer c.Unlock() c.data[key]++}
func main() { c := NewCache() c.Lock() // caller reaches into Cache's internal lock directly c.Inc("requests") c.Unlock()}Show the answer
Cache embeds sync.Mutex directly, which promotes Lock and Unlock into Cache’s own exported method set — any caller, not just Cache’s methods, can call c.Lock(). In main, the caller locks the mutex and then calls c.Inc, which tries to Lock again internally — sync.Mutex is not reentrant, so the goroutine blocks on itself forever: fatal error: all goroutines are asleep - deadlock!. The fix is a named, unexported field — mu sync.Mutex — so only Cache’s own methods can reach the lock, and callers can never interfere with its internal locking discipline.