用Go复刻GitHub搜索过滤语法
GitHub Issue 那个
下一篇
分享一个把视频剪辑交给AI Agent的开源神器 →
is:open label:bug 的搜索框其实是个很典型的解析问题。很多人第一反应是用正则或者 strings.Split 暴力拆分,但只要遇到 label:"help wanted" 这种带空格的引号,或者 created:>2024-01-01 这种比较运算符,手写逻辑立刻就会变成一场灾难。我尝试用 participle 这个库来实现一套类似的解析器,它的核心逻辑非常硬核:直接用 Go 的 struct 定义语法,不需要写单独的 .y 语法文件,也不需要代码生成。
避坑:为什么不要手写字符串切分
大多数人写这种过滤逻辑的初版代码大概长这样:

// 典型的“以后再重构”式写法
parts := strings.Fields(query)
for _, p := range parts {
if strings.HasPrefix(p, "label:") {
labels = append(labels, strings.TrimPrefix(p, "label:"))
} else if strings.HasPrefix(p, "is:") {
state = strings.TrimPrefix(p, "is:")
}
// 后面跟着几十个 else-if,每增加一个维度就多写一遍
}这种写法在处理简单词时没问题,但一旦涉及否定词(如 -author:bot)或范围查询,正则会堆叠得极其臃肿。把这个问题从“字符串处理”提升到“语法解析”维度,代码量反而会下降。
实战:用 Struct 定义语法树 (AST)

在 participle 中,结构体字段的 tag 就定义了语法规则。要实现一个支持限定符(Qualifier)、自由文本和否定词的搜索语言,只需要三个结构体:
// Query 代表整个查询语句:由多个 Term 组成,隐含 AND 关系
type Query struct {
Terms []*Term `parser:"@@*"`
}
// Term 是最小搜索单元:可以是 key:value 形式的限定符,也可以是纯文本
// @Dash? 处理前缀 "-" 表示否定
type Term struct {
Negated bool `parser:"@Dash?"`
Qualifier *Qualifier `parser:"( @@"`
Text *Value `parser:"| @(String | Ident | Number) )"`
}

// Qualifier 处理 "key:value" 格式,例如 is:open 或 created:>2024-01-01
type Qualifier struct {
Key string `parser:"@Ident"`
Colon string `parser:"@Colon"`
Value string `parser:"@Ident"`
}部署与运行效果
通过这种方式,输入字符串会被直接转换为一个强类型的语法树。接下来的步骤就是遍历这个树,将其编译成参数化的 SQL 语句,从而避免 SQL 注入并保证查询效率。
实操运行效果如下:
$ make run q='is:open label:bug'
query: is:open label:bug
6 issue(s)
# STATE AUTHOR CREATED COMMENTS LABELS TITLE
1 open octocat 2024-02-10 12 bug,urgent Login button unresponsive on mobile
6 open bob 2024-04-18 15 bug,p0,urgent Memory leak in worker pool这种基于 AST 的工作流比正则稳健得多,而且扩展性极强。如果你需要增加 OR 逻辑或者括号优先级,只需要在 Query 结构体中增加递归定义即可。
