Embedding and composition

Go · Advanced · 12 min

Go has no inheritance; embedding is how you reuse behaviour and build bigger interfaces from small ones.

A struct can embed another type by listing it without a field name:

type Logger struct{ prefix string }
func (l *Logger) Log(msg string) { fmt.Println(l.prefix + msg) }

type Service struct {
	*Logger
	name string
}

The embedded type's fields and methods are promoted: svc.Log("hi") works as if Service declared Log, and Service now satisfies any interface that Log helps satisfy. The embedded value is still a normal field, named after its type: svc.Logger.

This is composition, not inheritance:

  • Promoted methods run with the embedded value as receiver. Inside Log, l is the *Logger, and it knows nothing about the outer Service. There's no virtual dispatch back to the outer type.
  • An outer method or field with the same name shadows the promoted one; you can still call svc.Logger.Log explicitly.
  • If two embedded types at the same depth provide the same name, using that name is a compile error (ambiguous selector) — but only when you actually use it.

Interfaces embed interfaces to build larger contracts: io.ReadWriter is interface{ Reader; Writer }.

Embedding an interface in a struct is a useful trick for wrapping: the struct satisfies the whole interface, and you override only the methods you care about. It's common in tests and decorators, but calling a non-overridden method on a nil embedded interface panics.

Be careful embedding types in exported structs: every promoted method becomes part of your public API — including embedding sync.Mutex, which exports Lock and Unlock.

Example

package main

import (
	"fmt"
	"io"
	"strings"
)

// countingReader wraps any io.Reader and overrides only Read.
type countingReader struct {
	io.Reader // embedded interface
	n         int
}

func (c *countingReader) Read(p []byte) (int, error) {
	n, err := c.Reader.Read(p) // call the wrapped reader explicitly
	c.n += n
	return n, err
}

func main() {
	cr := &countingReader{Reader: strings.NewReader("hello, embedding")}
	data, _ := io.ReadAll(cr)
	fmt.Println(string(data), cr.n) // hello, embedding 16
}

Remember

  • Embedded fields' methods are promoted to the outer type
  • Promoted methods see only the inner value — no virtual dispatch
  • Embed interfaces to compose contracts or to wrap and override

Watch out for

  • Embedding a type in an exported struct and accidentally exposing its methods (e.g. Lock/Unlock) as API.

Check yourself

  1. Service embeds *Logger whose Log method calls l.Prefix(). Service also defines its own Prefix(). Which Prefix runs when you call svc.Log()?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: go.dev/doc/effective_go#embedding