🗂️ 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:
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.
In the standard library
io/fs&filepath.WalkDirwalk a composite file tree through thefs.FSinterface.go/astnodes form a composite —ast.Inspectrecurses 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.
Related patterns
Provide a way to access the elements of a collection sequentially without exposing its underlying representation.
STRUCTURAL DecoratorAttach new behavior to an object by wrapping it in another object of the same interface — without changing the original's type.
BEHAVIORAL VisitorAdd new operations to a set of object types without modifying those types, by moving each operation into a visitor.
Check your understanding
Score: 0 / 31. 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.