Skip to content

Embedding is not inheritance

Level: 301 · anyone who embedded a type, "overrode" one of its methods, and found the old one still being called

One line: Embedding promotes the inner type's fields and methods to the outer type, and that is all it does: a promoted method runs with the inner value as its receiver, so Account.Describe calling a.Name() reaches Account.Name however Savings redefines Name; two embedded types with the same member make the selector ambiguous; and a struct that embeds an interface has the whole method set at compile time and a nil-pointer panic at run time for any method it did not fill in.

Savings embeds Account and defines its own Name. The program deposits through the promoted pointer method, calls Name and Describe on the Savings, lists both method sets, and then wraps a Notifier interface in a LoudNotifier that fills in Notify only — once around a real notifier, once around nothing.

Verified output of embedding_is_not_inheritance_go.go — regenerated by tools/run_examples.py, never hand-typed.

s.Owner (promoted field) = Ann ; same variable as s.Account.Owner: true
s.Name()         = savings of Ann
s.Describe()     = account of Ann holding 100
s.Account.Name() = account of Ann
method set of Savings:  [Describe Name]
method set of *Savings: [Deposit Describe Name]
Savings is a Depositor: false ; *Savings is a Depositor: true
LoudNotifier over consoleNotifier: Notify -> LOUD: ORDER 7 SHIPPED ; Close() = <nil>
LoudNotifier over nothing:         Notify -> LOUD: ORDER 8 SHIPPED
LoudNotifier over nothing:         Close() panicked: runtime error: invalid memory address or nil pointer dereference

Reading the output

A promoted field is the inner field. &s.Owner == &s.Account.Owner is true: s.Owner is not a copy or an alias table, it is the spec's ↗ shorthand for the selector at depth 1 when there is no Owner at depth 0. The same shorthand gave s.Deposit(100) — Deposit has a pointer receiver on Account, s.Account is addressable, so it became (&s.Account).Deposit(100).

s.Name() is the Savings version; s.Describe() is not. Savings.Name at depth 0 shadows Account.Name at depth 1, so s.Name() prints savings of Ann. But s.Describe() printed account of Ann holding 100: Describe is promoted from Account, its receiver a is the Account, and a.Name() inside it is Account.Name. Effective Go ↗ puts it in one sentence — when the promoted method is invoked, the receiver is the inner type, not the outer one. There is no virtual dispatch to find; Account does not know it has been embedded.

The method sets follow the receivers. Savings has [Describe Name]; *Savings adds Deposit. The spec on struct types ↗ spells it out: if S embeds T, the method sets of S and *S both get the promoted methods with receiver T, and *S also gets those with receiver *T. So *Savings is a Depositor and Savings is not — the rule of A pointer receiver changes the method set, one level down.

Embedding an interface fills the method set, not the methods. LoudNotifier{} is a Notifier as far as the compiler is concerned — Notify and Close are both in its method set, Notify its own and Close promoted from the embedded Notifier. Around a consoleNotifier, Close() returned <nil>. Around nothing, the promoted Close is a call through a nil interface, and the runtime says invalid memory address or nil pointer dereference. The standard library uses this shape deliberately: sort.Reverse ↗ is a struct embedding a sort.Interface with only Less written — the kata below rebuilds it.

When two embedded types have the same name

Promotion works only while there is exactly one candidate at the shallowest depth. Two embedded types that both carry a Name field and a Describe method leave the outer type with neither: the compiler refuses the selector where it is used, not where the struct is declared.

Verified output of embedding_ambiguous_selector_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build ambiguous.go
# command-line-arguments
./ambiguous.go:21:16: ambiguous selector c.Name
./ambiguous.go:22:16: ambiguous selector c.Describe
exit status 1
$ go run resolved.go
Ann Lee
person Ann Lee at Northwind
exit status 0

Naming the path (c.Person.Name) is never ambiguous, and a Describe declared on Contact itself sits at depth 0 and wins. The rule is the spec's: a selector with more than one candidate at the shallowest depth is illegal. Note what this means for a type you embed from another package: a method added there in a later version can collide with one of yours, or with the other embedded type's.

embedding_is_not_inheritance_go.go and embedding_ambiguous_selector_sh.sh

embedding_is_not_inheritance_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// Embedding a type promotes its fields and methods to the outer type, and that
// is all it does. A promoted method still runs with the inner value as its
// receiver, so Account.Describe calling a.Name() reaches Account.Name even
// when the outer Savings has a Name of its own: there is no virtual dispatch.
// Embedding an interface in a struct gives the struct the whole method set at
// compile time; a method the struct did not fill in is a nil-pointer panic at
// run time.
//
//  go build embedding_is_not_inheritance_go.go && ./embedding_is_not_inheritance_go
package main

import (
    "fmt"
    "reflect"
    "strconv"
    "strings"
)

type Account struct {
    Owner   string
    balance int
}

