I. Visitor — Behavioral Pattern
Visitor lets you define a new operation over a set of related types without modifying those types themselves. Each element type exposes a single Accept(visitor) method; the actual behavior lives entirely inside separate visitor objects, one per operation. Adding a new operation means writing a new visitor, not touching the elements at all.
This solves the problem of scattering unrelated operations — pricing, labeling, tax calculation, serialization — across the same set of types. Without Visitor, every new operation means adding a new method to every element type, so types that should only represent data end up accumulating logic for every consumer that ever needs to process them. Visitor moves each operation into its own object and uses double dispatch (element.Accept(visitor) calling back into visitor.VisitX(element)) so the right method runs for the right concrete type without a type switch.
It's the same shape as an AST node visited by a linter, a formatter, and a type-checker — each walks the same tree, but none of that logic lives inside the node types themselves.
II. Real-world Example
A coffee shop menu made of two item types, with two independent operations that need to run over both:
- MenuItem (interface) — declares
Accept(visitor Visitor) string, the single method every menu item must implement.
- Visitor (interface) — declares one method per concrete element type:
VisitDrink(drink Drink) stringandVisitPastry(pastry Pastry) string.
- Drink and Pastry — the concrete elements. Each one's
Acceptmethod does nothing but call the matchingVisitmethod on whatever visitor it's given:drink.Accept(v)callsv.VisitDrink(drink).
- PriceVisitor — implements both
Visitmethods to format a price string for each item type.
- LabelVisitor — implements both
Visitmethods to format a category label instead.
Neither Drink nor Pastry contains any pricing or labeling logic — both live entirely in the visitors, and a third operation (say, a CalorieVisitor) could be added without changing Drink or Pastry at all.

III. Implementation
visitor.go
package visitor
type Visitor interface {
VisitDrink(drink Drink) string
VisitPastry(pastry Pastry) string
}
type MenuItem interface {
Accept(visitor Visitor) string
}items.go
package visitor
type Drink struct {
Name string
Price float64
}
func NewDrink(name string, price float64) Drink {
return Drink{Name: name, Price: price}
}
func (d Drink) Accept(visitor Visitor) string {
return visitor.VisitDrink(d)
}
type Pastry struct {
Name string
Price float64
}
func NewPastry(name string, price float64) Pastry {
return Pastry{Name: name, Price: price}
}
func (p Pastry) Accept(visitor Visitor) string {
return visitor.VisitPastry(p)
}visitors.go
package visitor
import "fmt"
type PriceVisitor struct{}
func (v PriceVisitor) VisitDrink(drink Drink) string {
return fmt.Sprintf("%s costs %.2f", drink.Name, drink.Price)
}
func (v PriceVisitor) VisitPastry(pastry Pastry) string {
return fmt.Sprintf("%s costs %.2f", pastry.Name, pastry.Price)
}
type LabelVisitor struct{}
func (v LabelVisitor) VisitDrink(drink Drink) string {
return "Drink: " + drink.Name
}
func (v LabelVisitor) VisitPastry(pastry Pastry) string {
return "Pastry: " + pastry.Name
}main.go (usage)
menuItems := []visitor.MenuItem{
visitor.NewDrink("Cappuccino", 4.25),
visitor.NewPastry("Muffin", 3.10),
}
labelVisitor := visitor.LabelVisitor{}
priceVisitor := visitor.PriceVisitor{}
for _, item := range menuItems {
fmt.Println(item.Accept(labelVisitor))
fmt.Println(item.Accept(priceVisitor))
}IV. Explain the example above
menuItemsholds aDrinkand aPastry, both stored as theMenuIteminterface — the loop has no idea which concrete type each one is.
- For the first item (
Drink{Cappuccino, 4.25}),item.Accept(labelVisitor)callsDrink.Accept, which callslabelVisitor.VisitDrink(d), returning"Drink: Cappuccino".
item.Accept(priceVisitor)on the same item callspriceVisitor.VisitDrink(d), returning"Cappuccino costs 4.25".
- For the second item (
Pastry{Muffin, 3.10}),Acceptinstead callsVisitPastryon each visitor, returning"Pastry: Muffin"and"Muffin costs 3.10".
Running it produces:
*** Example Visitor ***
Drink: Cappuccino
Cappuccino costs 4.25
Pastry: Muffin
Muffin costs 3.10
*** End of Visitor ***The loop body never branches on whether item is a Drink or a Pastry — Accept handles routing to the correct Visit method internally. Swapping labelVisitor for a totally different visitor changes the entire operation without touching Drink, Pastry, or the loop.
V. Conclusion
Visitor earns its keep when you have a stable set of element types and a growing, open-ended set of operations that need to run over them. Keeping each operation in its own visitor means new operations never require touching the element types, and unrelated operations (pricing, labeling, tax, serialization) stay cleanly separated from each other.
The trade-off runs the other direction: adding a new element type means adding a method to the Visitor interface and updating every existing visitor to implement it. Visitor is a poor fit when element types change often but operations are stable — that's the exact inverse of what it optimizes for. For one or two operations that rarely change, methods directly on the element types are simpler than the double-dispatch machinery Visitor requires.
No pattern is universally superior.
VI. References
- Go Design Patterns (Mario Castro Contreras)




