用 interface{} 做 AST 节点类型是错的起点,应定义明确接口如 Expr 并让各节点实现,以获得编译期检查、避免运行时 panic 和冗余类型断言。

为什么用 interface{} 做 AST 节点类型是错的起点
Go 没有泛型前,很多人用 interface{} 存储 AST 节点,结果在 Eval() 里写满类型断言和 panic,一跑就 panic: interface conversion: interface {} is *ast.BinaryExpr, not *ast.Identifier。
真正该做的是定义清晰的节点接口:
type Expr interface {
Eval(env map[string]interface{}) interface{}
}
所有节点(*ast.Literal、*ast.BinaryExpr、*ast.Identifier)都实现它。这样调用链干净,编译期能捕获漏实现。
- 别用
map[string]interface{}当环境变量容器——键名拼错 runtime 才报错;改用带方法的结构体,比如type Env struct{ vars map[string]Value },加Get(key string) (Value, bool) -
Eval()返回interface{}是权宜之计;一旦规则要支持类型校验(比如“金额必须是 float64”),就得立刻引入自定义Value类型封装原始值和元信息 - 递归求值时容易栈溢出:没做深度限制的表达式如
(((((1+1)+1)+1)+1)套 1000 层,会爆runtime: goroutine stack exceeds 1000000000-byte limit;加个depth int参数并提前检查
如何让 Parser 不依赖 go/parser
直接调 go/parser.ParseExpr() 看起来省事,但你的规则语法不是 Go 语法——比如你想支持 user.age > 18 and user.active == true,而 Go 不认 and,也不允许 . 左侧是标识符而非包名。
立即学习“go语言免费学习笔记(深入)”;
手写递归下降 parser 更可控,尤其配合 text/scanner:
scanner := &text/scanner.Scanner{}
scanner.Init(strings.NewReader(input))
for tok := scanner.Scan(); tok != scanner.EOF; tok = scanner.Scan() {
switch tok {
case scanner.Ident:
// 处理 user.age 这种链式访问
case '+', '-', '>', '==':
// 构建操作符节点
}
}
- 别自己解析
.链:把user.profile.name当成一个整体Identifier字符串存下来,留到Eval()时再按.分割查嵌套 map;否则 parser 逻辑膨胀,还搞不清a.b.c()是调用还是取字段 - 优先级容易写反:比如
and优先级应低于==,但手写时可能先处理and导致a == b and c == d变成(a == (b and c)) == d;建议用 Pratt 解析法,用绑定力(binding power)控制 - 错误提示要带位置:用
scanner.Pos()记录每个 token 起始列,报错时输出line 3:12: unexpected token "or",不然规则出错根本没法调试
RuleEngine 的执行上下文不能共享 map[string]interface{}
多个 goroutine 并发跑规则时,如果共用一个 env map[string]interface{},又没加锁,会出现 fatal error: concurrent map read and map write。
更糟的是,有人用 sync.Map 顶替,结果发现性能比预期差 3 倍——因为每次 Eval() 都要反复 Load/Store 几十个字段,锁争用严重。
- 每个规则执行前克隆一份环境:
envCopy := make(map[string]interface{}, len(env)); for k, v := range env { envCopy[k] = v };简单、无锁、GC 友好 - 避免在
Env里塞大对象(如整个用户 struct);只放规则真正需要的字段,比如env["user_age"] = u.Age,而不是env["user"] = u;否则深拷贝或 GC 压力陡增 - 如果规则需读数据库或调外部 API,别把 client 塞进
Env;改用闭包注入:func NewRuleExecutor(db *sql.DB) func(*Rule) error,让执行器自己持有依赖
为什么 reflect.Value 在 Eval() 里是性能黑洞
为了支持 “任意结构体字段访问”,有人在 Eval() 里疯狂用 reflect.ValueOf(x).FieldByName("age"),结果压测时 CPU 90% 耗在 runtime.reflectcall 上。
反射不是不能用,而是不该在热路径上用。真正该做的是:编译期生成字段访问函数。
- 对常见结构体(如
User),用go:generate+reflect生成专用访问器:func GetFieldUserAge(u interface{}) (interface{}, bool),运行时就是纯指针解引用 - 如果结构体类型不固定,缓存
reflect.Type到访问函数的 map,首次访问某类型时生成并存入,后续直接 call;避免每次重复reflect.Value.FieldByName - 千万别用
reflect.Value.Interface()回传——它会触发内存分配;如果只是比较或计算,尽量留在reflect.Value内部操作(比如v.Int() == 18)
规则引擎最麻烦的从来不是语法解析,而是怎么让 user.age > 18 这种简单表达式,在百万次/秒的调用量下,既不 panic,也不慢得像卡住。每层抽象都要问一句:这个接口,是不是真被调用到了?










