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.



