What It Is
The Visitor pattern adds a new operation to a family of types without changing them. The types learn one thing once — Accept(visitor) — and from then on, every new operation is just a new visitor with one method per type.
It flips the usual trade: adding operations becomes easy, adding types becomes the expensive change.
Example: Operations over Shapes
The shapes never change again. Printing areas is one visitor, naming them is another — and an export-to-SVG visitor tomorrow would touch neither Circle nor Rectangle.
New operations, untouched types
Both visitors walk the same shape list through the same Accept door. Each visitor holds the whole operation in one place, instead of scattering it across the types.
Output: Go program and captured output
package main
import "fmt"
type Shape interface {
Accept(v Visitor)
}
type Circle struct {
Radius float64
}
func (c Circle) Accept(v Visitor) { v.VisitCircle(c) }
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Accept(v Visitor) { v.VisitRectangle(r) }
// A visitor is one whole operation over every shape type.
type Visitor interface {
VisitCircle(c Circle)
VisitRectangle(r Rectangle)
}
type AreaPrinter struct{}
func (AreaPrinter) VisitCircle(c Circle) {
fmt.Printf("circle area: %.2f\n", 3.14159*c.Radius*c.Radius)
}
func (AreaPrinter) VisitRectangle(r Rectangle) {
fmt.Printf("rectangle area: %.2f\n", r.Width*r.Height)
}
type NamePrinter struct{}
func (NamePrinter) VisitCircle(c Circle) { fmt.Println("a circle") }
func (NamePrinter) VisitRectangle(r Rectangle) { fmt.Println("a rectangle") }
func main() {
shapes := []Shape{Circle{Radius: 2}, Rectangle{Width: 3, Height: 4}}
for _, s := range shapes {
s.Accept(AreaPrinter{})
}
for _, s := range shapes {
s.Accept(NamePrinter{})
}
}
When to Use It
Use it when a stable family of types keeps growing new operations — compilers visiting syntax trees, documents gaining exporters, shapes gaining renderers and measurers.
Skip it when the type family still grows: every new type forces a change in every visitor. In many Go programs a type switch does the same job with less machinery — reach for the visitor when the operations, not the types, are multiplying.
Pros and Cons
Pros: whole operations live in one type each; new operations need zero changes to the visited types; related logic stops being scattered.
Cons: the double dispatch (Accept calling back) confuses newcomers; adding one visited type means editing every visitor; the visited family's interface is frozen early.