# 23 解释器（行为型）

> 给一个简单的小语法(算术表达式、查询条件、规则)定义规则并求值，把句子表示成一棵语法树递归解释。讲清它的原理，以及为什么它是 23 个模式里最冷门、现实中你几乎永远不该手写的那一个。

- 作者：David（道雾轩）
- 专栏：经典设计模式浅析（https://daiw.org/manual/design-patterns.md）
- 最后更新：2026-07-12
- 原文：https://daiw.org/manual/design-patterns/interpreter
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 23 解释器 · Interpreter（行为型）

终章。我们用 GoF 里**最冷门、争议也最实在**的一个模式收尾——说实话，它是 23 个里你最不可能亲手实现的那个。但理解它，能让你看懂“语言是怎么被执行的”。

## 一、这是什么：给计算器造一个“小语言”

你要做个计算器，能算 `5 + 3 - 2`。计算机怎么“看懂”这串字符？它得：先把式子拆成一棵**树**(`5+3` 是一个加法节点，再和 `2` 组成减法节点)，然后从叶子往上**递归求值**。

解释器模式就是干这个的：**给一个简单的语言定义它的文法(每条语法规则对应一个类)，把一个句子表示成一棵抽象语法树(AST)，然后递归地“解释”(求值)这棵树。**

配套代码正是这个算术解释器：数字、变量是“终结符”节点，加法、减法是“非终结符”节点，`Parse` 把 `"x + y - 3"` 变成一棵表达式树，`Interpret(ctx)` 递归求值。

## 二、解决什么痛：一种小语法要反复解析求值

有时你会遇到一种**领域特定的小语言**，需要反复解析和执行它：

- 计算器的算术表达式。
- 规则引擎的条件：`age > 18 && vip == true`。
- 简单的查询过滤：`status = "active" AND score > 60`。

如果每次都用一堆字符串切割和 `if-else` 硬解析，代码会又臭又长、加一条语法规则就崩。解释器的思路：**为文法里的每一条规则定义一个类**，让“解析”变成“构造一棵由这些规则类组成的树”，“执行”变成“对树递归调用 `Interpret`”。

<Callout type="info">
  **解释器隔离的“会变的东西”，是“这门小语言的每一条文法规则”。** 每条规则(数字、变量、加法、减法……)封装成一个独立的表达式类，组合成树。加一种运算(比如乘法)= 加一个表达式类。语法的结构被显式地映射成了类的结构。
</Callout>

## 三、怎么实现：终结符 + 非终结符，递归求值

文法节点分两种：**终结符**(叶子，不再包含子表达式，如数字、变量)和**非终结符**(内部节点，由子表达式组合而成，如加法、减法)。所有节点实现同一个 `Interpret` 接口。

```go title="go/interpreter/main.go（核心）"
// 抽象表达式:所有节点都能在上下文中求值
type Expression interface{ Interpret(ctx *Context) int }

// 终结符:数字(叶子,直接返回值)
type NumberExpression struct{ value int }
func (n *NumberExpression) Interpret(ctx *Context) int { return n.value }

// 终结符:变量(叶子,从上下文查值)
type VariableExpression struct{ name string }
func (v *VariableExpression) Interpret(ctx *Context) int { return ctx.GetVar(v.name) }

// 非终结符:加法(内部节点,递归求值左右子表达式再相加)
type AddExpression struct{ left, right Expression }
func (a *AddExpression) Interpret(ctx *Context) int {
	return a.left.Interpret(ctx) + a.right.Interpret(ctx)   // ← 递归
}
```

`Parse` 把中缀表达式串成一棵树，`Interpret` 递归求值，还能带变量上下文：

```go
ctx := NewContext()
ctx.SetVar("x", 10); ctx.SetVar("y", 4)
ast, _ := Parse("x + y - 3")
ast.Interpret(ctx)                 // = 11
```

