24. Embedding: composition instead of inheritance

📖 Reading · 10 min
💡 Every code box below is live — edit it and hit Run.

Go has no extends. What it has is embedding: put a type inside a struct without giving it a field name, and its fields and methods are promoted to the outer type. It looks like inheritance from the outside and behaves much more predictably.

A named field vs an embedded type

package main

import "fmt"

type Address struct {
    City string
    Zip  string
}

type WithField struct {
    Name string
    Home Address
}

type WithEmbed struct {
    Name string
    Address
}

func main() {
    a := WithField{Name: "Ada", Home: Address{City: "London", Zip: "E1"}}
    b := WithEmbed{Name: "Grace", Address: Address{City: "Arlington", Zip: "22204"}}

    fmt.Println(a.Home.City)
    fmt.Println(b.City)
    fmt.Println(b.Address.City)
}

The only syntactic difference is the missing field name — Address instead of Home Address. But now b.City works directly: the embedded type's fields are promoted to the outer struct.

b.Address.City still works too. The promotion is a shortcut, not a merge — the embedded value is really there, under a field named after its type.

Why this is useful

You pull shared fields into one type and embed it wherever they belong:

package main

import "fmt"

type Meta struct {
    ID        int
    CreatedBy string
}

type Article struct {
    Meta
    Title string
    Words int
}

type Comment struct {
    Meta
    Body string
}

func main() {
    a := Article{
        Meta:  Meta{ID: 1, CreatedBy: "ada"},
        Title: "Slices explained",
        Words: 1200,
    }
    c := Comment{
        Meta: Meta{ID: 2, CreatedBy: "grace"},
        Body: "Great post!",
    }

    fmt.Println(a.ID, a.CreatedBy, a.Title)
    fmt.Println(c.ID, c.CreatedBy, c.Body)
}

Note the literal: you initialise the embedded value under its type name, Meta: Meta{...}. Only access is promoted, not construction.

Methods are promoted too

This is where embedding earns its keep. (Methods get a lesson of their own next — for now, read func (m Meta) Owner() string as "a function attached to Meta".)

package main

import "fmt"

type Meta struct {
    ID        int
    CreatedBy string
}

func (m Meta) Owner() string {
    return fmt.Sprintf("#%d by %s", m.ID, m.CreatedBy)
}

type Article struct {
    Meta
    Title string
}

func main() {
    a := Article{Meta: Meta{ID: 7, CreatedBy: "katherine"}, Title: "Maps"}

    fmt.Println(a.Owner())
    fmt.Println(a.Title, "-", a.Meta.Owner())
}

Article never declared Owner, but it has one. That's how Go reuses behaviour — and it's why you'll see sync.Mutex embedded directly in structs so the outer type gets Lock() and Unlock() for free.

Overriding: the outer type wins

Define a method with the same name on the outer type and it takes priority:

package main

import "fmt"

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

func (a Animal) Describe() string {
    return "I am " + a.Name + ". " + a.Speak()
}

type Dog struct {
    Animal
}

func (d Dog) Speak() string {
    return d.Name + " barks"
}

func main() {
    d := Dog{Animal{Name: "Rex"}}

    fmt.Println(d.Speak())
    fmt.Println(d.Animal.Speak())
    fmt.Println(d.Describe())
}

Look hard at the third line: Describe is Animal's method, and it calls a.Speak()Animal's Speak, not Dog's. It prints "Rex makes a sound".

If you come from Java or Python, that's the opposite of what you expect. There is no virtual dispatch here: Animal has no idea it's embedded in anything. Embedding is composition — the outer type forwards to the inner one, and the inner one never calls back up.

When you genuinely need the "call the specific implementation" behaviour, you use an interface, which is the next module. This distinction is the single biggest mental shift for people arriving from class-based languages, so it's worth running that program twice.

Ambiguity is an error, not a guess

Embed two types with the same field and Go refuses to pick:

package main

import "fmt"

type A struct{ Name string }
type B struct{ Name string }

type C struct {
    A
    B
}

func main() {
    c := C{A: A{Name: "from A"}, B: B{Name: "from B"}}

    // fmt.Println(c.Name) // won't compile: ambiguous selector
    fmt.Println(c.A.Name, c.B.Name)
}

The promotion simply doesn't happen for the ambiguous name; you must say which one you mean. No resolution order to memorise, no surprises — exactly the kind of multiple-inheritance headache Go's design avoids.

Embedding pointers and interfaces

You can embed a pointer type or an interface as well:

package main

import "fmt"

type Logger struct {
    Prefix string
}

func (l *Logger) Log(msg string) {
    fmt.Println(l.Prefix + msg)
}

type Service struct {
    *Logger
    Name string
}

func main() {
    s := Service{Logger: &Logger{Prefix: "[svc] "}, Name: "billing"}
    s.Log("started")
    s.Prefix = "[billing] "
    s.Log("ready")
}

Embedding *Logger shares one logger rather than copying it — several services can point at the same instance. Embedding an interface is rarer but powerful: it lets your type satisfy that interface by forwarding, and you override only the methods you care about.

Your turn

Build a Manager type that embeds an Employee (with Name and Salary) and adds a Reports int. Print the manager's name and salary through the promoted fields:

Grace earns 120000 and manages 4 people
package main

import "fmt"

type Employee struct {
    Name   string
    Salary int
}

// define Manager, embedding Employee

func main() {
    m := Manager{Employee: Employee{Name: "Grace", Salary: 120000}, Reports: 4}
    fmt.Printf("%s earns %d and manages %d people\n", m.Name, m.Salary, m.Reports)
}
package main

import "fmt"

type Employee struct {
    Name   string
    Salary int
}

type Manager struct {
    Employee
    Reports int
}

func main() {
    m := Manager{Employee: Employee{Name: "Grace", Salary: 120000}, Reports: 4}
    fmt.Printf("%s earns %d and manages %d people\n", m.Name, m.Salary, m.Reports)
}

Next: attaching behaviour to your types properly — methods.