{} The Go Reference

Structural pattern · Gang of Four · Intermediate

Composite

Treat individual objects and compositions of objects uniformly through one common interface.

Also known as — Object tree

Structural Intermediate ⏱ 5 min read Complete

🗂️ Analogy

A folder on your computer can hold files — and other folders, which hold more files and folders. When you ask the OS for a folder’s size, you don’t care how deep it nests: the same request works whether it contains one file or a thousand nested folders. A folder and a file are, in that moment, the same kind of thing.

The problem

You have a tree: files inside folders, UI elements inside containers, an org chart of managers and reports. The painful way to process it is to keep asking “is this a single thing or a group?” and branching:

func printAny(x any, indent int) {
	switch v := x.(type) {
	case *File:
		// print file
	case *Folder:
		// print folder, then loop children and recurse...
	}
}

Every operation re-implements that type switch and recursion. Composite removes both by giving leaves and containers one shared interface — so the recursion lives inside the containers, and callers just make one polymorphic call.

Structure

classDiagram
class Component {
  <<interface>>
  +Name() string
  +Print(indent)
}
class File {
  +Name() string
  +Print(indent)
}
class Folder {
  -children Component[]
  +Add(Component)
  +Name() string
  +Print(indent)
}
Component <|.. File
Component <|.. Folder
Folder o--> Component : children

The crucial line is Folder o--> Component: a container holds the same interface it implements. That self-reference is what lets folders nest folders to any depth.

How it works

One call on the root fans out through the whole tree:

graph TD
P["Folder: project"] --> M["File: go.mod"]
P --> S["Folder: src"]
S --> A["File: main.go"]
S --> B["File: util.go"]

Idiomatic Go

There’s no inheritance — just a small interface and a struct that embeds a slice of it. Edit and Run:

composite.go — editable & runnable
package main

import (
"fmt"
"strings"
)

// Component: the common interface leaves and composites both satisfy.
type Component interface {
Name() string
Print(indent int)
}

// File is a leaf — no children.
type File struct{ name string }

func (f *File) Name() string { return f.name }
func (f *File) Print(indent int) {
fmt.Printf("%sFile: %s\n", strings.Repeat("  ", indent), f.name)
}

// Folder is a composite — it holds child Components (files OR folders).
type Folder struct {
name     string
children []Component
}

func (f *Folder) Name() string   { return f.name }
func (f *Folder) Add(c Component) { f.children = append(f.children, c) }
func (f *Folder) Print(indent int) {
fmt.Printf("%sFolder: %s\n", strings.Repeat("  ", indent), f.name)
for _, c := range f.children {
	c.Print(indent + 1) // same call — leaf or folder
}
}

func main() {
root := &Folder{name: "project"}
root.Add(&File{name: "go.mod"})

src := &Folder{name: "src"}
src.Add(&File{name: "main.go"})
src.Add(&File{name: "util.go"})
root.Add(src)

root.Print(0) // one call walks the entire tree
}

🐹 Composite + Iterator together

You can take it one step further: have every Component also expose CreateIterator(), and a stack-based compositeIterator does a depth-first traversal of the tree. Leaves return a Null-Object emptyIterator. It’s a lovely demonstration of Composite and Iterator working together — the caller walks the whole tree with a plain for it.HasNext() loop and never sees the recursion.

Transparency vs safety

GoF frames Composite’s one real design decision as transparency vs safety — where do child-management methods (Add, Remove) live?

  • Transparency: put them on the shared Component interface. Every node looks identical and clients never type-assert — but a File.Add(...) is meaningless and must panic or no-op.
  • Safety: put them only on Folder. No nonsensical leaf methods — but a client that holds a Component must type-assert to *Folder before adding a child.

Go nudges firmly toward safety: keep the shared interface to what both leaf and container genuinely support (here Name/Print), and expose Add only on Folder. A small, honest interface beats a uniform-but-lying one — the same instinct behind Go’s tiny single-method interfaces.

In the standard library

  • io/fs & filepath.WalkDir walk a composite file tree through the fs.FS interface.
  • go/ast nodes form a composite — ast.Inspect recurses uniformly over them.
  • html.Node (golang.org/x/net/html) is a composite DOM tree.

Pitfalls

⚠️ Don't force a leaf to fake container methods

If your Component interface includes Add(child) and a leaf can’t have children, the leaf is stuck implementing a method that panics or no-ops. Keep the shared interface to what both truly support (here: Name, Print), and put child management (Add) only on Folder. A meaningful common interface beats a bloated one.

When to use it — and when not

✅ Reach for it when

  • Your data is a part-whole hierarchy — files & folders, UI nodes, org charts, expression trees.
  • You want client code to treat a single item and a group of items the same way.
  • Operations should recurse through the structure without the caller writing the recursion.

⛔ Think twice when

  • The structure is flat — a plain slice is simpler than a tree of interfaces.
  • Leaves and containers need very different APIs; forcing one interface makes leaves implement meaningless methods.

Check your understanding

Score: 0 / 5

1. What makes Composite work?

Both File (leaf) and Folder (container) satisfy the Component interface, and Folder holds []Component — so a folder can contain files and other folders interchangeably.

2. Why does the caller's `root.Print(0)` not need a type switch?

Polymorphism: Print is on the interface. A Folder's Print loops its children and calls their Print — leaf or folder, the call is identical.

3. Which pattern most naturally pairs with Composite to walk the tree?

Iterator gives clients a uniform way to traverse the composite without exposing its tree structure — exactly what a depth-first compositeIterator does.

4. GoF calls it the 'transparency vs safety' trade-off. What is it?

Transparency = one interface for leaves and containers (uniform, but a File.Add is nonsense). Safety = child-management only on Folder (no nonsensical leaf methods, but clients sometimes need a type assertion to add). Go leans safety: keep the shared interface to what both truly support.

5. Composite and Decorator both hold a value of their own interface. How do they differ?

Structurally similar (self-referential interface), different intent: Composite is about structure (a part-whole tree, often many children); Decorator is about behavior (one wrapped component, enriched). A decorator with a list of children is drifting toward composite.

Comments

Sign in with GitHub to join the discussion.