Composition and Embedding
By Syncfusion®, Inc. · Last updated
Build bigger types from smaller ones: has-a composition, struct embedding with promoted fields and methods, method resolution, and why Go prefers composition over inheritance.
What Is Composition?
Composition = building a bigger type by putting smaller types inside it.
- A
Schoolhas anAddress. - A
Studenthas aContact. - A
Libraryhas manyBooks.
Each small type does one job. The big type joins them.
Simple lesson: composition builds big things out of small, ready-made parts.
A Real-World Example
Think of building a school with toy blocks.
- One block set makes a classroom.
- Another makes a playground.
- Another makes a library.
- You put them together, and you have a school.
- If the library needs to change, you swap only that block.
Now the mapping:
- Each block = a small struct.
- The school = the big struct that contains them.
- Swapping one block = changing one part without touching the others.
Many languages use inheritance: a child class gets everything from a parent class.
Go has no inheritance. It uses composition and embedding instead.
What You Will Learn
In this lesson, you will learn:
- What composition is, and why Go prefers it.
- The has-a relationship.
- How struct embedding works, with promoted fields.
- How embedded methods are promoted, and how Go picks which method to call.
- How to build complex types from small parts.
- How composition differs from inheritance.
Composition Fundamentals
Has-A Relationship
Has-a relationship = one type has another type as a part.
- A
Studenthas anAddress. - A
Bushas aDriver. - If you can say "X has a Y", put a
Yfield insideX.
A student has an address
package main
import "fmt"
type Address struct {
City string
PinCode string
}
type Student struct {
Name string
Home Address
}
func main() {
s := Student{
Name: "Asha",
Home: Address{City: "Pune", PinCode: "411001"},
}
fmt.Println(s.Name, "lives in", s.Home.City)
}
Asha lives in Pune
Homeis a normal named field of typeAddress.- You reach its fields with two dots:
s.Home.City.
Why Composition Is Preferred in Go
- Small parts are easy to understand and test.
- Parts can be reused in many types:
Addressfits aStudent, aTeacherand aSchool. - Changes stay local. Changing
Addressdoes not break a family tree of classes. - No deep hierarchies like "A extends B extends C extends D".
Simple lesson: if X has a Y, put a Y inside X.
Struct Embedding
Struct Embedding
Embedding = putting a type inside a struct without a field name.
type Person struct {
Name string
Age int
}
type Teacher struct {
Person // embedded: no field name
Subject string
}
Personis written alone, with no name in front.- The embedded field's name is its type name:
Person.
Promoted Fields
Promoted field = a field of the embedded type that you can use directly on the outer type.
Promoted fields
t.Nameis short fort.Person.Name.- The data still lives inside the embedded
Person.
Reusing Functionality
One Contact part, used twice
package main
import "fmt"
type Contact struct {
Phone string
Email string
}
type Student struct {
Name string
Contact
}
type Teacher struct {
Name string
Contact
}
func main() {
s := Student{"Ravi", Contact{"98450 11111", "ravi@gvs.edu"}}
t := Teacher{"Mr. Khan", Contact{"98450 22222", "khan@gvs.edu"}}
fmt.Println(s.Name, s.Phone)
fmt.Println(t.Name, t.Email)
}
Ravi 98450 11111
Mr. Khan khan@gvs.edu
Contactis written once and reused in two types.
Simple lesson: embed a type with no field name, and its fields are promoted to the outer type.
Promoted Methods
Embedded Methods
Methods of the embedded type are promoted too.
Methods are promoted
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l Logger) Log(msg string) {
fmt.Println(l.Prefix + msg)
}
type Library struct {
Logger
Books int
}
func main() {
lib := Library{Logger: Logger{"[library] "}, Books: 1200}
lib.Log("opened")
lib.Log(fmt.Sprint(lib.Books, " books on the shelves"))
}
[library] opened
[library] 1200 books on the shelves
Libraryhas noLogmethod of its own.lib.Log(...)callslib.Logger.Log(...).
Method Resolution
Method resolution = how Go decides which method to run.
- Go first looks for the method on the outer type.
- If it is not there, Go looks in the embedded types.
- So an outer method with the same name hides the embedded one.
The outer method wins
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
type Dog struct {
Animal
}
func (d Dog) Speak() string {
return d.Name + " barks"
}
func main() {
d := Dog{Animal{"Tommy"}}
fmt.Println(d.Speak())
fmt.Println(d.Animal.Speak())
}
Tommy barks
Tommy makes a sound
d.Speak()findsDog.Speakfirst.d.Animal.Speak()reaches the hidden one on purpose.
Ambiguous Names
If two embedded types have the same field or method, and the outer type does not, Go cannot choose:
type Phone struct{ Number string }
type Locker struct{ Number string }
type Student struct {
Phone
Locker
}
// s.Number → error: ambiguous selector s.Number
// Use s.Phone.Number or s.Locker.Number instead.
Embedding is not inheritance.
The embedded value is a separate part. A method of Animal only sees the Animal fields. It does not know about Dog, and it cannot call Dog's methods.
Simple lesson: embedded methods are promoted, and a method on the outer type always wins.
Composition Patterns
Building Complex Types
A school bus built from parts
package main
import "fmt"
type Engine struct {
Fuel int
}
func (e *Engine) Refuel(litres int) {
e.Fuel += litres
}
type Driver struct {
Name string
}
type Route struct {
Stops []string
}
func (r Route) Count() int {
return len(r.Stops)
}
type SchoolBus struct {
Number string
Engine
Route
Driver Driver
}
func main() {
bus := SchoolBus{
Number: "GVS-04",
Route: Route{[]string{"Market", "Temple", "School"}},
Driver: Driver{"Raju"},
}
bus.Refuel(40)
fmt.Println(bus.Number, "driven by", bus.Driver.Name)
fmt.Println("Fuel:", bus.Fuel, "Stops:", bus.Count())
}
GVS-04 driven by Raju
Fuel: 40 Stops: 3
EngineandRouteare embedded: their fields and methods are promoted.Driveris a named field: you writebus.Driver.Name.bus.Refuel(40)changes the embedded engine, becauseRefuelhas a pointer receiver.
Embedded vs named field
Simple lesson: embed a part to share its abilities, and use a named field for a plain has-a part.
Composition vs Inheritance
Differences
| Inheritance (other languages) | Composition (Go) | |
|---|---|---|
| Relationship | is-a: a Dog is an Animal | has-a: a Bus has an Engine |
| How you get code | From a parent class | From parts you put inside |
| Change a parent | Every child may break | Only types that use that part are affected |
| Many sources | Often limited to one parent | Embed as many parts as you need |
| Shared behaviour | Through the class family | Through interfaces. See Interfaces and Polymorphism |
Why Go Avoids Inheritance
- Deep families of classes become hard to change.
- A change at the top can break many classes below it.
- "is-a" is often wrong in real life: is a
SquareaRectangle? It causes subtle bugs. - Go's designers chose simple parts plus interfaces instead.
Advantages of Composition
- Flexible: mix any parts.
- Clear: each part has one job.
- Testable: test each part alone.
- Safe to change: fewer surprises when one part changes.
Designing with Composition
- List the jobs: logging, address, contact, fuel.
- Make one small type per job.
- Build bigger types from them, embedded or named.
- Use interfaces to treat different types the same way.
Easy way to remember
- "X has a Y" → put a Y inside X.
- Want Y's fields and methods on X directly → embed Y.
- Want different types treated the same → use an interface.
Simple lesson: Go builds types from parts (has-a) instead of family trees (is-a).
TL;DR
Composition = build big types from small types inside them.
- Has-a = "a Student has an Address" → a field.
- Named field =
Home Address, reached ass.Home.City. Embedding = a type with no field name:
Person.- Promoted fields =
t.Namemeanst.Person.Name. - Promoted methods =
lib.Log()meanslib.Logger.Log().
- Promoted fields =
Method resolution = the outer type first, then embedded types.
- Same name in two embedded types = ambiguous, use the full path.
- Embedding is not inheritance. The embedded part does not know the outer type.
- Go prefers composition = small parts, flexible, safe to change.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Expecting Animal's method to call Dog's method | Embedding is inheritance | The embedded part knows only itself | The base behaviour runs. |
s.Number with two embedded Number fields | Go picks one | Go cannot choose | ambiguous selector s.Number |
Filling an embedded struct as Teacher{Name: "x"} | Promoted fields work in literals | Use Teacher{Person: Person{Name: "x"}} | unknown field Name in struct literal |
| Naming the field when you want promotion | Any field is promoted | Only embedded (unnamed) fields are | t.Name is undefined. |
| A pointer method on an embedded value in a copy | The copy changes the original | Copies are separate | Changes are lost. |
| Building deep chains of embedding | More levels = more reuse | Keep it shallow | Hard to see where a field comes from. |
Check Yourself
Exercises
What to Remember
- Composition builds big types from small parts (has-a).
- A named field is reached with its name:
s.Home.City. - An embedded field has no name. Its fields and methods are promoted.
- The outer type's own method always wins.
- Embedding is not inheritance. Go uses composition and interfaces instead.
Next: Interfaces and Polymorphism. You will learn how Go treats different types the same way, by what they can do.