Go中不适合用经典解释器模式写DSL,应优先用text/scanner+递归下降实现轻量解析,避免goyacc和反射执行,强调错误定位、结构化上下文与安全执行。

解释器模式在 Go 里真不适合写 DSL
Go 没有泛型(旧版)、缺乏运算符重载、函数类型不支持隐式转换,加上标准库对 AST 解析支持弱,硬套经典解释器模式只会让代码又长又难维护。你真正需要的不是 Expression 接口和一堆 TerminalExpression 实现,而是能快速验证语义、方便调试、上线后不崩溃的轻量解析逻辑。
用 text/scanner + 简单递归下降比手写解释器更稳
Go 标准库的 text/scanner 足够应付大多数配置类或规则类 DSL(比如权限表达式 user.role == "admin" && time.hour > 9)。它不生成完整 AST,但能精准定位错误位置,且无第三方依赖。
- 用
scanner.Mode开启ScanComments和ScanFloats,避免自己处理数字格式 - 跳过空格和注释后,
scanner.TokenText()返回的是原始 token 字面量,别直接拿scanner.Scan()的 int 类型做 switch —— 容易漏scanner.Ident和scanner.String的边界 - 递归下降时,每个解析函数(如
parseExpr())只负责一层优先级,用peek()预读下一个 token 再决定是否进入子表达式,否则会把a + b * c解错
goyacc 生成的 parser 在 Go 项目里反而容易翻车
一旦 DSL 规则变复杂(比如要支持函数调用、嵌套括号、自定义操作符),手写递归下降很快失控,这时有人会想上 yacc。但 go yacc 生成的代码默认不带错误恢复,一个语法错误就 panic;而且它输出的是全局状态机,跟 Go 的 error-first 风格拧着来。
- 必须手动改
yacc生成的Parse()函数,把panic()替成返回fmt.Errorf("syntax error at %d", s.line) -
%type声明的类型不能是 interface{},得是具体 struct,否则运行时类型断言失败没提示 - 生成的
yyParse不接受io.Reader,只能传[]byte,大文件直接内存爆掉 —— 得自己加 buffer 分块喂
DSL 的执行阶段别用反射调变量名
解析完 AST 或 token 流之后,执行层最容易犯的错是:看到 "user.name" 就去 reflect.ValueOf(ctx).FieldByName("user").FieldByName("name")。这在单元测试里跑得欢,一到线上就 panic:字段未导出、嵌套层级不对、nil 指针全靠运气。
立即学习“go语言免费学习笔记(深入)”;
- 提前约定上下文结构体,用嵌入式 map[string]interface{} 做 fallback,比如
ctx.Get("user.name")内部走点号分隔 + 逐层查 map,而不是反射 - 所有变量访问都走统一 getter 接口,比如
func(ctx Context, path string) (interface{}, bool),这样后续可以加缓存、审计日志、超时控制 - 禁止在执行阶段动态
eval任意字符串 —— Go 没有安全沙箱,os/exec.Command("sh", "-c", userInput)这种事,DSL 设计者比用户更早想到
DSL 的难点从来不在“怎么解析”,而在于“怎么让业务方改错一条规则时不连累整个服务”。越早把错误位置、变量绑定、执行超时这些细节钉死,后面省的 debug 时间越多。










