{} The Go Reference

Behavioral pattern · Gang of Four · Advanced

Interpreter

Define a grammar for a simple language and an interpreter that evaluates sentences by walking an expression tree.

Behavioral Advanced ⏱ 4 min read Complete

🧮 Analogy

A calculator reading 3 + 4 * 2 doesn’t just scan left to right — it understands the grammar: multiplication binds tighter, so it computes 4 * 2 first, then adds 3. Interpreter is that understanding made concrete: each rule of the little language is a piece that knows how to evaluate itself.

The problem

You have a small, well-defined language — arithmetic, a rules expression, a query filter — and you want to evaluate sentences in it. Interpreter represents each grammar rule as a node type, assembles sentences into an expression tree, and evaluates the tree recursively. (Turning text into that tree is a separate parsing step.)

Structure

classDiagram
class Expr {
  <<interface>>
  +Eval() int
}
class Num { -value int; +Eval() }
class Add { -left Expr; -right Expr; +Eval() }
class Mul { -left Expr; -right Expr; +Eval() }
Expr <|.. Num
Expr <|.. Add
Expr <|.. Mul
Add o--> Expr : left, right
Mul o--> Expr : left, right

The expression 3 + 4 * 2 becomes this tree, evaluated bottom-up:

graph TD
A["Add"] --> N3["Num 3"]
A --> M["Mul"]
M --> N4["Num 4"]
M --> N2["Num 2"]

Idiomatic Go

Each node implements Eval; non-terminal nodes recurse into their children. Edit and Run:

interpreter.go — editable & runnable
package main

import "fmt"

// Expr is a node in the expression tree.
type Expr interface {
Eval() int
}

// Terminal: a literal number.
type Num struct{ value int }

func (n Num) Eval() int { return n.value }

// Non-terminals: operations that recurse into sub-expressions.
type Add struct{ left, right Expr }

func (a Add) Eval() int { return a.left.Eval() + a.right.Eval() }

type Mul struct{ left, right Expr }

func (m Mul) Eval() int { return m.left.Eval() * m.right.Eval() }

func main() {
// represents: 3 + 4 * 2  ==  3 + (4 * 2)
expr := Add{
	left:  Num{3},
	right: Mul{left: Num{4}, right: Num{2}},
}
fmt.Println(expr.Eval()) // 11
}

🐹 It's Composite with an Eval, plus a parser you bring

Interpreter is really Composite — terminals and non-terminals sharing one Eval interface — walked recursively. The piece it leaves out is turning "3 + 4 * 2" into the tree; that’s a lexer/parser. Go’s standard library is full of interpreters: text/template and html/template interpret a template language, regexp compiles and runs patterns, and go/parser + go/types interpret Go itself. Use the pattern for small, stable grammars — and a real parser for anything bigger.

Adding variables: an environment

Real little languages have variables, not just literals. Thread an environment through Eval — a context map of name → value — and add a Var node that looks itself up. Terminals ignore the env; everything else passes it down. Edit and Run:

env.go — editable & runnable
package main

import "fmt"

// Env carries variable bindings through evaluation.
type Env map[string]int

type Expr interface{ Eval(env Env) int }

type Num struct{ v int }

func (n Num) Eval(Env) int { return n.v }

// Var looks its name up in the environment.
type Var struct{ name string }

func (x Var) Eval(env Env) int { return env[x.name] }

type Add struct{ l, r Expr }

func (a Add) Eval(env Env) int { return a.l.Eval(env) + a.r.Eval(env) }

type Mul struct{ l, r Expr }

func (m Mul) Eval(env Env) int { return m.l.Eval(env) * m.r.Eval(env) }

func main() {
// x*2 + y
expr := Add{Mul{Var{"x"}, Num{2}}, Var{"y"}}
env := Env{"x": 5, "y": 3}
fmt.Println(expr.Eval(env)) // 13
}

The same context channel later carries scopes, function bindings, and built-ins. When you also need multiple passes (print, optimize) over the tree, reach for a Visitor instead of piling more methods onto each node.

In the standard library

  • text/template / html/template — interpret a templating language.
  • regexp — compiles a pattern, then interprets it against input.
  • go/ast + go/types — a full interpreter/visitor over Go source.

Pitfalls

⚠️ Grammars grow; hand-rolled nodes don't scale

A node-per-rule interpreter is delightful for a five-rule grammar and miserable for fifty. Operator precedence, error handling, and parsing quickly dominate. The moment your “little language” starts growing, switch to a proper parser (a generator, a PEG library like participle, or go/parser if it’s Go-like) and keep Interpreter for the genuinely tiny DSLs.

When to use it — and when not

✅ Reach for it when

  • You have a small, stable language you control — a rules engine, an expression evaluator, a tiny DSL.
  • Sentences in that language recur often enough to be worth modelling as a tree.
  • Each grammar rule maps cleanly to a type with an Eval method.

⛔ Think twice when

  • The grammar is large or changes often — use a real parser/generator instead of hand-built nodes.
  • Performance matters — interpreting a tree is slower than compiling.

Check your understanding

Score: 0 / 5

1. What is an Interpreter, structurally?

Each grammar rule is a node type implementing a common Eval; non-terminal nodes hold child expressions and recurse — it's Composite plus recursion.

2. What does Interpreter NOT cover?

The pattern is about representing and evaluating a grammar. Producing the tree from source text is the job of a parser, which Interpreter assumes you already have.

3. When should you NOT hand-roll an Interpreter?

Hand-built node-per-rule interpreters scale badly. For real languages, use proper parsing tooling; Interpreter suits only small, stable grammars.

4. How do you add variables (like x, y) to an Interpreter?

Thread a context map through evaluation: Eval(env Env). Terminals like Num ignore it; a new Var{name} node returns env[name]. The same channel carries function bindings, scopes, and built-ins as the language grows.

5. You need both Eval and Print (and later Optimize) over the same expression tree. What composes well with Interpreter?

Once you want several passes over the tree, putting Eval/Print/Optimize all on every node bloats them. A [Visitor](/patterns/visitor/) moves each operation into its own object — the same reason go/ast pairs its node tree with ast.Visitor.

Comments

Sign in with GitHub to join the discussion.