优化gomarklint之前,作者手里只有单元测试和覆盖率报告。至于这个工具在实际文档上的运行成本?一无所知。
gomarklint是一个用Go写的命令行Markdown检查器,定位很明确:在读者发现之前捕获死链,保持Markdown整洁,单二进制文件,不需要Node.js。主流替代品markdownlint功能完善,但依赖Node.js运行时。对于追求精简CI环境的Go项目来说,为了检查Markdown而引入Node.js,这个交易并不划算。
gomarklint作为独立二进制文件,支持Homebrew、npm、go install安装,开箱即用地集成GitHub Actions和pre-commit。规则集覆盖约25项检查:结构规则如heading-level(禁止H4直接出现在H2下方而无H3过渡),内容规则如no-bare-urls和fenced-code-language,以及包含内部锚点检查的链接验证。每条规则输出包含文件路径、行号和严重程度的诊断信息——error级别会导致非零退出码,适合作为CI门禁。
内部实现上,每条规则是一个接收文件内容(按行切分的字符串切片)并返回违规切片的功能函数。没有共享解析树,没有AST,就是字符串上的纯函数。这种架构让快速添加规则变得容易。
但问题也藏在这里。每条规则处理非平凡逻辑时,都要独立解决同一个问题:"这行在代码块里面吗?"围栏代码块里的标题不是真标题,里面的URL也不是真URL。每条在意的规则都得自己想办法判断。
organically生长出来的解决方案是一个叫GetCodeBlockLineRanges的共享工具函数。它扫描整个文档,为每个围栏代码块构建[start, end]行范围列表。规则随后调用isInCodeBlock(lineNumber, ranges)通过线性搜索检查成员资格。
作者没发现这是性能瓶颈,因为从未测量过。
第一步是建立可信的基准测试。原有的per-rule _bench_test.go文件彼此隔离,未纳入CI对比,无法反映端到端成本。作者重写了benchmark,最终在20个PR、三周内,将处理10万+行的耗时压到170毫秒以内。典型200行README的全规则检查低于0.2毫秒,大型文档站点的检查融入CI步骤毫无感知。
核心优化围绕那个被重复调用的线性搜索展开。当规则数量、文件行数、代码块数量同时增长时,O(n²)的隐形成本终于暴露。解决方案是用更高效的数据结构替换线性扫描——具体实现作者未展开,但基准数字说明了一切:81%的性能提升。
回头看,作者认为最大的教训是"先测量,再优化"。没有基准的优化是盲目飞行,而好的基准测试本身就需要投入设计。
热门跟贴