
本文详解如何正确使用 Go 自带的 go tool yacc 构建基础语法解析器,涵盖 Yacc 文件结构、语法规则书写规范、词法分析器(Lex)实现要点,并提供可运行的 echo 示例代码与编译命令。
本文详解如何正确使用 go 自带的 `go tool yacc` 构建基础语法解析器,涵盖 yacc 文件结构、语法规则书写规范、词法分析器(lex)实现要点,并提供可运行的 echo 示例代码与编译命令。
Go 的 go tool yacc 是一个兼容 POSIX Yacc 的代码生成工具,用于从 .y 文件自动生成 LALR(1) 解析器(Go 源码)。但其语法要求严格,初学者常因格式错误(如缺失非终结符、误用正则式规则)导致 bad syntax on first rule 等编译失败。核心前提是:Yacc 文件必须严格遵循三段式结构——declarations(声明区)、%% 分隔符、rules(语法规则区)、%% 分隔符、programs(Go 程序区);且每条语法规则必须形如 NonTerminal : Body ;,其中 : 和 ; 不可省略,左侧必须为合法非终结符,右侧由终结符(token)、非终结符或字面量组成。
下面是一个功能完整的最小可运行示例(保存为 echo.y),它读取一行输入,并逐字符打印:
%{
package main
import (
"bufio"
"fmt"
"os"
"unicode"
)
type InputSymType struct {
val string
}
%}
%union {
val string
}
%token <val> CHARACTER
%%
in : /* empty */
| in input '\n'
{ fmt.Printf("Read character: %s\n", $2) }
;
input : CHARACTER
| input CHARACTER
{ $$ = $1 + $2 }
;
%%
type InputLex struct {
s string
pos int
}
func (l *InputLex) Lex(lval *InputSymType) int {
if l.pos >= len(l.s) {
return 0 // EOF
}
c := rune(l.s[l.pos])
l.pos++
// 跳过空格(保持与 calc.y 一致行为)
if c == ' ' || c == '\t' || c == '\r' {
return -1 // 忽略,继续 lex
}
// 仅识别小写字母和数字为有效 token
if unicode.IsLower(c) || unicode.IsDigit(c) {
lval.val = string(c)
return CHARACTER
}
// 其他字符直接作为 token 返回(如换行符 '\n' 会被上层规则匹配)
return int(c)
}
func (l *InputLex) Error(s string) {
fmt.Fprintf(os.Stderr, "parse error: %s\n", s)
}
func main() {
var input string
fi := bufio.NewReader(os.Stdin)
if line, err := fi.ReadString('\n'); err == nil {
input = line
} else {
fmt.Fprintln(os.Stderr, "read error:", err)
return
}
lexer := &InputLex{s: input}
InputParse(lexer)
}</val>
⚠️ 关键注意事项:
- %union 声明必须存在(即使只用一个字段),否则 InputSymType 类型无法正确生成;
- 所有自定义 token(如 CHARACTER)需通过 %token 显式声明;
- Lex() 方法返回 0 表示 EOF,返回负数表示忽略当前字符(不触发语法动作),返回正整数表示对应 token 类型;
- go tool yacc 不支持正则表达式直接写在 rules 区(如 .|\n ECHO; 是非法的),所有模式匹配逻辑必须在 Lex() 中手工实现;
- 生成的解析器函数名由 -p 参数决定(如 -p Input → InputParse),且要求 Lex() 方法签名严格匹配 func(*InputSymType) int;
- 若需处理多行输入,应扩展 main 中的读取逻辑(例如循环调用 readline),而非依赖单次 ReadString。
编译并运行该示例只需两步:
go tool yacc -o echo.go -p Input echo.y go run echo.go
输入 abc123 后回车,将输出:
Read character: a Read character: b Read character: c Read character: 1 Read character: 2 Read character: 3
总结而言,go tool yacc 并非“开箱即用”的轻量解析器,而是面向需要精确控制语法分析流程的场景。掌握其结构约束(尤其是三段式+规则语法)、将词法逻辑下沉至 Lex()、并参考官方示例(如 calc.y)是成功入门的关键。对于简单文本处理,建议优先考虑 regexp 或 text/scanner;而对 DSL、配置语言等需强语法校验的场景,yacc 仍是 Go 生态中值得信赖的底层工具。











