从 0 到 11 个 bug 已修复:GoAWK 如何经受实战考验,让我的 Regex 引擎提升 3000 倍
Source: Dev.to
最佳的反馈方式
一周前,我发布了《“Go 的 Regexp 太慢了,所以我自己写了一个”》。反响非常热烈——但最有价值的反馈来自 Ben Hoyt,他是 GoAWK 的创建者。
他不仅阅读了文章,还真的去使用 coregex。
“我已经开始把 coregex 集成到 GoAWK 中… 我发现了一些问题。”
这条信息引发了我有史以来最富有成效的一周调试。
7 天内的 11 个 Bug
Ben 的 GoAWK 测试套件非常严苛——包含 1000 多个正则模式,覆盖了我从未想象的边缘情况。他找到了如下问题:
| 天 | Bug | 模式 | 症状 |
|---|---|---|---|
| 1 | [^,]* | 否定字符类 | 崩溃 |
| 1 | [oO]+d | 不区分大小写 | 匹配错误 |
| 2 | ^foo | 起始锚点 | 在所有位置都匹配 |
| 2 | \bword\b | 单词边界 | Find 返回空 |
| 3 | ^ in FindAll | 循环中的锚点 | 每个位置都匹配 |
| 3 | Error format | – | 与标准库不同 |
| 4 | \w+@... | 捕获组 | DFA 返回 false |
| 4 | (?s:.) | 内联标志 | 被忽略 |
| 5 | a$ | 结束锚点 | 第一次调用错误 |
| 6 | (#\\\n#!) | Longest() | – |
每个 bug 都让我学到东西。有的是尴尬的疏忽;有的则暴露了我理解上的根本缺口。
最糟的 Bug:^ 锚点
起始锚点(^)是我的克星。它看似简单——只在位置 0 匹配。但在多引擎架构中,“简单”很快就变得复杂。
- 版本 1 – 朴素地检查
pos == 0。对IsMatch有效,却在FindAllIndex上失效。 - 版本 2 – 添加了
FindAt(haystack, offset)方法。现在FindAllIndex能告诉引擎“这是原始字符串的第 5 位”。 - 版本 3 – 发现 DFA 的
epsilonClosure并未遵守锚点。实现了遵循 Rust 的regex‑automata的正确LookSet。
两天内三次尝试。Ben 持续测试,我持续修复。
最狡猾的 Bug:Longest()
这一次让我非常谦卑。Longest() 方法自 v0.8.2 起就已经存在。文档声称它可用,测试也通过——但它实际上是一个空实现。
// What I wrote (v0.8.2)
func (r *Regex) Longest() {
// TODO: implement leftmost-longest semantics
}
// What Ben expected
re := coregex.MustCompile(`(a|ab)`)
re.Longest()
// "ab" should match "ab" (longest), not "a" (first)
AWK 使用 POSIX 语义(左侧最长)。Go 标准库默认使用 Perl 语义(左侧优先),但 Longest() 会切换模式。我的引擎只支持 Perl 语义。
修复需要理解一个根本区别:
Leftmost‑First (Perl): (a|ab) on "ab" → "a" (first alternative wins)
Leftmost‑Longest (POSIX): (a|ab) on "ab" → "ab" (longer match wins)
在 PikeVM 中实现该功能约用了 100 行代码。默认模式下没有出现性能回退。
修复速度
| 版本 | 日期 | 修复内容 |
|---|---|---|
| v0.8.3 | Dec 4 | 否定字符类、大小写不敏感 |
| v0.8.4 | Dec 4 | ^ 锚点(专业修复) |
| v0.8.5 | Dec 5 | 单词边界 \b \B |
| v0.8.6 | Dec 7 | FindAll/ReplaceAll 中的 ^ |
| v0.8.7 | Dec 7 | 错误信息格式 |
| v0.8.8 | Dec 7 | DFA + 捕获组 |
| v0.8.9 | Dec 7 | Linter 兼容性 |
| v0.8.10 | Dec 7 | 内联标志 (?s:…) |
| v0.8.11 | Dec 8 | 结束锚点首次调用 bug |
| v0.8.12 | Dec 8 | Longest() 实现 |
5 天内 9 次发布,每一次都让 coregex 更加兼容标准库。
性能:依旧飞快
真正的问题是:所有这些修复是否削弱了性能?
Pattern: .*connection.*
Input: 250 KB log file
stdlib: 12.6 ms
coregex: 4 µs
Speedup: 3,154× (unchanged from v0.8.0)
架构决策起了关键作用:SIMD 预过滤、惰性 DFA 与策略选择保证了快路径。bug 修复主要针对极少运行的边缘情况代码。
完全兼容标准库
在 v0.8.12 之后,GoAWK 的全部测试套件全部通过:
$ cd goawk
$ go test ./...
ok github.com/benhoyt/goawk 4.832s
即插即用的替代已确认。
// Before
import "regexp"
// After
import "github.com/coregx/coregex"
// That's it. Same API. 5‑3000× faster.
我的收获
- 真实场景测试 > 单元测试 – 我的覆盖率是 88 %;GoAWK 找到了 11 个 bug。用户会捕捉到你想不到的情况。
- 多引擎架构 = 多引擎 bug – 每种策略(DFA、NFA、ReverseAnchored、OnePass)都有自己的边缘案例。引擎之间的集成测试变得至关重要。
- “在我的机器上能跑”毫无价值 – Ben 以我从未在基准中使用的方式调用正则。
- 快速反馈循环很重要 – 发现 → 修复 → 发布 → 测试,有时一天两次。Ben 详细的报告让这一切成为可能。
合作致谢
在此公开感谢 Ben Hoyt。他本可以说“这个库有 bug,我直接用标准库”,但他提交了详细的 issue,提供了测试用例,并持续测试每一次发布。这正是开源精神的最佳体现。
亲自尝试
go get github.com/coregx/coregex@v0.8.12
package main
import (
"fmt"
"github.com/coregx/coregex"
)
func main() {
re := coregex.MustCompile(`\w+@[\w.]+`)
fmt.Println(re.FindString("email: test@example.com"))
// Output: test@example.com
}
发现 bug?打开 issue。我会修复。
下一步计划
- v0.9.0:ARM NEON SIMD(等待 Go 1.26)
- v1.0.0:API 稳定性保证,安全审计
你的反馈:让项目最快速走向生产就绪。
链接
- GitHub: coregx/coregex
- GoAWK PR #264 – 找到所有问题的集成
- 原始文章
从 0 到修复 11 个 bug。从“有趣的项目”到“生产就绪”。感谢那位真正去使用它的开发者。