Embedded Fields
How to embed struct types inside other structs in Go, what field promotion means, and when to use embedding instead of named fields
Go rejects class-based inheritance entirely, which means there is no extends keyword and no parent-child type hierarchy. The language replaces inheritance with a mechanism called embedding — the ability to include one struct type inside another without giving it an explicit field name. An embedded field sits inside the outer struct like any other field but the compiler promotes its own fields and methods up to the outer type as if they belonged there directly.
This promotion is what makes embedding feel unfamiliar at first. A field you never declared on the outer struct suddenly appears accessible through it.
What an Embedded Field Looks Like
In ordinary struct composition, every field has a name and a type. Embedding removes the name.
package main
import "fmt"
type Address struct {
Street string
City string
}
// Named field — this is NOT embedding
type CustomerWithNamedField struct {
addr Address
Name string
}
// Embedded field — no field name, only the type
type CustomerWithEmbedding struct {
Address
Name string
}
func main() {
c := CustomerWithEmbedding{
Address: Address{Street: "10 Downing St", City: "London"},
Name: "Alice",
}
// Promoted field: City is accessed directly on CustomerWithEmbedding
fmt.Println(c.City) // "London"
// The full path still works
fmt.Println(c.Address.City) // "London"
}
The line Address inside CustomerWithEmbedding is an embedded field declaration. Go automatically gives it the implicit name Address — the unqualified type name becomes the field identifier. But the fields inside Address (like Street and City) become reachable from CustomerWithEmbedding values without stepping through Address each time.
In the CustomerWithNamedField version, c.addr.City is the only path. There is no c.City. The difference is not cosmetic: embedded fields change which interfaces the outer type satisfies and which methods callers can invoke. Named fields do neither.
Why the Language Includes Embedding
The designers of Go observed that large inheritance hierarchies create tight coupling and surprise behavior. A method added to a base class five levels up can silently alter the behavior of dozens of subclasses. Composition — building types out of smaller, independent pieces — avoids that coupling but introduces a different friction: every method you want to reuse must be manually forwarded.
type Logger struct { /* ... */ }
func (l Logger) Info(msg string) { /* ... */ }
type Server struct {
log Logger
}
// Without embedding, every delegated method is boilerplate
func (s Server) Info(msg string) {
s.log.Info(msg)
}
As the embedded type grows more methods, this forwarding code multiplies. It is repetitive, error-prone, and makes refactoring painful — if Logger gains a Warn method, every type that wraps a Logger must be updated to expose it.
Embedding removes that boilerplate. When you embed Logger inside Server, every exported method and field on Logger automatically becomes available through Server. The compiler does the forwarding for you.
Embedding is not inheritance:
A Server value with an embedded Logger is not a Logger value. You cannot pass a Server to a function that expects a Logger. Embedding gives the outer type the embedded type's abilities, not its identity. This distinction is the single most important mental shift for programmers coming from Java or C++.
How Field Promotion Actually Works
When you write c.City on a CustomerWithEmbedding value, the compiler performs a lookup that follows a specific algorithm. It first checks whether the outer struct declares a field named City. If it does not, it descends into each embedded struct (in declaration order) and checks again. When it finds a match, it resolves the selector to that promoted field.
The full chain of selectors is always valid. c.Address.City is the explicit path. c.City is a shorter form that the compiler accepts because there is exactly one City reachable through the embedding tree.
This promotion also works through multiple levels of embedding.
type Country struct {
Name string
}
type Address struct {
Country
City string
}
type Person struct {
Address
Name string
}
func main() {
p := Person{
Address: Address{
Country: Country{Name: "France"},
City: "Paris",
},
Name: "Marie",
}
fmt.Println(p.Name) // "Marie" (own field)
fmt.Println(p.City) // "Paris" (promoted from Address)
fmt.Println(p.Country.Name) // explicit path still works
// But can we access the country name directly?
// p.Name is ambiguous — it matches both Person.Name and Country.Name
}
Ambiguous selectors stop compilation:
The last line would not compile. Name exists on both Person and Country, so the compiler cannot decide which one p.Name refers to. You must use the explicit path p.Address.Country.Name or p.Name (which resolves to the outer field). The compiler does not guess; it reports an error.
Beginners can think of promotion as the compiler automatically writing accessor methods. When you embed Address, the compiler effectively adds func (c Customer) City() string { return c.Address.City }. The field is still stored inside the Address value — promotion changes only how you reach it, not where it lives.
Embedded Fields and Struct Literals
Promotion changes field access but not initialization syntax. In a struct literal, you cannot set promoted fields directly — you must initialize the embedded type as a whole.
type Config struct {
Host string
Port int
}
type Server struct {
Config
MaxConnections int
}
// WRONG: promoted fields cannot appear in the outer literal
// s := Server{Host: "localhost", Port: 8080, MaxConnections: 100}
// CORRECT: initialize the embedded struct explicitly
s := Server{
Config: Config{Host: "localhost", Port: 8080},
MaxConnections: 100,
}
This rule prevents ambiguity when the outer struct and the embedded struct share field names. If both Server and Config had a Host field, a literal that set Host without specifying which one would be impossible to resolve. Requiring the embedded struct to be initialized as a named unit eliminates that ambiguity.
Embedded struct literal syntax is a common stumbling block:
New Go programmers frequently try to initialize promoted fields directly in the outer literal. The compiler error message ("unknown field Host in struct literal") can be confusing because Host is accessible through s.Host after construction. The rule is: promotion applies to reads and method calls, never to literal initialization.
Embedding Pointers to Structs
An embedded field can be a pointer to a struct type. The promotion rules are identical — you access promoted fields and methods through the outer value without dereferencing the pointer explicitly.
type Engine struct {
Horsepower int
}
func (e *Engine) Start() {
fmt.Println("engine running")
}
type Car struct {
*Engine // embedded pointer
Model string
}
func main() {
c := Car{
Engine: &Engine{Horsepower: 250},
Model: "Sedan",
}
fmt.Println(c.Horsepower) // 250 (promoted through pointer)
c.Start() // "engine running" (method promoted through pointer)
}
Pointer embedding solves two problems. First, it lets multiple outer structs share the same embedded instance — change the engine on one car and all cars sharing that pointer see the update. Second, it allows the embedded value to be nil, which is useful when the embedded component is optional.
Nil embedded pointers cause panics:
If you embed a pointer and construct the outer struct without initializing it, the pointer is nil. Accessing a promoted field or method through a nil embedded pointer panics at runtime — the compiler does not warn you. Always check for nil before using pointer-embedded fields, or initialize them in a constructor function that guarantees non-nil values.
When to Embed and When to Name the Field
Embedding is not always the right choice. The decision hinges on whether the embedded type's interface should become part of the outer type's public surface.
Choose embedding when the embedded type's methods represent capabilities the outer type genuinely offers to its callers. A Server that embeds a sync.Mutex exposes Lock and Unlock because callers may need to synchronize access. A ReadWriter that embeds *bufio.Reader and *bufio.Writer exposes their methods because the point of ReadWriter is to be both a reader and a writer.
Choose a named field when the embedded type is an implementation detail. If the mutex is only used internally, name it mu sync.Mutex and do not export it. Callers should not lock your type's internal mutex — that is your responsibility. Naming the field keeps the type's public API clean and prevents callers from depending on details you may later change.
// EMBEDDING: the mutex is part of the public API
type SyncMap struct {
sync.Mutex
data map[string]string
}
// NAMED FIELD: the mutex is an internal detail
type Server struct {
mu sync.Mutex
data map[string]string
}
The same reasoning applies when an embedded type would introduce methods that make no sense on the outer type. Embedding time.Time inside an Event struct gives Event all of time.Time's methods, including MarshalJSON. If Event also has fields beyond the timestamp, the promoted MarshalJSON will serialize only the time portion, silently dropping the rest. That is a bug, not a feature.
A good embedding feels invisible:
When you choose the right embedded type, callers interact with the outer struct without needing to know which fields are promoted and which are native. The embedding should feel like the outer type genuinely has those capabilities, not like a shortcut around writing forwarding methods.
Real-World Patterns That Rely on Embedded Fields
Several patterns in the Go standard library and production codebases depend on embedding. Recognizing them helps you read existing code and apply them in your own.
Extending Standard Library Types
The sync.Mutex embedding shown earlier appears throughout the standard library. crypto/tls embeds sync.Mutex in session cache types. net/http embeds it in connection state. In each case the type genuinely is a mutex from the caller's perspective — locking it is part of using the type correctly.
Building Interface-Satisfying Composites
The bufio package defines ReadWriter by embedding a *Reader and a *Writer:
type ReadWriter struct {
*Reader
*Writer
}
This type satisfies io.ReadWriter without a single explicit method declaration. The embedded *Reader provides Read, the embedded *Writer provides Write, and together they satisfy the interface. No forwarding boilerplate is needed.
Shared Base Configuration
When multiple config types share common fields, embedding a base config struct avoids duplication.
type BaseConfig struct {
Debug bool
Timeout int
}
type ServerConfig struct {
BaseConfig
Port int
}
type ClientConfig struct {
BaseConfig
ServerURL string
}
Both ServerConfig and ClientConfig inherit Debug and Timeout without redeclaring them. A change to BaseConfig propagates to both types automatically.
Decorating with Observability
Embedding an interface inside a struct creates a decorator that intercepts method calls while preserving the original type's interface compliance.
type InstrumentedHandler struct {
http.Handler // embedded interface, not struct
requestCount int64
}
func (ih *InstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ih.requestCount++
ih.Handler.ServeHTTP(w, r) // delegate to the wrapped handler
}
InstrumentedHandler satisfies http.Handler because it embeds one. But it overrides ServeHTTP to add counting before delegating. Callers pass an InstrumentedHandler anywhere an http.Handler is expected, without knowing the counting is happening.
Common Mistakes to Avoid
Assuming embedding creates an "is-a" relationship. A Car with an embedded Engine is not an Engine. You cannot assign a Car to an Engine variable. Embedding composes capabilities; it does not create a type hierarchy.
Embedding types that implement interfaces you do not want promoted. Embedding time.Time promotes MarshalJSON, which can break JSON serialization of the outer struct. Before embedding, check which methods the embedded type exports and whether those methods make sense on your type.
Forgetting that nil embedded pointers panic. If you embed *SomeStruct, construct your outer value, and forget to initialize the embedded pointer, any access to promoted fields or methods will panic. Constructor functions that always initialize embedded pointers prevent this.
Embedding the same type twice through different paths. If A embeds B and C, and both B and C embed D, the compiler will flag ambiguous selectors for fields from D. The fix is to use the full path (e.g., a.B.D.field) explicitly.
Embedding for convenience when a named field is clearer. If the outer type has no reason to expose the embedded type's methods, use a named field instead. Future maintainers will thank you for keeping the API surface small.
Summary
Embedded fields are Go's primary mechanism for composing types without inheritance. They remove the boilerplate of manual method forwarding while keeping the embedded type as a distinct, accessible value — not a parent in a class hierarchy.
The most important operational rule is this: embed when the inner type's capabilities genuinely belong in the outer type's public interface. If callers should call Lock and Unlock on your type, embed sync.Mutex. If the mutex is your private business, name it and keep it hidden.