23 解释器(行为型)
给一个简单的小语法(算术表达式、查询条件、规则)定义规则并求值,把句子表示成一棵语法树递归解释。讲清它的原理,以及为什么它是 23 个模式里最冷门、现实中你几乎永远不该手写的那一个。
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 接口。
// 抽象表达式:所有节点都能在上下文中求值
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 解释器,而是用专业的解析技术。
二十三式,到此收官
恭喜你走到了终点。回望这趟旅程,如果只让你带走三句话:
-
每个模式都在隔离某种“变化”。 忘掉类图,记住那句纲领——把会变的东西封装隔离起来。策略隔离算法、观察者隔离通知对象、状态隔离状态行为……抓住“它在隔离什么变化”,23 个模式就连成了一张网,而不是 23 条孤立口诀。
-
模式是解药,不是维生素。 见到痛点才用,别为用而用。本系列每一篇的“坑”都在反复劝一件事:过度设计比不设计更糟。菜鸟堆模式炫技,老手能不用就不用。
-
模式的流行度,反映语言的短板。 你一路都看到了:单例在 Go 里几乎不算模式(
sync.Once内建)、策略退化成一个函数值、迭代器变成for range、建造者在有具名参数的语言里被消解……语言越强,越多模式会“隐形”。 所以学模式,别学成教条——学的是背后那个反复出现的痛点,和止痛的思路。招式会因语言而变,内功不会。
配套的 9 语言完整代码都在 👉 github.com/davidapp/GoF,挑你顺手的语言跑一遍、改一改,比读十遍都管用。
祝你在真实的代码里,痛的时候想起它们,不痛的时候放过它们。