Struct Embedding
Go doesn’t have classical inheritance the way Java or C++ do — there’s no extends keyword and no class hierarchy to climb. Instead, Go gives you struct embedding: placing one type inside another without giving it a field name, so the outer type automatically gains access to the inner type’s fields and methods. This is Go’s primary mechanism for code reuse, and it shows up constantly in the standard library and in idiomatic Go code, so understanding exactly what it does — and what it deliberately does not do — is essential.
Overview: How Struct Embedding Works
When you declare a field in a struct using only a type name, with no identifier in front of it, that field is called an anonymous field, and the struct is said to embed that type. The field’s implicit name is the type’s own name (its unqualified name, ignoring any package prefix), so you can still refer to it explicitly when you need to.
type Engine struct {
Horsepower int
}
type Car struct {
Engine // embedded field, implicit name "Engine"
Model string
}
Here Car embeds Engine. Every exported (and unexported, within the same package) field and method of Engine becomes accessible directly on a Car value — this is called promotion. So car.Horsepower works even though Car never declared a Horsepower field itself; the compiler rewrites it as car.Engine.Horsepower behind the scenes. The same happens for methods: if Engine had a method Start(), then car.Start() would call car.Engine.Start() automatically.
It’s important to be precise about what’s happening under the hood, because it’s easy to mistake this for inheritance. A Car is not an Engine — there’s no type hierarchy and no is-a relationship enforced by the compiler. Car simply contains an Engine value as one of its fields, and the compiler generates convenience selectors so you don’t have to type car.Engine.Horsepower every time. This is composition, dressed up with syntax that makes the common case terse. The distinction matters most when you consider method dispatch: a method defined on Engine only ever sees Engine‘s own methods, never any method that Car later defines with the same name. There is no virtual dispatch — we’ll see exactly why that trips people up in the Common Mistakes section.
You can embed:
- A struct type, by value (
Engine) — the embedded struct is copied into the outer struct’s memory layout. - A pointer to a struct type (
*Engine) — the outer struct stores a pointer, so the embedded value can be shared, can benil, and mutations through promoted pointer-receiver methods are visible to anyone else holding the same pointer. - An interface type — the outer struct promotes whatever method set the interface defines, and calling a promoted method dispatches to whatever concrete value is currently stored in that interface field at runtime. This is how types like
bufio.ReadWriterare built by embedding a reader and a writer.
Field and method promotion is resolved at compile time using a shallowest-match-wins rule: Go looks at the outer struct’s own fields and methods first, then one level of embedding, then two levels, and so on, stopping at the first depth where it finds a unique match. If two embedded types at the same depth both have a field or method with the same name, and you try to use it without qualifying which one you mean, the compiler refuses to guess — you get an ambiguous selector error, and you must disambiguate explicitly.
Syntax
type Outer struct {
Inner // embedded value type -- field name is "Inner"
*PointerType // embedded pointer type -- field name is "PointerType"
SomeInterface // embedded interface -- field name is "SomeInterface"
RegularField Type // an ordinary, named field for comparison
}
| Form | Meaning |
|---|---|
Inner |
Embeds a struct (or any named type) by value; fields/methods are promoted. |
*Inner |
Embeds a pointer to a struct; the field can be nil, and calling a promoted method through a nil pointer panics unless the method is written to tolerate a nil receiver. |
SomeInterface |
Embeds an interface type; the outer type gets that interface’s method set promoted, dispatched dynamically to whatever concrete value the field holds. |
Outer{}.Inner |
The explicit, always-valid way to reach the embedded field, useful for disambiguation or initialization. |
Examples
Example 1: Basic field and method promotion
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Describe() string {
return "Animal: " + a.Name
}
type Dog struct {
Animal
Breed string
}
func main() {
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name)
fmt.Println(d.Describe())
fmt.Println(d.Breed)
}
Output:
Rex
Animal: Rex
Labrador
Notice the struct literal: even though Name is promoted, you still initialize the embedded field using its type name as the key (Animal: Animal{Name: "Rex"}) — promotion only affects how you read fields and call methods afterward, not how you build the literal. Reading d.Name and calling d.Describe() both work directly on the Dog value because Dog embeds Animal at depth one.
Example 2: Shadowing a promoted method
package main
import "fmt"
type Base struct {
ID int
}
func (b Base) Describe() string {
return fmt.Sprintf("Base#%d", b.ID)
}
type Wrapper struct {
Base
Label string
}
func (w Wrapper) Describe() string {
return fmt.Sprintf("%s (%s)", w.Base.Describe(), w.Label)
}
func main() {
w := Wrapper{Base: Base{ID: 42}, Label: "special"}
fmt.Println(w.Describe())
fmt.Println(w.Base.Describe())
}
Output:
Base#42 (special)
Base#42
Because Wrapper declares its own Describe method, that method exists at depth zero and wins over the promoted Base.Describe at depth one — this is exactly the shallowest-match-wins rule. Wrapper.Describe can still reach the shadowed method explicitly through w.Base.Describe(), which is the standard way to build on top of an embedded type’s behavior instead of only replacing it.
Example 3: Embedding a pointer for shared, mutable state
package main
import "fmt"
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func (c *Counter) Value() int {
return c.count
}
type Stats struct {
*Counter
Name string
}
func main() {
s := Stats{Counter: &Counter{}, Name: "hits"}
s.Increment()
s.Increment()
s.Increment()
fmt.Println(s.Name, s.Value())
}
Output:
hits 3
Stats embeds *Counter, a pointer. The promoted Increment and Value methods have pointer receivers, so calling s.Increment() is shorthand for s.Counter.Increment(), which mutates the single Counter that s.Counter points to. If you shared that same *Counter pointer with another struct, both would see each other’s increments — embedding a pointer, unlike embedding a value, means the embedded state is shared, not copied.
How It Works Step by Step
When the compiler sees a selector expression like s.Value(), it performs this resolution:
- Look for a field or method literally named
Valuedeclared directly onStats. None exists. - Descend one level into each embedded field (
*Counterhere) and look forValuethere.Counterhas aValue()method, so it’s found at depth one. - If more than one embedded type at the same depth had matched, the compiler would stop and report an ambiguous selector instead of guessing — you’d have to write
s.Counter.Value()explicitly. - Once resolved, the compiler rewrites the call as
s.Counter.Value()and, sinceValuehas a pointer receiver, uses the pointer stored ins.Counterdirectly.
This resolution happens entirely at compile time — there’s no runtime lookup table walking a class hierarchy. That’s also why a method defined on the embedded type can never call back into an overriding method defined on the outer type: at the point Counter.Increment is compiled, the compiler only knows about Counter; it has no idea Stats even exists, let alone that Stats might someday embed it.
Common Mistakes
Mistake 1: Ambiguous selectors from same-depth embedding
If two embedded types at the same depth both define a field or method with the same name, referencing it without qualification is a compile error:
type A struct {
Name string
}
type B struct {
Name string
}
type C struct {
A
B
}
func main() {
c := C{A: A{Name: "a"}, B: B{Name: "b"}}
fmt.Println(c.Name) // compile error: ambiguous selector c.Name
}
The fix is to qualify the selector with the embedded type’s name so the compiler knows exactly which one you mean:
package main
import "fmt"
type A struct {
Name string
}
type B struct {
Name string
}
type C struct {
A
B
}
func main() {
c := C{A: A{Name: "a"}, B: B{Name: "b"}}
fmt.Println(c.A.Name, c.B.Name)
}
Output:
a b
Mistake 2: Expecting virtual dispatch like in Java or C#
This is the mistake that catches people coming from class-based languages hardest. It compiles fine, but doesn’t do what you’d expect:
package main
import "fmt"
type Base struct{}
func (b Base) Name() string {
return "Base"
}
func (b Base) Greet() string {
return "Hello, " + b.Name()
}
type Outer struct {
Base
}
func (o Outer) Name() string {
return "Outer"
}
func main() {
o := Outer{}
fmt.Println(o.Greet())
}
Output:
Hello, Base
A Java or C# programmer would expect Greet to call the overriding Outer.Name, printing “Hello, Outer” — but Go embedding is composition, not inheritance, and there’s no dynamic dispatch through it. Base.Greet was compiled knowing only about Base; it calls Base.Name, full stop, regardless of what any struct embedding Base later defines. The fix is to not rely on this pattern: put the method that needs the current type’s behavior directly on the outer type, and have it call the pieces it needs explicitly.
package main
import "fmt"
type Base struct{}
func (b Base) Name() string {
return "Base"
}
func (b Base) greetWith(name string) string {
return "Hello, " + name
}
type Outer struct {
Base
}
func (o Outer) Name() string {
return "Outer"
}
func (o Outer) Greet() string {
return o.greetWith(o.Name())
}
func main() {
o := Outer{}
fmt.Println(o.Greet())
}
Output:
Hello, Outer
Outer now owns Greet, so it resolves at depth zero and its body calls o.Name(), which correctly picks up Outer‘s own method. If you genuinely need runtime-polymorphic behavior, model it with an interface and a field of that interface type instead of leaning on struct embedding to fake it.
Best Practices
- Use embedding to reuse behavior and data through composition, not to simulate a class hierarchy — think has-a with convenience, not is-a.
- When you embed two or more types that might share a field or method name, be ready to disambiguate with
outer.TypeName.Field; don’t rely on only one of them ever adding that name. - Prefer a regular, named field over embedding when you don’t actually want the inner type’s whole method set promoted onto the outer type’s public API — embedding is not just for saving keystrokes.
- Initialize pointer-typed embedded fields before calling their promoted methods; a
nilembedded pointer used through a promoted pointer-receiver method panics just like any othernilpointer dereference. - Embed interfaces to compose small interfaces into bigger ones, rather than redeclaring every method signature by hand.
- Keep embedding shallow. Embedding a struct that itself embeds another struct multiple levels deep makes promoted-method resolution and error messages harder to follow — favor one level whenever possible.
- Document embedded fields in exported types; because promoted members don’t show up as ordinary field declarations, readers and tools benefit from a short comment explaining what’s being reused and why.
Practice Exercises
- Define a
Personstruct withName stringandAge int, then anEmployeestruct that embedsPersonand addsSalary float64. Construct anEmployee, and print the promotedNamefield together withSalaryin onefmt.Printlncall. - Create two structs,
LoggerandCache, each with its ownReport() stringmethod. Embed both into aServicestruct and try callingservice.Report()— note the compiler error you get, then fix it by callingservice.Logger.Report()andservice.Cache.Report()explicitly. - Write a
Boxstruct that embeds*Counter(reusing theCountertype from Example 3). Create twoBoxvalues that share the same*Counterpointer and confirm, by incrementing through one and reading through the other, that the count is shared — then create a thirdBoxwith its own fresh*Counterand confirm its count stays independent.
Summary
- Struct embedding places one type inside another as an anonymous field, whose implicit name is the type’s own name.
- Embedded fields and methods are promoted onto the outer type, so you can call them directly without an extra selector — but you can always fall back to the qualified form (
outer.Inner.Field). - Embedding is composition, not inheritance: there’s no is-a relationship and, critically, no virtual or dynamic dispatch when an embedded type’s method calls another method on itself.
- You can embed a value, a pointer, or an interface; pointer and interface embedding allow shared or dynamically-dispatched state, while value embedding copies the inner struct into the outer one.
- Promotion resolves by depth at compile time — the shallowest unique match wins, and same-depth conflicts between two embedded types produce an ambiguous-selector compile error that you resolve by qualifying the selector explicitly.
- Use embedding deliberately for genuine code reuse, keep it shallow, and reach for interfaces (not embedding tricks) when you actually need runtime polymorphism.
