Struct embedding as composition

Go favors composition over inheritance, and embedding is the language feature that makes composition ergonomic. There are three forms of embedding in Go: structs in structs, interfaces in interfaces, and interfaces in structs. This post covers the first form, with examples drawn mainly from the standard library.

Basics of struct-in-struct embedding

Declaring an embedded struct field is done by omitting the field name:

type Base struct {
  b int
}

type Container struct {     // Container is the embedding struct
  Base                      // Base is the embedded struct
  c string
}

The embedded struct's fields are promoted to the outer struct. Given the declaration above, an instance of Container has a b field accessible directly as co.b, just like co.c:

co := Container{}
co.b = 1
co.c = "string"
fmt.Printf("co -> {b: %v, c: %v}\n", co.b, co.c)

Composite literals are an exception: you initialize the embedded struct as a whole, not its individual fields. Promoted fields cannot be used as keys in a struct literal:

co := Container{Base: Base{b: 10}, c: "foo"}
fmt.Printf("co -> {b: %v, c: %v}\n", co.b, co.c)

The co.b syntax is only convenience; co.Base.b is always available as the explicit equivalent.

Method promotion and receiver semantics

Methods on an embedded struct are promoted too. If Base has a method:

func (base Base) Describe() string {
  return fmt.Sprintf("base %d belongs to us", base.b)
}

Then Container instances can call it:

fmt.Println(cc.Describe())

It helps to think of this as if Container had an explicit Base field and a forwarding method:

type Container struct {
  base Base
  c string
}

func (cont Container) Describe() string {
  return cont.base.Describe()
}

There is a crucial difference from inheritance in languages like Python or C++. When the promoted method runs, its receiver is the embedded Base value — not the outer Container. The method has no knowledge of the struct it was called through, which is a fundamental distinction between Go's embedding and classical inheritance.

Shadowing

If the outer struct declares a field with the same name as a field in the embedded struct, the outer field wins when accessed through the outer struct. The embedded field is shadowed, not hidden entirely.

type Base struct {
  b   int
  tag string
}

func (base Base) DescribeTag() string {
  return fmt.Sprintf("Base tag is %s", base.tag)
}

type Container struct {
  Base
  c   string
  tag string
}

func (co Container) DescribeTag() string {
  return fmt.Sprintf("Container tag is %s", co.tag)
}

With this setup:

b := Base{b: 10, tag: "b's tag"}
co := Container{Base: b, c: "foo", tag: "co's tag"}

fmt.Println(b.DescribeTag())
fmt.Println(co.DescribeTag())

Output:

Base tag is b's tag
Container tag is co's tag

In this case, co.tag refers to Container's own tag field. The shadowed value is still reachable via co.Base.tag.

Example: sync.Mutex embedding

A classic standard library pattern is embedding sync.Mutex so that the lock methods become part of the struct's public API. In crypto/tls/common.go, lruSessionCache is declared as:

type lruSessionCache struct {
  sync.Mutex
  m        map[string]*list.Element
  q        *list.List
  capacity int
}

With this embedding, callers can invoke cache.Lock() and cache.Unlock() directly using the promoted methods. This removes the need for explicit forwarding methods and is appropriate when locking is part of the public contract. If the mutex is only for internal synchronization and not part of the user-facing API, an unexported field like mu sync.Mutex is the better choice.

Example: ELF file headers

Embedding is not only for behavior. In debug/elf/file.go, structs describing ELF files use embedding for data organization:

// A FileHeader represents an ELF file header.
type FileHeader struct {
  Class      Class
  Data       Data
  Version    Version
  OSABI      OSABI
  ABIVersion uint8
  ByteOrder  binary.ByteOrder
  Type       Type
  Machine    Machine
  Entry      uint64
}

// A File represents an open ELF file.
type File struct {
  FileHeader
  Sections  []*Section
  Progs     []*Prog
  closer    io.Closer
  gnuNeed   []verneed
  gnuVersym []byte
}

Rather than listing all header fields directly in File, the package keeps them in a separate struct and embeds it. This self-documents the data layout and allows FileHeader values to be initialized and manipulated independently. The same design appears in compress/gzip, where both gzip.Reader and gzip.Writer embed gzip.Header, avoiding duplication of header-related fields.

Example: implementing interfaces

Embedding gives a struct an easy path to interface satisfaction by promoting all methods of the embedded type. The bufio package's ReadWriter is a minimal demonstration:

type ReadWriter struct {
  *Reader
  *Writer
}

Because *bufio.Reader implements io.Reader and *bufio.Writer implements io.Writer, the embedded struct automatically satisfies io.ReadWriter — no named fields, no forwarding stubs.

A more involved example is timerCtx from the context package:

type timerCtx struct {
  cancelCtx
  timer *time.Timer

  deadline time.Time
}

timerCtx embeds cancelCtx, which already provides three of the four methods required by the Context interface (Done, Err, and Value). The fourth, Deadline, is implemented directly on timerCtx. The embedding handles the interface's breadth while letting the type customize only what it needs.