Method and Field Promotion
How embedded struct fields and methods become directly accessible on the outer struct type through Go's promotion rules
When you embed a struct inside another, Go does something quietly useful: it makes the embedded type's fields and methods available directly on the outer type, as if the outer type had declared them itself. A field like Name on an embedded Person struct can be accessed as employee.Name without writing employee.Person.Name. This mechanism is called promotion, and it is the reason embedding feels like a lightweight form of code reuse rather than just a nested data structure.
Promotion is not inheritance. The embedded type remains a distinct, accessible field. You can always reach it explicitly. Promotion is a selector shorthand—one that the compiler resolves according to a specific set of rules—and understanding those rules is what separates confident use of embedding from guesswork.
Field Promotion
Embed a struct, and its fields become accessible through the outer struct directly. The compiler resolves the shorthand by walking through embedded fields until it finds a match.
package main
import "fmt"
type Address struct {
Street string
City string
Zip string
}
type Customer struct {
Name string
Address
}
func main() {
c := Customer{
Name: "Maria Chen",
Address: Address{
Street: "742 Evergreen Terrace",
City: "Springfield",
Zip: "62701",
},
}
// Promoted field access — no need for c.Address.City
fmt.Println(c.City)
fmt.Println(c.Zip)
// The explicit path still works
fmt.Println(c.Address.Street)
}
Run this and c.City prints Springfield. The City field belongs to Address, but the compiler finds it by looking inside the embedded Address field and promotes it to Customer. The explicit path c.Address.Street remains available—promotion adds a shortcut, it does not remove the original route.
The trade-off is in struct literals. You cannot initialize promoted fields directly at the outer level. This does not compile:
c := Customer{
Name: "Maria Chen",
City: "Springfield", // compile error: unknown field City
}
Promoted Fields in Struct Literals:
Promoted fields cannot appear as keys in a composite literal of the embedding struct. You must initialize the embedded struct as a whole, using its type name as the key, as shown in the working example above. Forgetting this is one of the most frequent compiler errors beginners encounter with embedding.
Method Promotion
Methods promote the same way fields do. If the embedded type has a method, you can call it directly on the outer type. The method still receives the embedded type as its receiver—not the outer type—which is a critical distinction from classical inheritance.
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.Prefix, msg)
}
type Service struct {
Name string
Logger
}
func main() {
svc := Service{
Name: "AuthService",
Logger: Logger{Prefix: "AUTH"},
}
// Promoted method call
svc.Log("starting up")
// Equivalent explicit call
svc.Logger.Log("starting up")
}
Both calls print [AUTH] starting up. The Log method is defined on Logger, but Service can call it as if it owned the method. Internally, the receiver is still the Logger value—l.Prefix inside Log refers to svc.Logger.Prefix, not something on Service. The outer type's Name field is invisible to the promoted method.
The Receiver Does Not Change:
A promoted method always receives the embedded value as its receiver, never the outer struct. If Log needed access to Service.Name, embedding would not help—you would need to write an explicit method on Service that calls Logger.Log and adds the extra context. This is by design and is the single most important mental model shift when moving from inheritance-based languages to Go.
Depth and the Shallowest-Match Resolution Rule
Embedding can be nested. A struct can embed a struct that itself embeds another. When you write a selector like x.Name, the compiler searches through the embedding tree, and the depth of a field or method—how many embedding levels the compiler must traverse to reach it—determines which one wins.
A direct field or method on the outer type has depth 0. A field reached through one embedded type has depth 1. Through two levels of embedding, depth 2, and so on. The rule is simple: the shallowest match wins. If the compiler finds a match at depth 1, it stops looking deeper.
package main
import "fmt"
type Base struct {
Version string
}
type Middle struct {
Base
Version int // shadows Base.Version at depth 1
}
type Outer struct {
Middle
}
func main() {
o := Outer{
Middle: Middle{
Base: Base{Version: "1.0.0"},
Version: 42,
},
}
fmt.Println(o.Version) // prints 42, not "1.0.0"
fmt.Println(o.Base.Version) // prints "1.0.0"
}
o.Version resolves to Middle.Version (depth 1) rather than Base.Version (depth 2). The shallowest match is Middle.Version, an int with value 42. The string "1.0.0" on Base.Version is still reachable, but only through the explicit path o.Base.Version.
The compiler applies this rule independently for every selector. It does not compare types or try to guess which field you meant—it purely uses depth. If two candidates exist at the same depth, you get a compile error.
Resolution Is Deterministic:
The shallowest-match rule means promotion resolution is fully deterministic. Given the same type definitions and the same selector, the compiler will always pick the same field or method. There is no runtime ambiguity and no virtual dispatch—promotion is resolved entirely at compile time.
Shadowing
When the outer struct defines a field or method with the same name as a promoted one, the outer definition shadows the promoted one. This is just the shallowest-match rule applied to depth 0: a direct declaration always beats anything from an embedded type.
package main
import "fmt"
type Engine struct {
Horsepower int
}
func (e Engine) Start() string {
return "engine rumbling"
}
type ElectricCar struct {
Engine
BatteryKWh int
}
// Shadows the promoted Engine.Start
func (e ElectricCar) Start() string {
return "silent start"
}
func main() {
car := ElectricCar{
Engine: Engine{Horsepower: 180},
BatteryKWh: 75,
}
fmt.Println(car.Start()) // "silent start"
fmt.Println(car.Engine.Start()) // "engine rumbling"
}
Shadowing is not overriding. Both methods still exist. car.Start() calls the ElectricCar version. car.Engine.Start() calls the original. In classical inheritance, the base class method might be completely replaced or require an explicit super call. In Go, the promoted method is simply hidden behind a shallower declaration and remains fully accessible through the embedded type name.
This has practical implications for design. If you want the outer type to extend rather than replace the embedded behavior, write the outer method to call the embedded one explicitly:
func (e ElectricCar) Start() string {
return fmt.Sprintf("%s — and then silence", e.Engine.Start())
}
Shadowing Can Hide Behavior Unintentionally:
If you add a field or method to an outer struct without realizing an embedded type already has that name, you silently shadow the promoted version. Callers who previously relied on the promoted behavior will now get the outer type's version instead. This compiles without warning. When adding fields or methods to a struct that embeds other types, check for name collisions.
Ambiguous Selectors
If two embedded types at the same depth both provide a field or method with the same name, the compiler cannot choose between them. It refuses to guess and produces an error.
package main
type UIComponent struct {
ID string
}
func (u UIComponent) Render() string {
return "<component>"
}
type DataBinding struct {
ID string
}
func (d DataBinding) Render() string {
return "<binding>"
}
type View struct {
UIComponent
DataBinding
}
func main() {
v := View{
UIComponent: UIComponent{ID: "btn-submit"},
DataBinding: DataBinding{ID: "user.email"},
}
// v.ID — compile error: ambiguous selector v.ID
// v.Render() — compile error: ambiguous selector v.Render
_ = v
}
Both UIComponent and DataBinding are embedded at depth 1 in View, and both contribute an ID field and a Render method. The compiler sees two equally valid resolutions and stops with ambiguous selector. The fix is to be explicit: v.UIComponent.ID or v.DataBinding.ID.
Ambiguity Is Always a Compile Error:
Go never silently resolves ambiguity between embedded types. Even if the two fields have different types, or if one method seems more relevant than another, the compiler treats any same-depth collision as a fatal error. The only resolution is to use the full explicit path through the embedded type name.
The ambiguity check applies even if you never actually use the conflicting name. The mere existence of the ambiguity in the type definition is enough—the compiler will reject any selector that would be ambiguous, even if the code that triggers it is in a different file or package.
Pointer Receivers and Promotion
Methods with pointer receivers promote, but with an important constraint: the outer value must be addressable for the promoted method to be callable. If you embed a type with pointer-receiver methods and try to call one on a non-addressable outer value, the compiler rejects it.
package main
import "fmt"
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func (c Counter) Value() int {
return c.count
}
type Metrics struct {
Label string
Counter
}
func main() {
m := Metrics{Label: "requests"}
m.Increment() // OK: m is addressable
m.Increment()
fmt.Println(m.Value()) // prints 2
// But a non-addressable value fails:
// getMetrics().Increment() — would not compile
}
func getMetrics() Metrics {
return Metrics{Label: "errors"}
}
The Increment method has a pointer receiver on *Counter. When you call m.Increment() on a local variable m, Go can take its address automatically because m is addressable. But getMetrics() returns a value that is not stored in a variable—it is temporary and has no address. Calling getMetrics().Increment() produces a compile error because Go cannot take the address of the return value to satisfy the pointer receiver.
The same issue appears with map indexing, function return values used inline, and other non-addressable expressions. If a promoted method requires a pointer receiver, the outer value must exist in a variable or be explicitly converted to a pointer first.
Embedding a Pointer Type Changes the Rules:
When you embed a pointer type directly—for example, type Metrics struct { *Counter }—the method sets of both Counter and *Counter are promoted to both Metrics and *Metrics. However, you must ensure the embedded pointer is not nil before calling promoted methods, or the call will panic at runtime.
Interface Satisfaction Through Promoted Methods
Promoted methods count toward interface satisfaction. If an embedded type provides all the methods an interface requires, the outer type automatically satisfies that interface. You do not need to write forwarding methods by hand.
package main
import (
"fmt"
"io"
"strings"
)
// CountingWriter wraps an io.Writer and tracks bytes written.
type CountingWriter struct {
io.Writer
Written int64
}
// Write overrides the promoted method to add counting behavior.
func (cw *CountingWriter) Write(p []byte) (int, error) {
n, err := cw.Writer.Write(p)
cw.Written += int64(n)
return n, err
}
func main() {
var buf strings.Builder
cw := &CountingWriter{Writer: &buf}
fmt.Fprintln(cw, "hello")
fmt.Fprintln(cw, "world")
fmt.Printf("bytes written: %d\n", cw.Written)
fmt.Printf("content: %s\n", buf.String())
}
CountingWriter embeds io.Writer, which is an interface. By embedding it, CountingWriter automatically satisfies io.Writer through the promoted Write method of whatever concrete writer is stored in the field. The outer type then shadows that promoted method with its own Write to add byte counting, while still delegating the actual writing to the embedded writer.
This pattern—embed an interface, shadow one or more of its methods to add behavior, and let the rest promote through—appears throughout the Go standard library. bufio.ReadWriter embeds *Reader and *Writer to compose an io.ReadWriter without a single forwarding method. The context package's timerCtx embeds cancelCtx and only declares the one method (Deadline) that cancelCtx does not provide.
Promotion Enables Interface Composition:
If your type embeds a struct that satisfies an interface, your type satisfies that interface automatically. This is how Go achieves polymorphic behavior through composition: you build larger interfaces by assembling smaller ones, and you build concrete types that satisfy them by embedding types that already implement the pieces. There is no implements keyword and no explicit declaration of intent—satisfaction is structural and promotion is part of the structure.
Common Mistakes
Several pitfalls recur when developers first use promotion. Recognizing them early avoids hours of debugging.
Initializing promoted fields in literals. As noted earlier, you cannot write Customer{City: "NYC"} if City comes from an embedded Address. Use Customer{Address: Address{City: "NYC"}} instead. The compiler error is clear once you know to expect it.
Assuming the receiver changes. A promoted method operates on the embedded value, not the outer struct. If Logger.Log formats a message with l.Prefix, adding a Prefix field to the outer Service struct does nothing—the method still reads from the embedded Logger. This is not a bug but a design constraint that surprises people coming from languages where this or self always refers to the outermost type.
Embedding without intention. Not every struct included inside another should be embedded. Embedding says "this outer type is-a or behaves-like-a inner type." If the relationship is purely "has-a"—a Server has a Config, but a Server is not a Config—use a named field instead. Named fields prevent unwanted promotion and keep the API surface of the outer type clean.
Nil embedded pointers. Embedding a pointer type (*Config) means the field can be nil. Calling a promoted method on a nil embedded pointer panics. Always check for nil before use, or initialize the embedded pointer in a constructor function.
Ambiguity with large embedding trees. When a struct embeds multiple types, each of which embeds others, the set of promoted names can become large and collisions hard to spot. A new method added to a deeply embedded type in a dependency can suddenly create an ambiguity in your code. Keep embedding hierarchies shallow and deliberate.
Promotion Happens Across Packages:
Promoted fields and methods from exported embedded types are visible to all callers, including code in other packages. If you embed a type from a third-party library, every exported field and method it declares becomes part of your type's public API through promotion. Upgrading that dependency could introduce new promoted names that collide with your own fields or methods. Audit the exported surface of any type you embed from an external package.
Summary
Promotion is the mechanism that makes embedding in Go feel like code reuse rather than mere nesting. When a struct embeds another, the embedded type's exported fields and methods appear directly on the outer type. The compiler resolves selectors by searching through embedding levels and picking the shallowest match. A direct declaration always shadows a promoted one. Two promoted names at the same depth produce a compile-time ambiguity.
The mental model that serves most Go developers best: think of promotion as the compiler generating implicit forwarding for every exported field and method of the embedded type. The forwarding points to the embedded value as the receiver, never the outer struct. The embedded type remains a named, accessible field—promotion adds a shortcut without removing the original path.
What promotion unlocks is the ability to compose types that satisfy interfaces without writing boilerplate. An outer type that embeds an io.Reader is an io.Reader. An outer type that embeds a sync.Mutex gains Lock and Unlock in its method set. This is the foundation of Go's answer to polymorphism: not inheritance trees, but assembled capabilities.
If you are deciding whether to embed or use a named field, the rule of thumb is straightforward: embed when the outer type genuinely exposes the inner type's behavior as part of its own identity; use a named field when the inner type is an implementation detail that callers should not see.