Skip to main content

Composition

What is Composition?

Composition is the practice of building complex types or behavior by combining simpler, smaller parts, instead of relying on class inheritance (which Go does not have). Composition is a "has-a" instead of "is-a".

  • Inheritance (not in Go): A Car is-a Vehicle.
  • Composition (Go way): A Car has-a Engine, has-a Wheels.

Composition in Go

  • Simplicity: No inheritance chains to follow.
  • Flexibility: Swap parts easily (e.g., replace Engine with ElectricMotor).
  • Reusability: Components are loosely coupled and testable in isolation.
  • Idiomatic Go: Encourages small, composable building blocks (e.g., io.Reader, io.Writer).

Struct fields (explicit composition)

type Engine struct {
HorsePower int
}

type Car struct {
Engine Engine // Car *has-a* Engine
Wheels int
}

func main() {
c := Car{Engine: Engine{HorsePower: 200}, Wheels: 4}
fmt.Println(c.Engine.HorsePower) // Access Engine through Car
}

Struct embedding (promoted fields & methods)

Go lets you embed one struct inside another. The embedded struct's fields/methods are promoted, so you can call them directly.

type Logger struct{}

func (Logger) Info(msg string) {
fmt.Println("[INFO]", msg)
}

type Service struct {
Logger // embedded
Name string
}

func main() {
s := Service{Name: "Payments"}
s.Info("starting...") // calls Logger.Info
}
  • Service doesn’t inherit from Logger.
  • Instead, it has a Logger, and Go automatically promotes Logger's methods.

Interfaces + composition

Behavior can be composed by depending on small interfaces.

type Reader interface {
Read(p []byte) (int, error)
}

type Writer interface {
Write(p []byte) (int, error)
}

// Compose both into a new interface
type ReadWriter interface {
Reader
Writer
}

Any type that implements both Read and Write automatically implements ReadWriter.

Real-world example

github.com/ygrebnov/config.Provider[T]: