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 School has an Address.
  • A Student has a Contact.
  • A Library has many Books.

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:

  1. What composition is, and why Go prefers it.
  2. The has-a relationship.
  3. How struct embedding works, with promoted fields.
  4. How embedded methods are promoted, and how Go picks which method to call.
  5. How to build complex types from small parts.
  6. How composition differs from inheritance.

Composition Fundamentals

Has-A Relationship

Has-a relationship = one type has another type as a part.

  • A Student has an Address.
  • A Bus has a Driver.
  • If you can say "X has a Y", put a Y field inside X.

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
  • Home is a normal named field of type Address.
  • 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: Address fits a Student, a Teacher and a School.
  • Changes stay local. Changing Address does 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
}
  • Person is 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.Name is short for t.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
  • Contact is 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
  • Library has no Log method of its own.
  • lib.Log(...) calls lib.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() finds Dog.Speak first.
  • 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
  • Engine and Route are embedded: their fields and methods are promoted.
  • Driver is a named field: you write bus.Driver.Name.
  • bus.Refuel(40) changes the embedded engine, because Refuel has 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)
Relationshipis-a: a Dog is an Animalhas-a: a Bus has an Engine
How you get codeFrom a parent classFrom parts you put inside
Change a parentEvery child may breakOnly types that use that part are affected
Many sourcesOften limited to one parentEmbed as many parts as you need
Shared behaviourThrough the class familyThrough 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 Square a Rectangle? 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

  1. List the jobs: logging, address, contact, fuel.
  2. Make one small type per job.
  3. Build bigger types from them, embedded or named.
  4. 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

  1. Composition = build big types from small types inside them.

    • Has-a = "a Student has an Address" → a field.
  2. Named field = Home Address, reached as s.Home.City.
  3. Embedding = a type with no field name: Person.

    • Promoted fields = t.Name means t.Person.Name.
    • Promoted methods = lib.Log() means lib.Logger.Log().
  4. Method resolution = the outer type first, then embedded types.

    • Same name in two embedded types = ambiguous, use the full path.
  5. Embedding is not inheritance. The embedded part does not know the outer type.
  6. Go prefers composition = small parts, flexible, safe to change.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Expecting Animal's method to call Dog's methodEmbedding is inheritanceThe embedded part knows only itselfThe base behaviour runs.
s.Number with two embedded Number fieldsGo picks oneGo cannot chooseambiguous selector s.Number
Filling an embedded struct as Teacher{Name: "x"}Promoted fields work in literalsUse Teacher{Person: Person{Name: "x"}}unknown field Name in struct literal
Naming the field when you want promotionAny field is promotedOnly embedded (unnamed) fields aret.Name is undefined.
A pointer method on an embedded value in a copyThe copy changes the originalCopies are separateChanges are lost.
Building deep chains of embeddingMore levels = more reuseKeep it shallowHard 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.