递归下降解析器是将bnf/ebnf文法直接翻译为相互调用的parse函数,要求语法为ll(1)、词法分析支持peek、优先级通过分层parseexpr+阈值控制、错误恢复需显式跳过而非panic、ast构建与错误收集必须解耦。

递归下降解析器在 Go 里本质是函数调用栈映射文法规则
它不是框架或库,而是把 BNF 或 EBNF 文法直接翻译成一组相互调用的 parseExpr、parseTerm、parseFactor 这类函数。Go 的函数一等公民特性 + 明确的错误返回习惯,让这种手动写法非常自然——你不需要生成器(如 yacc),也不依赖反射。
关键判断:只要你的语法是 LL(1) 或接近 LL(1)(比如通过提取左公因子、消除左递归预处理过),就可以手写;否则会陷入无限递归或反复回溯,这时该换 PEG 库(如 peg)或改用 Pratt 解析。
必须先做词法分析,且 Token 流要支持“窥视一个”
递归下降靠的是“看一眼下一个 token 决定走哪个分支”,所以 lexer.Next() 必须能 Peek() 而不消耗。常见错误是每次 Next() 都推进位置,导致 parseIfStmt 看了 if 后,parseExpr 再调用时已错过第一个操作数。
实操建议:
- 用结构体封装 lexer,带
pos int和tokens []Token,Peek()返回tokens[pos],Next()返回并pos++ - Token 类型至少含
Type(如TOKEN_IF、TOKEN_INT)和Literal(原始字符串) - 在入口函数(如
Parse())开头就调用一次lexer.Next()预热,确保首次Peek()有效
parseExpr 怎么处理优先级和左结合性
不能简单写成 parseExpr → parseTerm (+ parseTerm)* 并循环调用——这会丢失结合性,且难以绑定 AST 节点。正确做法是分层函数 + 传入最小优先级阈值。
示例片段(简化):
func (p *parser) parseExpr(prec int) ast.Expr {
lhs := p.parseTerm()
for p.peek().Precedence() >= prec {
op := p.next()
rhs := p.parseExpr(op.Precedence() + 1) // +1 实现左结合
lhs = &ast.BinaryExpr{Op: op, Left: lhs, Right: rhs}
}
return lhs
}
注意点:
-
Precedence()是 token 方法,返回+为 10,*为 20 - 初始调用用
p.parseExpr(0),保证所有操作符都参与 - 没做右结合(如
^)时,用op.Precedence()而非+1,否则幂运算会错
错误恢复不能靠 panic,要用显式 token 跳过策略
一旦某个 parseIfStmt 发现当前是 for 而不是 if,它不该 panic,而应返回 nil + 错误,并让上层决定是否跳过到下一个语句边界(如 ; 或 })。否则整个解析就断了。
实操建议:
- 每个 parse 函数签名统一为
func() (ast.Node, error),绝不 panic - 遇到意外 token 时,记录错误(如
p.errs = append(p.errs, fmt.Errorf("expected %s, got %s", "if", p.peek().Type))),然后调用p.skipTo(stmtBoundary) -
skipTo是个辅助函数:循环Next()直到Peek()匹配TOKEN_SEMI或TOKEN_RBRACE,最多跳 10 个 token 防止死循环
最易被忽略的是:AST 构建和错误收集必须解耦。你可能成功构造出部分节点,但仍有语法错误——这时候不能丢弃已构建的树,而是把 error 附加到对应节点的 Comments 字段或单独维护错误列表。











