从 0 到 11 个 bug 已修复:GoAWK 如何经受实战考验,让我的 Regex 引擎提升 3000 倍

发布: (2025年12月8日 GMT+8 17:49)
7 min read
原文: Dev.to

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循环中的锚点每个位置都匹配
3Error format与标准库不同
4\w+@...捕获组DFA 返回 false
4(?s:.)内联标志被忽略
5a$结束锚点第一次调用错误
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.3Dec 4否定字符类、大小写不敏感
v0.8.4Dec 4^ 锚点(专业修复)
v0.8.5Dec 5单词边界 \b \B
v0.8.6Dec 7FindAll/ReplaceAll 中的 ^
v0.8.7Dec 7错误信息格式
v0.8.8Dec 7DFA + 捕获组
v0.8.9Dec 7Linter 兼容性
v0.8.10Dec 7内联标志 (?s:…)
v0.8.11Dec 8结束锚点首次调用 bug
v0.8.12Dec 8Longest() 实现

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.

我的收获

  1. 真实场景测试 > 单元测试 – 我的覆盖率是 88 %;GoAWK 找到了 11 个 bug。用户会捕捉到你想不到的情况。
  2. 多引擎架构 = 多引擎 bug – 每种策略(DFA、NFA、ReverseAnchored、OnePass)都有自己的边缘案例。引擎之间的集成测试变得至关重要。
  3. “在我的机器上能跑”毫无价值 – Ben 以我从未在基准中使用的方式调用正则。
  4. 快速反馈循环很重要 – 发现 → 修复 → 发布 → 测试,有时一天两次。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 稳定性保证,安全审计

你的反馈:让项目最快速走向生产就绪。

链接

从 0 到修复 11 个 bug。从“有趣的项目”到“生产就绪”。感谢那位真正去使用它的开发者。

Back to Blog

相关文章

阅读更多 »

规划我的下一个开源贡献

背景 在过去的一段时间里,我更加积极地参与开源项目,尤其是与 TypeScript 生态系统相关的项目。在我的 pull request…