func (a Account) Name() string { return "account of " + a.Owner }

// Describe calls a.Name() -- and a is the Account, whatever embeds it.
func (a Account) Describe() string { return a.Name() + " holding " + strconv.Itoa(a.balance) }

func (a *Account) Deposit(cents int) { a.balance += cents }

// Savings embeds Account and gives Name a new meaning.
type Savings struct {
    Account
    Rate int
}

func (s Savings) Name() string { return "savings of " + s.Owner }

type Depositor interface{ Deposit(cents int) }

// Notifier is the interface a delivery service needs. LoudNotifier embeds it
// and fills in Notify only; Close is promoted from whatever is inside.
type Notifier interface {
    Notify(message string) string
    Close() error
}

type consoleNotifier struct{}

func (consoleNotifier) Notify(message string) string { return "console: " + message }
func (consoleNotifier) Close() error                 { return nil }

type LoudNotifier struct{ Notifier }

func (LoudNotifier) Notify(message string) string { return "LOUD: " + strings.ToUpper(message) }

func methods(t reflect.Type) string {
    names := make([]string, 0, t.NumMethod())
    for i := range t.NumMethod() {
        names = append(names, t.Method(i).Name)
    }
    return "[" + strings.Join(names, " ") + "]"
}

func panicOf(f func()) (value any) {
    defer func() { value = recover() }()
    f()
    return "none"
}

func main() {
    s := Savings{Account: Account{Owner: "Ann"}, Rate: 3}
    s.Deposit(100) // promoted pointer method: s.Account is addressable, so (&s.Account).Deposit(100)
    fmt.Println("s.Owner (promoted field) =", s.Owner, "; same variable as s.Account.Owner:", &s.Owner == &s.Account.Owner)
    fmt.Println("s.Name()         =", s.Name())
    fmt.Println("s.Describe()     =", s.Describe())
    fmt.Println("s.Account.Name() =", s.Account.Name())

    fmt.Println("method set of Savings: ", methods(reflect.TypeOf(s)))
    fmt.Println("method set of *Savings:", methods(reflect.TypeOf(&s)))
    _, valueOK := any(s).(Depositor)
    _, pointerOK := any(&s).(Depositor)
    fmt.Println("Savings is a Depositor:", valueOK, "; *Savings is a Depositor:", pointerOK)

    var n Notifier = LoudNotifier{consoleNotifier{}}
    fmt.Println("LoudNotifier over consoleNotifier: Notify ->", n.Notify("order 7 shipped"), "; Close() =", n.Close())

    n = LoudNotifier{} // the embedded Notifier is nil, and the compiler cannot tell
    fmt.Println("LoudNotifier over nothing:         Notify ->", n.Notify("order 8 shipped"))
    fmt.Println("LoudNotifier over nothing:         Close() panicked:", panicOf(func() { _ = n.Close() }))
}

embedding_ambiguous_selector_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# Two embedded types at the same depth that both have a Name field and a
# Describe method: the outer type has neither, and using either through it
# is a compile error, "ambiguous selector". Naming the path, or giving the
# outer type its own Name and Describe, resolves it.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/ambiguous.go" <<'GO'
package main

import "fmt"

type Person struct{ Name string }

func (p Person) Describe() string { return "person " + p.Name }

type Company struct{ Name string }

func (c Company) Describe() string { return "company " + c.Name }

// Contact embeds both, so both promote a Name and a Describe at depth 1.
type Contact struct {
    Person
    Company
}

func main() {
    c := Contact{Person{"Ann Lee"}, Company{"Northwind"}}
    fmt.Println(c.Name)
    fmt.Println(c.Describe())
}
GO

cat >"$dir/resolved.go" <<'GO'
package main

import "fmt"

type Person struct{ Name string }

func (p Person) Describe() string { return "person " + p.Name }

type Company struct{ Name string }

func (c Company) Describe() string { return "company " + c.Name }

type Contact struct {
    Person
    Company
}

// A Describe at depth 0 shadows both promoted ones.
func (c Contact) Describe() string { return c.Person.Describe() + " at " + c.Company.Name }

