Introduction
The composite pattern allows you treat a group of objects like a single object. The objects are composed into some form of a tree-structure to make this possible.
This patterns solves two problems:
- Sometimes it is practical to treat part and whole objects the same way
- An object hierarchy should be represented as a tree structure
We do this by doing the following:
- Define a unified interface for both the part objects (Leaf) and the whole object (Composite)
- Composite delegate calls to that interface to their children, Leaf objects deal with them directly.
This all sounds rather cryptic, so let us have a look at the diagram:

This is basically a graphical representation of the last two points: Composites delegate and Leafs perform the actual operation.
Implementation in Go
In this example we will deal with a country, and provinces. We will start with the usual preliminaries:
package main
import "fmt"
Next we define a GeographicalEntity interface, with just one method:
type GeographicalEntity interface {
search(string)
}
Next we define the Country struct:
type Country struct {
provinces []GeographicalEntity
name string
}
func newCountry(name string) Country {
return Country{
provinces: make([]GeographicalEntity, 0),
name: name,
}
}
func (c Country) search(term string) {
fmt.Printf("Search for city %s in country %s\n", term, c.name)
if c.name == term {
fmt.Printf("Found country: %s\n", term)
}
for _, composite := range c.provinces {
composite.search(term)
}
}
func (c *Country) addProvince(province GeographicalEntity) {
c.provinces = append(c.provinces, province)
}
Some notes:
- A country consists of some provinces. That is where the provinces variable comes from
- A country also has a name
- The
newCountryfunction is a utility function to construct a new Country struct - In the search we iterate over the
provincesfield. Since the objects implement thesearch()method, we delegate our search-request to them. Note that any object that satisfies theGeographicalEntityinterface could be in that array. Also note that if the country name happens to be the same as the search term, this is also reported. - The
addProvincesimply adds a province, or to be precise an object satisfying theGeographicalEntityinterface, to the provinces slice.
Next we implement the Province:
type Province struct {
name string
}
func newProvince(name string) Province {
return Province{
name: name,
}
}
func (p Province) search(term string) {
fmt.Printf("Search for city %s in province %s\n", term, p.name)
}
func (p *Province) addCity(city GeographicalEntity) {
p.cities = append(p.cities, city)
}
Also some notes here:
- A
Provincehas one name, and list of cities. - The
newProvince()function is a utility function to create aProvincestruct - In the
search()method we simply announce we are searching. - The
addCity()method adds a city to the cities-list.
Finally we need a City which will act as the leaf node of our composite structure:
type City struct {
name string
}
func newCity(name string) City {
return City{name: name}
}
func (c City) search(term string) {
if c.name == term {
fmt.Printf("Found city: %s\n", term)
}
}
Some notes:
- Since
Cityis the leaf node it does not have any references to children, and just has a name. - Like our previous structs this struct has a constructor function:
newCity() - Since there are no children, the
search()method is extremely simple: simple compare the search term to the city’s name, and print a message if they match.
Time to test
Time to set up a small geographical database, and see if we can search it:
func main() {
city1 := newCity("city1")
city2 := newCity("city2")
province1 := newProvince("province1")
province1.addCity(city1)
province1.addCity(city2)
province2 := newProvince("province2")
country := newCountry("Country")
country.addProvince(province1)
country.addProvince(province2)
country.search("city1")
}
Line by line:
- We start by constructing two cities, and one province.
- Add these cities to the province.
- Then create a second province without any cities.
- Now add a new country, and add these two provinces.
- Finally we can perform a search.
Conclusion
The Composite Pattern, as demonstrated through the simple, elegant power of Go interfaces, is a truly indispensable tool for managing hierarchical data structures.
Key Takeaways
- Unified Treatment: By defining the
GeographicalEntityinterface, we achieved the core goal of the pattern: treating individual objects (Leaves) likeCityand composite objects (Composites) likeCountryandProvinceuniformly. This simplifies client code, as you only interact with the unified interface, regardless of the object’s complexity. - Recursive Operations: The true magic of the Composite Pattern lies in its recursive nature. When we called
country.search("city1"), the request elegantly cascaded down the hierarchy—fromCountrytoProvincetoCity—until the leaf node performed the final, atomic operation. - Flexibility and Extensibility: This design makes the system highly flexible. Adding a new hierarchical level (e.g., a “Region” that contains multiple “Countries”) or a new leaf type (e.g., a “Village”) simply requires implementing the
GeographicalEntityinterface. The existingCountryandProvincelogic remains untouched, adhering to the Open/Closed Principle.
The Composite Pattern in Go allows developers to build complex tree structures that are easy to navigate, understand, and maintain. It’s a testament to how simple object-oriented principles, leveraged by Go’s powerful interface system, can solve complex architectural problems with clean, maintainable code.




