Skip to content

Embedding

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 field
fmt.Println(e.Describe()) // promoted method

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.Name

Promotion 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
A field or method at a shallower embedding depth wins; two candidates at the same depth are ambiguous

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().

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.

A critical distinction:

Inheritance (Java/C++)Embedding (Go)
Virtual dispatch: calling a method through the base type dispatches to the derived overrideNo virtual dispatch: calling the inner method always calls that method, regardless of what is on the outer type
instanceof / type hierarchyNo hierarchy — types are unrelated
Base class can reference derived class via virtual methodsEmbedded 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 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:

eng
panic: runtime error: invalid memory address or nil pointer dereference

Embedding 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.

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-x
panic: runtime error: invalid memory address or nil pointer dereference

mockStore 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.

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`.

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() above
func (e Employee) String() string {
return fmt.Sprintf("%s (Dept: %s)", e.Name, e.Department)
}
// Interface embedding
type 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
}
What you getBenefitCost
Struct embeddingReuses fields and methods without inheritanceField shadowing if the outer type declares a field with the same name as the embedded type
Interface embeddingComposes a large interface from small onesThe bigger the interface gets, the harder it is to implement
Method promotionCalls an embedded type’s methods directlyConfusing which type a method comes from once embedding is nested
  • 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 Person that embeds Address is not a subtype of Address — 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.ReadWriter embeds io.Reader and io.Writer — a type implementing both automatically satisfies io.ReadWriter with no extra declaration.

bufio.ReadWriter embeds *bufio.Reader and *bufio.Writer directly, promoting their Read/Write methods (and others) so callers reach both through a single ReadWriter value, with no duplicated 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 *T field has a nil T until 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/Unlock leak 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.

An Employee embeds Named. How do you access the promoted Name field?
What happens?
type Named struct{ Name string }
func (n *Named) Hello() string { return "hi " + n.Name }

type Employee struct {
    *Named
    Dept string
}

e := Employee{Dept: "eng"}
fmt.Println(e.Hello())
What is the problem with embedding sync.Mutex directly like this?
type Cache struct {
    sync.Mutex
    data map[string]string
}
Does this compile?
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
}

var _ Sayer = Employee{}
Interface io.ReadWriter embeds io.Reader and io.Writer. A type that satisfies io.ReadWriter automatically satisfies…
A Person struct embeds Address (no json tag) and both are marshaled with encoding/json. What does the JSON look like?