LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 2818 min read

Embedding & Composition

Learn struct embedding, Go's alternative to classical inheritance, and how promoted fields and methods let embedded types share behavior.

Introduction

Go has no class hierarchies and no "extends" keyword. Instead of inheritance, Go embraces composition — building complex types by combining simpler ones. Struct embedding is the language feature that makes this convenient, letting one struct include another and automatically gain access to its fields and methods.

What You Will Learn
  • Why Go favors composition over classical inheritance.
  • How to embed one struct inside another.
  • What promoted fields and methods are.
  • How to override a promoted method.
  • How embedding interacts with interface satisfaction.

Composition Over Inheritance

In languages with classical inheritance, a Dog class might "extend" an Animal class, inheriting its behavior and creating an "is-a" relationship. Go deliberately avoids this model. Instead, Go encourages "has-a" relationships: a struct can embed another struct as a field, gaining its behavior through composition rather than a rigid class hierarchy. This tends to produce flatter, more flexible designs that avoid the fragile-base-class problems that deep inheritance chains are prone to.

Embedding a Struct

To embed a struct, declare a field using just the type name, with no field name of its own. The embedded struct's fields and methods then become directly accessible on the outer struct.

package main
import "fmt"
type Engine struct {
Horsepower int
}
func (e Engine) Start() string {
return fmt.Sprintf("Engine starting with %d HP", e.Horsepower)
}
type Car struct {
Engine // embedded struct — no field name
Brand string
}
func main() {
c := Car{
Engine: Engine{Horsepower: 300},
Brand: "Speedster",
}
fmt.Println(c.Brand)
fmt.Println(c.Horsepower) // promoted field, accessed directly
fmt.Println(c.Start()) // promoted method, accessed directly
}
Output

Click Run to see what this code prints.

The fields and methods of an embedded type are said to be "promoted" to the outer struct — you can access c.Horsepower directly, without writing c.Engine.Horsepower, though the longer form still works and is occasionally needed to disambiguate.

package main
import "fmt"
type Base struct {
ID int
}
func (b Base) Describe() string {
return fmt.Sprintf("Base with ID %d", b.ID)
}
type Widget struct {
Base
Name string
}
func main() {
w := Widget{Base: Base{ID: 7}, Name: "Button"}
// Both forms work — the short form uses promotion.
fmt.Println(w.ID)
fmt.Println(w.Base.ID)
fmt.Println(w.Describe())
}
Output

Click Run to see what this code prints.

Overriding Promoted Methods

The outer struct can define its own method with the same name as a promoted one. The outer struct's own method takes precedence — this is the closest Go gets to method "overriding," though it's really just normal method resolution rather than polymorphic dispatch.

package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
type Dog struct {
Animal
}
// Dog defines its own Speak, which shadows Animal's promoted Speak.
func (d Dog) Speak() string {
return d.Name + " barks"
}
func main() {
generic := Animal{Name: "Creature"}
dog := Dog{Animal: Animal{Name: "Rex"}}
fmt.Println(generic.Speak())
fmt.Println(dog.Speak()) // uses Dog's own Speak
fmt.Println(dog.Animal.Speak()) // explicitly call the embedded version
}
Output

Click Run to see what this code prints.

Embedding and Interfaces

Because promoted methods count as the outer struct's own methods for the purpose of interface satisfaction, embedding is a common way to "inherit" an interface implementation for free. If Engine satisfies some Starter interface, then Car automatically satisfies it too, purely through embedding — no extra code required.

package main
import "fmt"
type Starter interface {
Start() string
}
type Engine struct {
Horsepower int
}
func (e Engine) Start() string {
return fmt.Sprintf("Engine starting with %d HP", e.Horsepower)
}
type Car struct {
Engine
Brand string
}
func announce(s Starter) {
fmt.Println(s.Start())
}
func main() {
c := Car{Engine: Engine{Horsepower: 250}, Brand: "Voyager"}
// Car satisfies Starter purely because Engine's Start() is promoted.
announce(c)
}
Output

Click Run to see what this code prints.

Common Mistakes

Avoid These Mistakes
  • Calling struct embedding "inheritance" in design discussions — it's composition with field/method promotion, not polymorphic inheritance, and the distinction matters for how method resolution works.
  • Creating naming collisions by embedding two types that both have a field or method with the same name, which requires explicit qualification (outer.TypeName.Field) to resolve.
  • Expecting a promoted method to dynamically dispatch to an overriding method the way virtual methods do in OOP languages — Go resolves promoted vs. outer methods statically, not polymorphically.
  • Over-embedding many unrelated types into one struct, creating a confusing, sprawling promoted API surface.
  • Forgetting to initialize the embedded field in a struct literal, leaving it at its zero value unexpectedly.

Best Practices

  • Reach for embedding when you have a genuine "has-a" or "is-composed-of" relationship, not to simulate class inheritance.
  • Keep embedded types small and focused, mirroring the same "small interfaces" philosophy that Go encourages elsewhere.
  • Use explicit field.Method() qualification whenever a name collision between embedded types could cause ambiguity.
  • Document why a type is embedded when the reason (e.g., "for interface satisfaction") isn't obvious from the code alone.
  • Prefer named fields over embedding when you don't actually want field/method promotion — embedding is a deliberate choice, not a default.

Frequently Asked Questions

No. Embedding gives you field and method promotion for convenience, but there is no polymorphic dispatch, no "protected" access, and no is-a relationship enforced by the type system — it is composition, not inheritance.

Yes, you can embed *Engine instead of Engine. This is useful when the embedded value is optional (nil-able) or expensive to copy.

Neither is promoted automatically — you must call it explicitly through the specific embedded field (e.g., outer.TypeA.Method()) to resolve the ambiguity.

Yes — you saw this in the interfaces lesson. An interface can embed other interfaces, combining their method sets, which is a very common pattern in the standard library (like io.ReadWriter combining io.Reader and io.Writer).

Key Takeaways

  • Go replaces classical inheritance with composition, implemented through struct embedding.
  • Embedding a type (with no field name) promotes its fields and methods to the outer struct.
  • An outer struct can define its own method with the same name to shadow a promoted one.
  • Promoted methods count toward interface satisfaction, letting embedding types "inherit" interface implementations.
  • Name collisions between multiple embedded types must be resolved with explicit qualification.

Summary

Struct embedding rounds out Go's composition-first philosophy, giving you a flexible way to reuse behavior without the rigid hierarchies and fragile-base-class issues that classical inheritance can introduce. With arrays, slices, maps, functions, pointers, structs, methods, interfaces, and embedding all in your toolkit, you're ready to tackle one of the most important topics in writing production-quality Go: proper error handling.

Next Lesson →

Error Handling