Skip to content
AgileLogics
6 min read

Embedding & Composition

Prerequisites: Interfaces

Go has no classes and no inheritance. Instead you build bigger types out of smaller ones — composition. Go gives composition a shortcut called embedding: include a type in a struct without a field name, and its fields and methods are promoted so you can use them directly on the outer struct.

Embedding is not inheritance: the outer type is not a subtype of the inner one, and there is no super. The embedded value is just a field whose name is its type name.

Syntax

type Service struct {
    Logger       // embedded: field name is "Logger"
    Name string
}

type ReadWriter interface {
    Reader       // interfaces can embed other interfaces
    Writer
}

Example

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 (s Service) Start() {
    s.Log("starting " + s.Name) // promoted method from Logger
}

type Base struct{}

func (Base) Describe() string { return "base" }

type Special struct {
    Base
}

// Describe on Special shadows the promoted Base.Describe.
func (Special) Describe() string { return "special" }

type Describer interface {
    Describe() string
}

func main() {
    svc := Service{Logger: Logger{Prefix: "[svc]"}, Name: "api"}
    svc.Start()
    svc.Log("direct call")
    fmt.Println(svc.Prefix)        // promoted field
    fmt.Println(svc.Logger.Prefix) // the same field, spelled out

    sp := Special{}
    fmt.Println(sp.Describe())
    fmt.Println(sp.Base.Describe()) // the embedded method is still reachable

    var d Describer = Base{} // Base satisfies Describer...
    fmt.Println(d.Describe())
    d = sp // ...and so does Special
    fmt.Println(d.Describe())
}

Expected output

[svc] starting api
[svc] direct call
[svc]
[svc]
special
base
base
special

Because promoted methods become part of the outer type's method set, a Service satisfies any interface that Logger satisfies. But you cannot assign a Service to a variable of type Logger — they are different types.

Common mistakes

  • Expecting "virtual" behaviour: when Logger.Log runs, it only knows about the Logger, never the Service that contains it. Methods on the embedded type cannot call "overridden" methods of the outer type.
  • Embedding a type only to reuse one method, which also exposes all of its other exported fields and methods. A named field (log Logger) keeps the API of the outer type small.
  • Embedding two types that both have a method with the same name, then calling it on the outer type — the call is ambiguous and won't compile unless the outer type defines its own method.

Practice

Create an Animal struct with a Name field and a method Hello() string returning "Hi, I'm <Name>". Create a Dog struct that embeds Animal and adds a Breed field. Print d.Hello() and d.Breed.

Show solution
package main

import "fmt"

type Animal struct {
    Name string
}

func (a Animal) Hello() string {
    return "Hi, I'm " + a.Name
}

type Dog struct {
    Animal
    Breed string
}

func main() {
    d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Beagle"}
    fmt.Println(d.Hello()) // Hi, I'm Rex
    fmt.Println(d.Breed)   // Beagle
}