23 解释器(行为型)

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

作者 David更新于

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”。

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

三、怎么实现:终结符 + 非终结符,递归求值

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

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 递归求值,还能带变量上下文:

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

你会发现,这棵表达式树本质就是一棵组合模式的树,而“求值”就是对它的递归遍历——解释器 = 组合模式(AST 结构) + 递归解释。

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

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

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

其它要点:

  • 现实中的正确做法,几乎都不是手写解释器。 语法稍复杂,业界用的是解析器生成器(yacc/bison、ANTLR、Go 的 goyacc)或成熟的表达式求值库(如 expr、govaluate)。它们帮你处理优先级、错误、优化等一大堆脏活,比手搓 GoF 解释器专业得多。
  • 它和访问者是天生一对。 求值逻辑如果直接写进每个表达式类的 Interpret 里,类一多就乱。实践中常把“解释/求值”用访问者从节点类里抽出去——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,挑你顺手的语言跑一遍、改一改,比读十遍都管用。

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

本页目录