func main() {
    c := Contact{Person{"Ann Lee"}, Company{"Northwind"}}
    fmt.Println(c.Person.Name) // the full path is never ambiguous
    fmt.Println(c.Describe())
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build ambiguous.go'
(cd "$dir" && go build ambiguous.go) 2>&1
echo "exit status $?"

say 'go run resolved.go'
(cd "$dir" && go run resolved.go) 2>&1
echo "exit status $?"

What to do

  • Embed to reuse, not to specialise. A method of the inner type will never call one of yours. If the inner type needs to call back into the outer one, hand it a function value or an interface at construction — that is delegation written out, and it is what Go has instead of virtual methods.
  • Redefine a promoted method only to wrap it. Savings.Name calling s.Account.Name() and adding to the result is fine; a Name that means something different from the Account's will surprise whoever calls Describe.
  • Embed an interface only to override part of it, and say what the rest is. sort.Reverse embeds a sort.Interface that is never nil, because the constructor takes it as a parameter. A struct that can be built with a nil inside — LoudNotifier{} — compiles as a full implementation and panics on first use of the missing method.
  • Embedding exports the inner type's methods. A struct that embeds sync.Mutex has Lock and Unlock in its public method set, callable by any package; a named field mu sync.Mutex keeps them private. Either way the lock must not be copied — the vet page's copylocks entry shows the value receiver that copies it, and A mutex guards a counter shows the lock working.

Practice

Write descending(s sort.Interface) sort.Interface the way the standard library writes sort.Reverse: a struct that embeds the given sort.Interface and declares only Less, returning the embedded Less with its arguments swapped. Sort a slice of invoices by amount ascending with sort.Sort, then descending with your descending, print both orders, and confirm with sort.IsSorted that the descending order is sorted according to descending(invoices) and not according to invoices.

Solution

embedding_is_not_inheritance_kata_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// Kata: write descending(s sort.Interface) sort.Interface the way the standard
// library writes sort.Reverse -- a struct that embeds the given sort.Interface
// and overrides Less only -- and use it to sort invoices largest first.
// Print the result and confirm it with sort.IsSorted.
//
//  go build embedding_is_not_inheritance_kata_go.go && ./embedding_is_not_inheritance_kata_go
package main

import (
    "fmt"
    "sort"
)

type Invoice struct {
    ID     string
    Amount int
}

// byAmount sorts invoices by amount, smallest first.
type byAmount []Invoice

func (b byAmount) Len() int           { return len(b) }
func (b byAmount) Less(i, j int) bool { return b[i].Amount < b[j].Amount }
func (b byAmount) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }

// reversed embeds a sort.Interface: Len and Swap are promoted from it, and
// only Less is written here. Its Less calls the embedded Less with the
// indexes swapped -- it has to name it, r.Interface.Less, because a plain
// r.Less would be this method calling itself.
type reversed struct{ sort.Interface }

func (r reversed) Less(i, j int) bool { return r.Interface.Less(j, i) }

func descending(s sort.Interface) sort.Interface { return reversed{s} }

func main() {
    invoices := byAmount{{"INV-1", 120}, {"INV-2", 250}, {"INV-3", 75}, {"INV-4", 300}}
    sort.Sort(invoices)
    fmt.Println("ascending: ", invoices)
    sort.Sort(descending(invoices))
    fmt.Println("descending:", invoices)
    fmt.Println("sort.IsSorted(descending(invoices)):", sort.IsSorted(descending(invoices)))
    fmt.Println("sort.IsSorted(invoices):            ", sort.IsSorted(invoices))
}

Verified output of embedding_is_not_inheritance_kata_go.go — regenerated by tools/run_examples.py, never hand-typed.

ascending:  [{INV-3 75} {INV-1 120} {INV-2 250} {INV-4 300}]
descending: [{INV-4 300} {INV-2 250} {INV-1 120} {INV-3 75}]
sort.IsSorted(descending(invoices)): true
sort.IsSorted(invoices):             false

In other languages

  • The Rust library's Supertraits ↗ — Rust has no struct embedding and no inheritance either, but a trait's default method ↗ that calls self.name() does reach the implementing type's name, because the trait is implemented for that type as a whole; Go's promoted Describe reaches Account.Name because it is Account's method running on an Account.
  • C++ is the language this lesson is contrasting with: a virtual member called from a base-class method dispatches to the derived class's override (cppreference ↗), and Java does the same for every non-static, non-private method (the Java tutorial on overriding ↗). Go's s.Describe() is the case both of them would resolve the other way.

Sources

  • The Go specification: Struct types ↗ (embedded fields, promoted fields and methods, and which method sets they join), Selectors ↗ (depth, shadowing, and the illegal ambiguous selector) and Method sets ↗.
  • Effective Go, "Embedding" ↗ — the receiver of a promoted method is the inner type; the name-conflict rules.
  • sort.Reverse ↗ and its source, src/sort/sort.go at go1.25.5 ↗ — a struct embedding Interface and declaring Less.
  • Alan A. A. Donovan and Brian W. Kernighan, The Go Programming Language (Addison-Wesley, 2015), chapter 6, "Methods" — section 6.3, "Composing Types by Struct Embedding", and chapter 4 — section 4.4.3, "Struct Embedding and Anonymous Fields".
  • Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 7, "Types, Methods, and Interfaces" — "Use Embedding for Composition", "Embedding Is Not Inheritance" and "Embedding and Interfaces".
  • Adam Freeman, Pro Go (Apress, 2022), chapter 13, "Type and Interface Composition" — "Composing Types", "Understanding When Promotion Cannot Be Performed" and "Using Composition to Implement Interfaces".