Struct Embedding and Composition
Learn how Go replaces inheritance with struct embedding, how fields and methods get promoted, and what to do when embedded names collide.
In Go you never write class Dog extends Animal. The language has no inheritance at all. Instead, you build types by composing smaller types together, and the primary tool for doing that with structs is embedding.
Embedding means placing one struct type inside another without giving that field a name. The inner type’s fields and methods are then promoted — they become directly available on the outer struct, as though the outer type had declared them itself. This page walks through the mechanics, the design philosophy, and the edge cases you will hit when you use embedding in real code.
Embedded Fields
An embedded field is a field declared with just a type and no field name. The type itself acts as the field name.
type Address struct {
Street string
City string
Zip string
}
type Customer struct {
Address // embedded field
Name string
Phone string
}
The Customer struct now has three promoted fields — Street, City, and Zip — that you can reach as cust.Street or, if you prefer to be explicit, as cust.Address.Street. Both paths work. You choose the short form when you want to treat the address as part of the customer’s public surface; you use the long form when you need to talk about the address as a whole unit.
Initialising structs with embedded fields
When you create a value with a composite literal, the embedded field must be initialised using the type name as the key.
cust := Customer{
Address: Address{
Street: "12 Main St",
City: "Springfield",
Zip: "62701",
},
Name: "Alice",
Phone: "555-1234",
}
If you omit the embedded field entirely, it gets its zero value — the whole Address struct is zero-valued, not nil. That means cust.Street is "" rather than causing a panic.
Embedding pointers
You are not limited to embedding value types. Embedding a pointer to a struct — *Engine — is common when you want multiple outer structs to share the same inner instance.
type Engine struct {
Horsepower int
Type string
}
type Car struct {
*Engine // pointer embedding
Model string
}
engine := &Engine{Horsepower: 300, Type: "V6"}
car1 := Car{Engine: engine, Model: "Sedan"}
car2 := Car{Engine: engine, Model: "Coupe"}
car1.Horsepower = 350
fmt.Println(car2.Horsepower) // 350 — same engine
The same Engine lives behind both car1 and car2, so a change through one is visible through the other. That shared ownership is the main reason to embed a pointer.
Nil embedded pointer:
If you embed *Engine and never assign it, the field is nil. Calling a method or reading a field on a nil embedded pointer panics. Always check for nil before using a promoted field or method from a pointer embedding, or initialise the pointer during construction.
What counts as embedding — and what does not
Embedding happens only when the field has no name. The moment you add a name, you have a regular named field.
type Car struct {
e Engine // named field, NOT embedding
}
No promotion occurs. To reach a field of Engine you must write car.e.Horsepower. This is still composition, just without the syntactic convenience of promotion.
Method and Field Promotion
Promotion means that the outer struct automatically gains the exported fields and methods of the embedded type. The outer type does not “inherit” them — the compiler simply resolves short names by looking inside embedded fields.
type Person struct {
Name string
}
func (p Person) Greet() string {
return "Hi, I'm " + p.Name
}
type Employee struct {
Person
ID int
}
e := Employee{Person: Person{Name: "Bob"}, ID: 123}
fmt.Println(e.Greet()) // "Hi, I'm Bob"
e.Greet() works because the compiler finds Greet inside the embedded Person. Under the hood it is equivalent to e.Person.Greet(). No runtime dispatch, no virtual table — just a name lookup at compile time.
Promotion applies to both value and pointer receivers. If Person had a pointer receiver method, Employee could still call it through the embedded Person as long as the embedded value is addressable.
Interface satisfaction via promotion
A useful consequence of promotion is that the outer type satisfies any interface that the embedded type satisfies. If Person implements fmt.Stringer, then Employee also implements fmt.Stringer — without any extra code.
type Describer interface {
Describe() string
}
type Base struct {
Value int
}
func (b Base) Describe() string {
return fmt.Sprintf("value: %d", b.Value)
}
type Wrapper struct {
Base
Label string
}
var d Describer = Wrapper{Base: Base{Value: 42}, Label: "answer"}
fmt.Println(d.Describe()) // "value: 42"
This is one of the main reasons Go programmers embed types: you can pull in an interface implementation just by embedding a concrete type that already fulfills it. The standard library uses this pattern heavily — for example, io.ReadWriter embeds both io.Reader and io.Writer.
Composed interface implementations:
If your type embeds a struct whose methods already satisfy an interface, your type automatically satisfies that interface too. This is a deliberate design choice, not a side effect, and it lets you build complex types from small, well-defined pieces.
Method shadowing
What happens when the outer struct declares a method with the same name as one promoted from an embedded type? The outer method wins — it shadows the promoted one.
type Animal struct {
Name string
}
func (a Animal) Speak() string { return "..." }
type Dog struct {
Animal
Breed string
}
func (d Dog) Speak() string { return "Woof!" }
dog := Dog{Animal: Animal{Name: "Buddy"}, Breed: "Labrador"}
fmt.Println(dog.Speak()) // "Woof!"
fmt.Println(dog.Animal.Speak()) // "..."
The compiler resolves dog.Speak() to Dog.Speak. If you want the embedded type’s version, you must write the full path. Shadowing is resolved statically — it is not the same as overriding in a language with virtual dispatch. Inside a Dog method, calling Speak() calls Dog.Speak; there is no automatic delegation to the embedded Animal version.
Shadowing is not polymorphism:
If a function takes an Animal value and calls Speak(), it always calls Animal.Speak — even if you pass a Dog. The outer type’s method does not change the behavior of the embedded type. That distinction trips up developers who come from inheritance-based languages.
Composition Over Inheritance
Go’s lack of inheritance is not an omission. It is a deliberate trade-off: inheritance creates deep, fragile hierarchies where a change in a base class ripples into every subclass. Composition, in contrast, keeps types small and dependencies explicit.
Embedding is the main mechanism for composition with structs. It gives you code reuse without coupling the outer type to the inner type’s identity. A Dog that embeds Animal can use Animal’s methods, but it is not an Animal in the type hierarchy — there is no “is-a” relationship enforced by the language. A function that expects an Animal cannot receive a Dog unless Dog explicitly satisfies some interface through promotion.
Extending standard library types
A classic pattern is embedding a type from the standard library, then wrapping its behaviour with extra logic.
type CountingWriter struct {
io.Writer
Count int64
}
func (cw *CountingWriter) Write(p []byte) (int, error) {
n, err := cw.Writer.Write(p)
cw.Count += int64(n)
return n, err
}
CountingWriter embeds io.Writer. Any Write call that goes through CountingWriter.Write increments the count, but the actual writing is delegated to the embedded writer. Because CountingWriter embeds an io.Writer, it is an io.Writer — the promotion takes care of that. You can pass it to any function that accepts io.Writer, and the counting layer is transparent to the caller.
Shared configuration structs
Another common use is pulling shared configuration fields into a base config and embedding that base in specific config types.
type BaseConfig struct {
Debug bool
Timeout int
}
type ServerConfig struct {
BaseConfig
Port int
}
type ClientConfig struct {
BaseConfig
ServerURL string
}
ServerConfig.Debug and ClientConfig.Timeout both work without repeating field definitions. This keeps configurations consistent and avoids drift. Because you can still access cfg.BaseConfig as a whole, you can pass the base portion to any function that only cares about common settings.
Embedding is not inheritance:
Despite the superficial resemblance, embedding does not create a parent-child class relationship. There is no constructor chaining, no super, no dynamic dispatch. The outer type simply has direct access to the inner type’s members. Think of embedding as a convenient way to delegate — nothing more.
Name Conflicts in Embedding
When two embedded types declare fields or methods with the same name, the compiler refuses to guess which one you mean. You must resolve the ambiguity yourself.
Conflicting fields
type A struct {
Name string
X int
}
type B struct {
Name string
Y int
}
type C struct {
A
B
Z int
}
c := C{A: A{Name: "A-side", X: 1}, B: B{Name: "B-side", Y: 2}, Z: 3}
// fmt.Println(c.Name) // compile error: ambiguous selector c.Name
fmt.Println(c.A.Name) // "A-side"
fmt.Println(c.B.Name) // "B-side"
The short selector c.Name is ambiguous because the compiler finds Name in both A and B. It will not compile until you qualify the path.
Conflicts resolved by the outer struct
If the outer struct itself declares a field with the same name, that field shadows all embedded fields of the same name, and the ambiguity disappears.
type D struct {
A
B
Name string // D's own Name
}
d := D{A: A{Name: "A"}, B: B{Name: "B"}, Name: "D"}
fmt.Println(d.Name) // "D" — no ambiguity
fmt.Println(d.A.Name) // "A"
d.Name now refers exclusively to D’s field. The embedded fields’ Name values are still reachable through their qualified paths.
Unresolved ambiguity stops compilation:
If you see an “ambiguous selector” error, the compiler is protecting you from a silent bug. The fix is always explicit qualification. Do not try to work around it by renaming fields in embedded types you do not control — qualify the path and move on.
Conflicting methods
The same rules apply to methods. If two embedded types have a method with the identical signature, the outer type must provide its own version to resolve the conflict, or callers must use the qualified path.
func (a A) String() string { return "A" }
func (b B) String() string { return "B" }
// c.String() — compile error unless C defines its own String()
Embedding is a compile-time feature, so all name resolution happens before the program runs.
Summary
Embedding is the mechanism Go gives you instead of inheritance. It promotes fields and methods, it satisfies interfaces, and it keeps your types flat and explicit. The trade-off is that you have to manage name collisions yourself, and you must remember that shadowing is static — it does not provide the kind of polymorphic dispatch you might expect from a classical OOP language.
When you embed a type, you are deciding that the outer type’s API includes the inner type’s API. If you do not want that, use a named field. If you want shared state, embed a pointer — and guard against nil.