你会发现，这棵表达式树本质就是一棵[组合模式](https://daiw.org/manual/design-patterns/composite)的树，而“求值”就是对它的递归遍历——**解释器 = 组合模式(AST 结构) + 递归解释**。

## 四、有哪些坑：为什么说它“最鸡肋”

必须实话实说，这是全系列唯一一个我要**劝你基本别用**的模式：

<Callout type="warn">
  **解释器只适合“极其简单、且稳定”的小语法。** 一旦语法稍微复杂一点(要支持运算符优先级、括号、函数调用、错误恢复……)，你手写的表达式类和解析逻辑就会**爆炸式膨胀**，难写、难调、性能还差。GoF 自己都在书里提醒：文法复杂时，别用这个模式。
</Callout>

其它要点：

- **现实中的正确做法，几乎都不是手写解释器。** 语法稍复杂，业界用的是**解析器生成器**(yacc/bison、ANTLR、Go 的 `goyacc`)或成熟的**表达式求值库**(如 `expr`、`govaluate`)。它们帮你处理优先级、错误、优化等一大堆脏活，比手搓 GoF 解释器专业得多。
- **它和访问者是天生一对。** 求值逻辑如果直接写进每个表达式类的 `Interpret` 里，类一多就乱。实践中常把“解释/求值”用[访问者](https://daiw.org/manual/design-patterns/visitor)从节点类里抽出去——AST 节点只管表示结构，一个 `EvalVisitor` 负责遍历求值，一个 `PrintVisitor` 负责打印，等等。**真正的编译器就是这么干的。**
- **性能。** 树遍历式的解释，每次求值都递归走一遍树，比编译成字节码/机器码慢得多。反复高频执行同一表达式时，考虑编译缓存。

## 五、开源里怎么用

- **`go/parser` + `go/ast`**:Go 工具链把源码解析成 AST(解释器模式的“表示”部分)，再由各种工具遍历处理。虽然 Go 编译器最终是编译而非解释，但“源码→AST→遍历”这套结构，正是解释器模式的骨架。
- **模板引擎**：Go 的 `text/template`/`html/template`、各类模板语言，解析模板成语法树后求值渲染——解释器的实用形态。
- **表达式/规则引擎**：`expr`、`govaluate` 等库让你在运行时求值 `user.age > 18` 这样的表达式，背后是解释器思想(但实现远比 GoF 版精良)。规则引擎、特征开关、告警条件求值都靠它。
- **正则表达式引擎、SQL 解析器、配置语言(HCL)**：凡是“把一段文本按文法解析成结构再执行”的地方，都有解释器的影子——但再强调一次，**它们几乎无一是手写 GoF 解释器，而是用专业的解析技术**。

---

## 二十三式，到此收官

恭喜你走到了终点。回望这趟旅程，如果只让你带走三句话：

1. **每个模式都在隔离某种“变化”。** 忘掉类图，记住那句纲领——**把会变的东西封装隔离起来**。策略隔离算法、观察者隔离通知对象、状态隔离状态行为……抓住“它在隔离什么变化”，23 个模式就连成了一张网，而不是 23 条孤立口诀。

2. **模式是解药，不是维生素。** 见到痛点才用，别为用而用。本系列每一篇的“坑”都在反复劝一件事：**过度设计比不设计更糟**。菜鸟堆模式炫技，老手能不用就不用。

3. **模式的流行度，反映语言的短板。** 你一路都看到了：单例在 Go 里几乎不算模式(`sync.Once` 内建)、策略退化成一个函数值、迭代器变成 `for range`、建造者在有具名参数的语言里被消解……**语言越强，越多模式会“隐形”。** 所以学模式，别学成教条——学的是**背后那个反复出现的痛点，和止痛的思路**。招式会因语言而变，内功不会。

配套的 9 语言完整代码都在 👉 [github.com/davidapp/GoF](https://github.com/davidapp/GoF)，挑你顺手的语言跑一遍、改一改，比读十遍都管用。

祝你在真实的代码里，**痛的时候想起它们，不痛的时候放过它们**。
