<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; 持续集成</title>
	<atom:link href="http://tonybai.com/tag/%e6%8c%81%e7%bb%ad%e9%9b%86%e6%88%90/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sun, 31 May 2026 22:20:31 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>数据说话：Go 1.26 或成近年来“问题最多”的大版本，现在升级安全吗？</title>
		<link>https://tonybai.com/2026/03/06/go-1-26-most-problematic-release/</link>
		<comments>https://tonybai.com/2026/03/06/go-1-26-most-problematic-release/#comments</comments>
		<pubDate>Thu, 05 Mar 2026 23:38:50 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ast]]></category>
		<category><![CDATA[CI/CD]]></category>
		<category><![CDATA[CompilerBugs]]></category>
		<category><![CDATA[EdgeCases]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.26]]></category>
		<category><![CDATA[Go1.26.1]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GreenTeaGC]]></category>
		<category><![CDATA[InternalCompilerError]]></category>
		<category><![CDATA[IssueAnalysis]]></category>
		<category><![CDATA[Issue分析]]></category>
		<category><![CDATA[Modernization]]></category>
		<category><![CDATA[new(expr)]]></category>
		<category><![CDATA[ProductionEnvironment]]></category>
		<category><![CDATA[Regression]]></category>
		<category><![CDATA[RuntimeErrors]]></category>
		<category><![CDATA[SecurityFix]]></category>
		<category><![CDATA[SoftwareQuality]]></category>
		<category><![CDATA[Stability]]></category>
		<category><![CDATA[StaticAnalysis]]></category>
		<category><![CDATA[toolchain]]></category>
		<category><![CDATA[VersionUpgrade]]></category>
		<category><![CDATA[代码现代化]]></category>
		<category><![CDATA[内部编译错误]]></category>
		<category><![CDATA[回归问题]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[安全修复]]></category>
		<category><![CDATA[工具链]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[版本升级]]></category>
		<category><![CDATA[生产环境]]></category>
		<category><![CDATA[稳定性]]></category>
		<category><![CDATA[编译器Bug]]></category>
		<category><![CDATA[语法糖]]></category>
		<category><![CDATA[软件质量]]></category>
		<category><![CDATA[边界情况]]></category>
		<category><![CDATA[运行时错误]]></category>
		<category><![CDATA[静态分析]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5987</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/03/06/go-1-26-most-problematic-release 大家好，我是Tony Bai。 2026 年 2 月，Go 1.26 如约而至。伴随着 new(expr) 语法糖的引入、Green Tea GC 的全面转正，以及go fix 现代化重构等一系列重磅特性，许多 Gopher 都按捺不住尝鲜的冲动。 然而，在经验丰富的 Go 团队和架构师群体中，流传着一条不成文的“潜规则”：永远不要在生产环境第一时间升级 X.Y.0 大版本，至少等到 X.Y.1 补丁发布后再做决定。 这条潜规则并非空穴来风。Go 的 1.N.0 版本虽然经过了长达半年的开发和 RC 阶段的测试，但只有当它真正被全球几百万开发者投入到千奇百怪的生产环境中时，那些隐藏在深处的边界 Bug 才会浮出水面。而 1.N.1 版本，正是官方对这“第一波真实世界火力测试”所暴露问题的集中修复。 因此，一个非常客观且有趣的推论诞生了：观察 1.N.1 里程碑下的 Issue 数量，可以作为衡量 1.N.0 初始质量的一张“晴雨表”。 最近，我在例行了解 Go 官方仓库的 GitHub 里程碑数据时，发现了一个令人警惕的信号：Go 1.26.1 的 Issue 数量，正在呈现出明显的“异常峰值”。 本文将用真实的数据说话，带你横向拉网式对比 Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/go-1-26-most-problematic-release-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/03/06/go-1-26-most-problematic-release">本文永久链接</a> &#8211; https://tonybai.com/2026/03/06/go-1-26-most-problematic-release</p>
<p>大家好，我是Tony Bai。</p>
<p>2026 年 2 月，<a href="https://tonybai.com/2026/02/14/some-changes-in-go-1-26/">Go 1.26 如约而至</a>。伴随着 new(expr) 语法糖的引入、<a href="https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/">Green Tea GC 的全面转正</a>，以及<a href="https://tonybai.com/2026/02/19/using-go-fix-to-modernize-go-code">go fix 现代化重构</a>等一系列重磅特性，许多 Gopher 都按捺不住尝鲜的冲动。</p>
<p>然而，在经验丰富的 Go 团队和架构师群体中，流传着一条不成文的“潜规则”：<strong>永远不要在生产环境第一时间升级 X.Y.0 大版本，至少等到 X.Y.1 补丁发布后再做决定。</strong></p>
<p>这条潜规则并非空穴来风。Go 的 1.N.0 版本虽然经过了长达半年的开发和 RC 阶段的测试，但只有当它真正被全球几百万开发者投入到千奇百怪的生产环境中时，那些隐藏在深处的边界 Bug 才会浮出水面。而 1.N.1 版本，正是官方对这“第一波真实世界火力测试”所暴露问题的集中修复。</p>
<p>因此，一个非常客观且有趣的推论诞生了：观察 1.N.1 里程碑下的 Issue 数量，可以作为衡量 1.N.0 初始质量的一张“晴雨表”。</p>
<p>最近，我在例行了解 Go 官方仓库的 GitHub 里程碑数据时，发现了一个令人警惕的信号：<a href="https://github.com/golang/go/milestone/424">Go 1.26.1</a> 的 Issue 数量，正在呈现出明显的“异常峰值”。</p>
<p>本文将用真实的数据说话，带你横向拉网式对比 Go 1.17 到 Go 1.26 这五年间、共十个大版本的初期质量水平，并深度拆解这些 Issue 的具体成分。Go 1.26 到底稳不稳定？现在升级安全吗？答案就在这些数据里。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/agentic-software-engineering-qr.png" alt="" /></p>
<h2>核心数据全景：Go 1.26 的“异常峰值”</h2>
<p>为了得出客观的结论，我利用 GitHub cli端gh工具 提取了从 Go 1.17.1 到 Go 1.26.1 的完整里程碑数据。这跨越了 Go 语言 2021 年至 2026 年的五年黄金发展期。</p>
<p>为了更直观地感受这组数据的冲击力，我们将其绘制成趋势图（数据采集于 2026 年 3 月4日晚）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-1-26-most-problematic-release-2.png" alt="" /></p>
<h3>从数据中读出的残酷真相</h3>
<p>仔细审视这组数据，我们可以得出几个不容忽视的结论：</p>
<ol>
<li>总量拉响警报：Go 1.26.1 的总 Issue 数目前已升至 39 个，直接打破了五年来历史最差的 Go 1.21.1 的记录（38 个）。这意味着它发布后暴露出的问题远超常规水平。</li>
<li>与“前任”形成鲜明对比：就在半年前发布的 Go 1.25，其 Go 1.25.1 补丁仅有 9 个 Issue，堪称近年来最稳定的“神仙版本”。Go 1.26 的问题数量是其四倍有余，这种断崖式的质量波动令人意外。</li>
<li>修复压力巨大：截至数据采集时，Go 1.26.1 仍有 17 个 Open Issue 亟待修复，官方团队正处于“救火”状态中，Go 1.26.1 补丁的发布可能还需要一些时间。</li>
</ol>
<p>初步结论：Go 1.26 大版本的初始质量（Initial Quality）存在明显瑕疵，社区踩坑率偏高。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-1-26-most-problematic-release-3.png" alt="" /><br />
<center>图Go 1.26.1 milestone下的issues列表</center></p>
<h2>深度挖掘：为什么 Go 1.26 成了“重灾区”？</h2>
<p>看到这里，你可能会问：Go 团队的开发流程一向严谨，为什么 1.26 会出现如此多的问题？</p>
<p>为了探寻真相，我没有停留在宏观数字上，而是将 Go 1.26.1 里程碑下的 <strong>全部 39 个 Issue</strong> 逐一扒开，按其性质进行了分类。不看不知道，一看吓一跳，这 39 个问题背后的成分大有玄机。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-1-26-most-problematic-release-4.png" alt="" /></p>
<p>通过分类数据，我们可以清晰地看到导致 Go 1.26 翻车的“三大元凶”：</p>
<h3>cmd/fix / modernize 相关：创新的“生长痛” (占比 33%)</h3>
<p>这是 Go 1.26 核心新特性——全新的 go fix 自动代码现代化工具——直接引发的问题（约 13 个）。</p>
<p>静态分析并自动修改代码是一把双刃剑。在真实世界极其复杂的抽象语法树（AST）场景中，go fix 暴露出了一些“好心办坏事”的边界 Bug。例如：</p>
<ul>
<li>stringsbuilder 重写规则破坏了某些合法代码。</li>
<li>rangeint 升级在某些跨平台场景下存在兼容问题。</li>
<li>minmax 替换规则意外破坏了 select 语句的结构。</li>
<li>waitgroup 检查器导致了误报的编译错误。</li>
<li>&#8230; &#8230;</li>
</ul>
<p>好消息是：这个类别虽然问题多，但大多数是被工具链“误伤”的语法层面的问题，且 绝大部分已被 Go 团队快速修复（目前该类别仅剩少数处于 Open 状态）。这说明 Go 团队对新特性的反馈响应非常迅速。</p>
<h3>compiler/runtime 相关：最令人担忧的核心动荡 (占比 44%)</h3>
<p>这是本次分析中<strong>最令人担忧的类别</strong>。多达 17 个 Issue 直指 Go 的心脏——编译器和运行时。</p>
<p>引入 Green Tea GC 全面转正、<a href="https://go.dev/blog/allocation-optimizations">栈分配优化</a>以及<a href="https://tonybai.com/2025/08/22/go-simd-package-preview">实验性的 SIMD</a> 等底层变动，不可避免地触碰了最敏感的神经：</p>
<ul>
<li>出现了多个 <strong>Internal Compiler Error (ICE)</strong>，这意味着编译器在处理特定代码时直接崩溃了。</li>
<li>曝出了 <strong>runtime segfault / panic</strong>，这是运行时层面的致命错误。</li>
<li>32 位架构上的 timespec 定义错误。</li>
<li>SIMD 实验特性的相关 Bug。</li>
</ul>
<p>这些直击核心的问题中，有大约一半目前仍处于 Open（待修复）状态。底层 Bug 的修复往往需要极其谨慎的测试和论证，这可能会直接影响 Go 1.26 在高并发、复杂内存场景下的稳定性。</p>
<h3>Regression (回归问题)：亮起最高级别的红灯 (占比 10%)</h3>
<p>虽然只有 4 个 Issue 被打上了 regression（回归）标签，但这是<strong>最严重的信号</strong>。回归意味着：<strong>在 Go 1.25 中能够正常编译和完美运行的代码，在什么都不改的情况下，升级到 Go 1.26 后却出错了！</strong></p>
<p>这打破了 Go 最引以为傲的“向后兼容”承诺。这些回归问题包括：</p>
<ul>
<li>Synology Linux 环境下 fork syscall 发生冲突。</li>
<li>32 位 Android 系统下的 seccomp 问题。</li>
<li>mipsle 架构下出现的 segfault。</li>
<li>Windows 平台上 os.RemoveAll 行为异常（已修复）。</li>
</ul>
<p>4 个 regression 问题中有 3 个至今尚未修复（Open）。这意味着，如果你恰好使用了相关的平台或系统接口，升级 Go 1.26 后将掉入一个“大坑”。</p>
<h3>数据背后的真相总结</h3>
<p>综合以上硬核拆解，我们得到了一张更为清晰的“风险热力图”：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-1-26-most-problematic-release-5.png" alt="" /></p>
<h2>理性决策：现在该升级 Go 1.26 吗？</h2>
<p>数据虽然冰冷，但它为我们的技术决策提供了极其理性的支撑。面对目前 Go 1.26 这样一份成分复杂的“体检报告”，我为不同场景的开发者提供以下实操建议：</p>
<h3>场景一：公司核心生产环境</h3>
<p><strong>强烈建议：暂缓升级，绝对按兵不动！</strong></p>
<p>不要拿核心业务去为新编译器和新 Runtime 做“小白鼠”。鉴于存在多个未解决的 Compiler/Runtime Bug 和严重的 Regression 问题，至少要等到 <strong>Go 1.26.1 正式发布</strong>，仔细阅读其 Release Notes 确认相关雷区被排除后，再做评估。如果可能的话，我甚至建议那些对稳定性要求极高的金融或电商系统，等到 <strong>Go 1.26.2</strong> 发布后再进行灰度迁移。</p>
<h3>场景二：团队的辅助工具 / 内部系统</h3>
<p><strong>建议：可以在本地或测试环境准备迁移，但不要上生产。</strong></p>
<p>现在是让团队架构师开始在本地测试 Go 1.26 兼容性的好时机。你可以利用这段时间跑一遍全量的单元测试和集成测试，看看新的 Green Tea GC 是否对你们的特定负载有负面影响，或者有没有踩中那几个未修复的 Regression 雷区。</p>
<h3>场景三：个人项目 / 新技术学习</h3>
<p><strong>建议：大胆升级，享受新特性。</strong></p>
<p>对于没有历史包袱的个人项目，new(expr) 和强大的 go fix 绝对值得立刻体验。遇到 Bug 怎么办？去 GitHub 提 Issue！这也是参与开源社区建设、为 Go 生态排雷的绝佳方式。</p>
<h2>小结：读懂版本号背后的语言演进</h2>
<p>软件工程没有魔法，没有哪个大版本能在经历了底层大换血后依然完美无瑕。</p>
<p>Go 1.26.1 高达 39 个的 Issue 数量，以及占比极高的底层 Runtime/Compiler 报错，并不是在说“Go 团队不行了”，而恰恰反映了这门语言仍在保持着<strong>极其旺盛的生命力、以及为了追求更高性能而积极重构底层债务的勇气</strong>。</p>
<p>只不过，作为一线开发者和架构师，我们需要学会读懂这些数据发出的信号。在“享受技术红利”和“保障业务稳定”之间，让数据帮助我们找到最完美的平衡点。</p>
<p>最后，做个小调查：你目前在使用 Go 的哪个版本？是否有计划在近期升级到 1.26？欢迎在评论区分享你的看法！</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/03/06/go-1-26-most-problematic-release/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>拒绝无效告警！用 Govulncheck 构建高信噪比的 Go 安全扫描工作流</title>
		<link>https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow/</link>
		<comments>https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow/#comments</comments>
		<pubDate>Wed, 25 Feb 2026 00:26:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AlertFatigue]]></category>
		<category><![CDATA[AutomatedUpdates]]></category>
		<category><![CDATA[CallGraph]]></category>
		<category><![CDATA[CI/CD]]></category>
		<category><![CDATA[CVE]]></category>
		<category><![CDATA[Dependabot]]></category>
		<category><![CDATA[DependencyManagement]]></category>
		<category><![CDATA[FalsePositives]]></category>
		<category><![CDATA[FilippoValsorda]]></category>
		<category><![CDATA[GitHubActions]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoLanguage]]></category>
		<category><![CDATA[GoSecurity]]></category>
		<category><![CDATA[govulncheck]]></category>
		<category><![CDATA[Go安全]]></category>
		<category><![CDATA[Go语言]]></category>
		<category><![CDATA[OSv]]></category>
		<category><![CDATA[ReachabilityAnalysis]]></category>
		<category><![CDATA[Sandboxing]]></category>
		<category><![CDATA[SCA]]></category>
		<category><![CDATA[SecurityAudit]]></category>
		<category><![CDATA[ShiftLeft]]></category>
		<category><![CDATA[SignaltoNoiseRatio]]></category>
		<category><![CDATA[StaticAnalysis]]></category>
		<category><![CDATA[SupplyChainSecurity]]></category>
		<category><![CDATA[SymbolReachability]]></category>
		<category><![CDATA[VulnerabilityDatabase]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[信噪比]]></category>
		<category><![CDATA[假阳性]]></category>
		<category><![CDATA[可达性分析]]></category>
		<category><![CDATA[告警疲劳]]></category>
		<category><![CDATA[安全审计]]></category>
		<category><![CDATA[安全左移]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[沙箱]]></category>
		<category><![CDATA[漏洞数据库]]></category>
		<category><![CDATA[符号可达性]]></category>
		<category><![CDATA[自动更新]]></category>
		<category><![CDATA[调用图]]></category>
		<category><![CDATA[软件供应链安全]]></category>
		<category><![CDATA[软件成分分析]]></category>
		<category><![CDATA[静态分析]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5943</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow 大家好，我是Tony Bai。 在当今的软件开发流程中，持续集成/持续部署（CI/CD）和自动化的安全左移（Shift Left）已经成为行业共识。在这个大背景下，诸如 GitHub Dependabot 这样的自动化依赖更新工具应运而生，并迅速占据了几乎每一个开源项目和商业级代码库的 Repository 设置。它们不知疲倦地扫描 go.mod，一旦发现有依赖项爆出 CVE 漏洞，就会自动生成一个拉取请求（Pull Request, PR），仿佛是在告诉你：“别担心，我已经帮你修好了。” 然而，事实真的如此美好吗？ 近日，密码学领域的权威专家、前 Google Go 安全团队负责人 Filippo Valsorda 在其个人博客上发表了一篇极具冲击力的文章，标题直截了当：“TURN DEPENDABOT OFF”（关掉 Dependabot）。他毫不客气地指出，这款被无数开发者信赖的工具，实际上是一个“噪音制造机”（Noise Machine）。它不仅浪费了开发者的宝贵精力，更在无形中损害了整个 Go 生态系统的安全根基。 作为 Go 开发者，我们该如何审视这种看似“政治正确”的安全自动化工具？如果不使用 Dependabot，我们又该如何保卫代码库的安全？本文将深度剖析 Filippo 的核心观点，揭示传统版本比对扫描的致命缺陷，并手把手教你如何利用官方推荐的 govulncheck 构建真正高效、高信噪比的现代化 Go 安全扫描工作流。 安全自动化的幻象与“告警疲劳” 为了理解 Filippo 为什么如此强烈地反对 Dependabot 这种类型的扫描工具，我们需要先剖析软件工程心理学中的一个经典问题：告警疲劳（Alert Fatigue）。 什么是告警疲劳？ 告警疲劳是指操作人员或开发人员在长时间暴露于频繁且大量低价值（即假阳性、False Positives）的系统警告下，逐渐变得对这些警告麻木、脱敏的现象。 在医疗领域，如果重症监护室的心电监护仪总是因为轻微干扰而发出刺耳的警报声，护士最终可能会忽略真正的病危信号；在网络安全领域，如果防火墙每天产生一万条拦截记录，安全分析师就不可能从中挑出那一条真正的 APT 高级持续性威胁。 图：Dependabot [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/govulncheck-high-signal-to-noise-ratio-security-workflow-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow">本文永久链接</a> &#8211; https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow</p>
<p>大家好，我是Tony Bai。</p>
<p>在当今的软件开发流程中，持续集成/持续部署（CI/CD）和自动化的安全左移（Shift Left）已经成为行业共识。在这个大背景下，诸如 GitHub Dependabot 这样的自动化依赖更新工具应运而生，并迅速占据了几乎每一个开源项目和商业级代码库的 Repository 设置。它们不知疲倦地扫描 go.mod，一旦发现有依赖项爆出 CVE 漏洞，就会自动生成一个拉取请求（Pull Request, PR），仿佛是在告诉你：“别担心，我已经帮你修好了。”</p>
<p>然而，事实真的如此美好吗？</p>
<p>近日，密码学领域的权威专家、前 Google Go 安全团队负责人 Filippo Valsorda 在其个人博客上发表了一篇<a href="https://words.filippo.io/dependabot/">极具冲击力的文章</a>，标题直截了当：<strong>“TURN DEPENDABOT OFF”（关掉 Dependabot）</strong>。他毫不客气地指出，这款被无数开发者信赖的工具，实际上是一个“噪音制造机”（Noise Machine）。它不仅浪费了开发者的宝贵精力，更在无形中损害了整个 Go 生态系统的安全根基。</p>
<p>作为 Go 开发者，我们该如何审视这种看似“政治正确”的安全自动化工具？如果不使用 Dependabot，我们又该如何保卫代码库的安全？本文将深度剖析 Filippo 的核心观点，揭示传统版本比对扫描的致命缺陷，并手把手教你如何利用官方推荐的 govulncheck 构建真正高效、高信噪比的现代化 Go 安全扫描工作流。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-crypto-101-qr.png" alt="" /></p>
<h2>安全自动化的幻象与“告警疲劳”</h2>
<p>为了理解 Filippo 为什么如此强烈地反对 Dependabot 这种类型的扫描工具，我们需要先剖析软件工程心理学中的一个经典问题：<strong>告警疲劳（Alert Fatigue）</strong>。</p>
<h3>什么是告警疲劳？</h3>
<p>告警疲劳是指操作人员或开发人员在长时间暴露于频繁且大量低价值（即假阳性、False Positives）的系统警告下，逐渐变得对这些警告麻木、脱敏的现象。</p>
<p>在医疗领域，如果重症监护室的心电监护仪总是因为轻微干扰而发出刺耳的警报声，护士最终可能会忽略真正的病危信号；在网络安全领域，如果防火墙每天产生一万条拦截记录，安全分析师就不可能从中挑出那一条真正的 APT 高级持续性威胁。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/govulncheck-high-signal-to-noise-ratio-security-workflow-2.png" alt="" /><br />
<center>图：Dependabot alerts</center></p>
<p>在软件开发中，Dependabot 完美地扮演了那个“总是狼来了”的角色。它带来的不是安全感，而是一种<strong>虚假的工作充实感</strong>。正如 Filippo 所言：“它让你感觉自己好像在做有用的工作，但实际上你是在阻碍真正有用的工作。”</p>
<h3>传统版本扫描的致命缺陷：一刀切的模块级匹配</h3>
<p>Dependabot 和大多数传统的软件成分分析（SCA）工具一样，其工作原理极其简单粗暴，可以概括为<strong>基于版本的字符串比对</strong>。</p>
<p>以 Go 语言为例，它们的逻辑是这样的：<br />
1. 解析你的 go.mod 和 go.sum 文件，列出你所使用的所有依赖模块（Module）及其版本（如 github.com/foo/bar v1.0.0）。<br />
2. 查询公共漏洞数据库（如 NVD）。<br />
3. 如果数据库显示 github.com/foo/bar 在 &lt; v1.2.0 时存在某个漏洞，且你的版本在这个范围内，立刻生成一个高危告警，并创建一个将版本升级到 v1.2.0 的 PR。</p>
<p>在某些动态类型语言（如 Ruby 或早期 JavaScript）生态中，这种方法或许是唯一可行的。但在 Go 语言这样强调静态类型、拥有明确抽象边界和包级结构的生态中，这种“模块级”的一刀切匹配就显得极其愚蠢和低效。</p>
<h3>真实案例分析：edwards25519 漏洞风波</h3>
<p>为了让这个问题更加具象化，Filippo 在文章中分享了一个他亲身经历的“案发现场”。</p>
<p>不久前，Filippo 为他维护的密码学基础库 filippo.io/edwards25519 发布了一个安全修复版本（v1.1.1）。这个库在 Go 生态中举足轻重，被数十万个开源项目间接依赖。然而，这个漏洞的触发条件极其苛刻：</p>
<p>漏洞仅存在于 (*Point).MultiScalarMult 这个非常高级且罕用的 API 方法中，且只有当该方法的接收者（Receiver）不是初始的 identity point 时才会产生未定义的行为。</p>
<p><strong>现实情况是：在整个 Go 生态系统中，几乎没有任何项目实际调用了这个存在缺陷的特定方法。</strong> 大多数依赖该库的项目（比如著名的 github.com/go-sql-driver/mysql 库，拥有 22.8 万以上的依赖者）仅仅是导入了该库的其他基础功能，与有漏洞的代码路径八竿子打不着。</p>
<p><strong>Dependabot 的反应是什么？</strong></p>
<p>灾难性的噪音。Dependabot 不分青红皂白，仅仅因为版本号低于 v1.1.1，就向 GitHub 上的数千个甚至根本不受影响的 Repository 发送了疯狂的更新 PR。更糟糕的是，这些 PR 附带了由算法自动生成的、耸人听闻的、根本不合逻辑的 CVSS v4 漏洞评分，以及所谓的“73% 兼容性风险警告”。</p>
<p>结果就是，无数个深夜，开源项目的维护者们收到了刺耳的安全警报，被迫中断手中的工作，去 review 一个修改了一行他们压根用不到的代码的依赖升级 PR。如果他们不合并，项目上就会一直挂着一个红色的“安全风险”标签；如果他们机械地合并了，这就成了“告警疲劳”的典型发作。</p>
<p>Filippo 一针见血地指出这种行为的荒谬性：</p>
<blockquote>
<p>“由于扫描器未能过滤掉无关的漏洞，这种额外的劳作被硬生生地扔到了开源维护者的脚下，这是不可持续的。<strong>维护者的责任是确保项目不受安全漏洞影响；而扫描工具的责任是确保它们不会用假阳性告警去打扰用户。</strong>”</p>
</blockquote>
<p>当升级依赖（Dependency bump）成为一种应付扫描工具的机械动作，而不是基于对漏洞影响的真实评估（如是否需要轮换生产环境的密钥、是否需要通知受影响的用户），我们距离真正的安全就已经越来越远了。</p>
<h2>拥抱静态分析，Govulncheck 的降维打击</h2>
<p>既然基于版本的 Dependabot 如此不堪，我们应该如何科学地防范软件供应链安全风险？</p>
<p>答案是：抛弃盲目的版本匹配，<strong>使用严肃的、基于静态代码分析的漏洞扫描器。</strong> 计算机完全有能力为你完成过滤无用噪音的工作。在 Go 语言生态中，这个“杀手级”的工具就是官方出品的 <a href="https://tonybai.com/2022/09/10/an-intro-of-govulncheck">govulncheck</a>。</p>
<h3>丰富的 Go 官方漏洞数据库</h3>
<p>要实现精准的扫描，首先需要高质量的数据源。这正是 Filippo 在 2020 年至 2021 年领导 Go 安全团队时极力推动的战略——投入大量资源建设 <strong>Go 官方漏洞数据库（Go Vulnerability Database）</strong>。</p>
<p>与一般只记录模块版本和一段文字描述的 CVE 库不同，Go 漏洞数据库包含了极其丰富的、机器可读的元数据。它严格遵循标准的 OSV (Open Source Vulnerability) 格式。</p>
<p>让我们看看前面提到的 edwards25519 漏洞（GO-2026-4503）在数据库中的记录：</p>
<pre><code class="yaml">modules:
  - module: filippo.io/edwards25519
    versions:
      - fixed: 1.1.1
    vulnerable_at: 1.1.0
    packages:
      - package: filippo.io/edwards25519
        symbols:
          - Point.MultiScalarMult   # 关键所在：精确到了有漏洞的具体方法！
</code></pre>
<p>请注意最底部的 symbols 字段。Go 安全团队并没有笼统地标记整个模块不安全，而是像外科手术刀一样，精准定位到了那个有缺陷的方法 Point.MultiScalarMult。这就为后续的精准静态分析提供了弹药。</p>
<h3>Govulncheck 的核心优势：基于可达性分析</h3>
<p>有了精确到“符号（函数/方法）”级别的数据源，govulncheck 就可以对你的代码库施展“降维打击”了。相比于 Dependabot，它具有两大碾压级的优势：</p>
<h4>优势一：包级别的过滤</h4>
<p>Go 语言的模块通常由多个子包（Packages）组成，这是良好的代码组织习惯。如果一个漏洞发生在模块的 pkgA 中，而你的代码只导入了 pkgB，你显然是安全的。</p>
<p>任何合格的漏洞扫描器至少应该做到这一层过滤。实际上，这只需要执行一次简单的 go list -deps ./&#8230; 命令即可分析出包依赖关系。Dependabot 甚至连这基本的一步都没有做到，导致了大量的假阳性。</p>
<h4>优势二：基于调用图的符号可达性分析</h4>
<p>这是 govulncheck 引以为傲的黑科技。它不仅知道你引入了哪些包，它还会像编译器一样分析你的代码，构建出一棵完整的<strong>函数调用图（Call Graph）</strong>。</p>
<p>当扫描器运行时，它会沿着调用链路一路追溯：从你的 main 函数或测试入口开始，顺着你的业务逻辑，追踪到你调用的第三方库，再追踪到第三方库调用的更底层的库……</p>
<p>如果 govulncheck 发现，存在漏洞的那个特定函数（比如 Point.MultiScalarMult），在这棵庞大的调用树中根本不可达（即没有任何一条代码执行路径会调用到它），那么它就会保持沉默。</p>
<p>让我们看看实际的运行效果。如果你的项目只使用了 go-sql-driver/mysql，并且运行 govulncheck：</p>
<pre><code class="bash">$ govulncheck ./...
=== Symbol Results ===
No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 2
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
</code></pre>
<p>看，结果多么清爽！</p>
<p>govulncheck 明确地告诉你：“我看到了你的依赖树里有一个有漏洞的模块，但是不用慌，你的代码逻辑根本没有触碰到那个雷区，你是安全的。”</p>
<p>这种极高的信噪比，是 Dependabot 永远无法企及的。它把安全专家的宝贵时间，留给了真正需要紧急响应的致命漏洞，而不是在日常的升级杂务中消耗殆尽。</p>
<h2>重塑现代 Go 项目的 CI/CD 工作流</h2>
<p>如果你被 Filippo 的观点说服，决定彻底关闭 Dependabot 的安全警报，那么你必须建立一套更为科学的自动化机制来接管依赖管理和漏洞检测的工作。</p>
<p>Filippo 给出了非常具体的行动指南：用两个定时执行的 GitHub Actions 替换 Dependabot。</p>
<h3>行动一：部署独立的 Govulncheck 定时扫描任务</h3>
<p>你应该每天定时运行一次 govulncheck。它的作用是充当真正有价值的安全哨兵。</p>
<pre><code class="yaml">name: Govulncheck Scan
on:
  push:
    branches: [ "main" ]
  pull_request:
  schedule:
    # 每天 UTC 时间 10:22 执行
    - cron: '22 10 * * *'
  workflow_dispatch:

permissions:
  contents: read

jobs:
  govulncheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          persist-credentials: false

      - uses: actions/setup-go@v6
        with:
          go-version-file: go.mod

      - name: Run govulncheck
        run: |
          go run golang.org/x/vuln/cmd/govulncheck@latest ./...
</code></pre>
<p><strong>为什么这个 Action 不会自动开 PR？</strong></p>
<p>这是深思熟虑后的设计。如果 govulncheck 报警并导致 CI 失败，这意味着：你的代码明确且切实地调用了一个有已知漏洞的函数。</p>
<p>此时，情况已经相当严重了。你不能仅仅是指望像机器人一样点击“Merge”升级一个版本就万事大吉。你需要人类工程师介入：</p>
<ol>
<li>评估该漏洞在你的特定业务上下文中是否可被利用。</li>
<li>检查是否有数据泄露。</li>
<li>评估是否需要紧急轮换生产环境的数据库凭证、API 密钥或 JWT 签名密钥。</li>
<li>手动更新依赖，运行详尽的回归测试，然后再部署上线。</li>
</ol>
<p>把安全审计权交还给人类大脑，这才是对工程负责的态度。</p>
<h3>行动二：测试最新的依赖项，而不是盲目更新</h3>
<p>有人会反驳：可是 Dependabot 除了报安全漏洞，还能帮我们保持依赖常新，避免未来积累过多的技术债啊！</p>
<p>Filippo 认为，这种做法同样陷入了误区。</p>
<p>依赖的更新节奏，应当服从于你自身项目的开发周期和发布节奏，而不是被你的上游库作者的发布频率牵着鼻子走。例如，你应该在决定发布下一个主要版本时，集中精力进行一次依赖升级和全面测试，而不是天天被各种次要版本的更新 PR 打扰。</p>
<p>但是，保持对上游变化的敏感度同样重要。如果我们不天天更新，等真正需要安全更新时，可能会因为版本跨度太大而遭遇严重的 API 不兼容（Patch Delta 过大）。</p>
<p>Filippo 提出的巧妙解法是：每天在 CI 中，使用你所有依赖的最前沿版本运行一次你的测试套件。</p>
<pre><code class="yaml">name: Go Nightly Tests against Latest Dependencies
on:
  schedule:
    # 每天运行
    - cron: '22 10 * * *'

# ... 省略部分环境配置 ...

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        go:
          - { go-version: stable }
          - { go-version-file: go.mod }
        deps:
          - locked  # 针对锁定版本的 go.mod 运行测试
          - latest  # 针对最新版本依赖运行测试
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-go@v6
        with:
          go-version: ${{ matrix.go.go-version }}

      - name: Run tests with sandboxed CI environment
        uses: geomys/sandboxed-step@v1.2.1
        with:
          run: |
            if [ "${{ matrix.deps }}" = "latest" ]; then
              # 关键指令：将所有依赖临时拉取到最新版本，但不修改 go.mod
              go get -u -t ./...
            fi
            go test -v ./...
</code></pre>
<p>这种策略的双赢之处：</p>
<ol>
<li>零打断的早期预警：你的测试套件每天都在与最前沿的第三方代码搏斗。一旦某个上游库发布了一个引发不兼容的改动，你的每日 CI 就会立刻失败并向你报警，你可以在闲暇时从容应对，而不需要在某个紧急修复的当口被卡住。</li>
<li>极简的代码库：只要测试通过，你根本不需要去修改 go.mod 提交没必要的版本跳跃。你的仓库历史依然干净。</li>
</ol>
<p><strong>进阶安全提示：防范 CI 投毒</strong></p>
<p>当你在 CI 中运行 go get -u 时，你实际上是在无审查的情况下执行可能包含了恶意代码的第三方库（尤其是在执行测试时）。为了缓解供应链攻击带来的风险，Filippo 强烈推荐在执行此类测试时引入安全沙箱机制。在上述配置中，geomys/sandboxed-step 是一个基于 gVisor 的沙盒工具，它收回了工作流脚本对 GitHub 环境变量、机密信息以及不必要网络的访问权，确保即使拉取到了恶意的依赖包，它也无法窃取凭证或进行横向移动。这种防御深度，展现了前 Google 安全专家一贯的严谨。</p>
<h2>小结：让工具回归辅助的本位</h2>
<p>从盲目轻信机器人的批量 PR，到利用编译原理和图论（可达性分析）进行精准手术刀式的漏洞定位，Filippo Valsorda 给 Go 社区上了一堂生动的工程哲学课。</p>
<p>自动化绝不是推卸责任的借口。作为一个成熟的软件开发团队，我们应当停止对“警报数量”的崇拜，转而追求“警报质量”。关闭那些让你产生疲劳的噪音机器，配置好你的 govulncheck，把精力集中在真正需要人类智慧去解决的架构演进和安全设计上。</p>
<p>这不仅是 Go 语言最佳实践的一次更迭，更是我们在面对日益复杂的软件供应链时，应有的冷静与定力。</p>
<p>资料链接：https://words.filippo.io/dependabot/</p>
<hr />
<p><strong>你被 Dependabot “骚扰”过吗？</strong></p>
<p>自动生成的 PR 虽然方便，但也可能成为开发者的负担。在你的项目中，你是选择一键合并所有的安全更新，还是会仔细评估漏洞的真实影响？你会考虑关掉 Dependabot 的警报，转而投奔 Govulncheck 吗？</p>
<p>欢迎在评论区分享你的安全治理心得！</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Claude Code 官方最佳实践：50 条没人告诉你的“核心军规”</title>
		<link>https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules/</link>
		<comments>https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules/#comments</comments>
		<pubDate>Sun, 25 Jan 2026 00:24:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AgenticCodingEnvironment]]></category>
		<category><![CDATA[Antipatterns]]></category>
		<category><![CDATA[AutomatedWorkflow]]></category>
		<category><![CDATA[BestPractices]]></category>
		<category><![CDATA[CIIntegration]]></category>
		<category><![CDATA[Claude.md]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[CLITools]]></category>
		<category><![CDATA[CodeIntelligence]]></category>
		<category><![CDATA[CodeStyle]]></category>
		<category><![CDATA[configuration]]></category>
		<category><![CDATA[ContextWindow]]></category>
		<category><![CDATA[CoreRules]]></category>
		<category><![CDATA[CustomSubagents]]></category>
		<category><![CDATA[DeveloperExperience]]></category>
		<category><![CDATA[DomainKnowledge]]></category>
		<category><![CDATA[FanoutPattern]]></category>
		<category><![CDATA[HeadlessMode]]></category>
		<category><![CDATA[ImportRules]]></category>
		<category><![CDATA[ModelTier]]></category>
		<category><![CDATA[ParallelSessions]]></category>
		<category><![CDATA[PermissionsAllowlist]]></category>
		<category><![CDATA[PermissionsManagement]]></category>
		<category><![CDATA[PlanMode]]></category>
		<category><![CDATA[PromptEngineering]]></category>
		<category><![CDATA[RealtimeFeedback]]></category>
		<category><![CDATA[RootCause]]></category>
		<category><![CDATA[SandboxMode]]></category>
		<category><![CDATA[SelfCorrection]]></category>
		<category><![CDATA[StructuredOutput]]></category>
		<category><![CDATA[TaskDecomposition]]></category>
		<category><![CDATA[TaskFraming]]></category>
		<category><![CDATA[Verification]]></category>
		<category><![CDATA[WorkflowAutomation]]></category>
		<category><![CDATA[上下文窗口]]></category>
		<category><![CDATA[代码智能]]></category>
		<category><![CDATA[代码风格]]></category>
		<category><![CDATA[任务分解]]></category>
		<category><![CDATA[任务框架]]></category>
		<category><![CDATA[反模式]]></category>
		<category><![CDATA[命令行工具]]></category>
		<category><![CDATA[子智能体]]></category>
		<category><![CDATA[实时反馈]]></category>
		<category><![CDATA[导入规则]]></category>
		<category><![CDATA[并行会话]]></category>
		<category><![CDATA[开发者体验]]></category>
		<category><![CDATA[扇出模式]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[提示词工程]]></category>
		<category><![CDATA[无头模式]]></category>
		<category><![CDATA[最佳实践]]></category>
		<category><![CDATA[权限白名单]]></category>
		<category><![CDATA[权限管理]]></category>
		<category><![CDATA[核心军规]]></category>
		<category><![CDATA[根本原因]]></category>
		<category><![CDATA[模型层级]]></category>
		<category><![CDATA[沙箱模式]]></category>
		<category><![CDATA[流程自动化]]></category>
		<category><![CDATA[结构化输出]]></category>
		<category><![CDATA[自主智能体]]></category>
		<category><![CDATA[自动化工作流]]></category>
		<category><![CDATA[自我纠错]]></category>
		<category><![CDATA[计划模式]]></category>
		<category><![CDATA[配置文件]]></category>
		<category><![CDATA[项目宪法]]></category>
		<category><![CDATA[领域知识]]></category>
		<category><![CDATA[验证]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5772</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules 大家好，我是Tony Bai。 在使用 Claude Code 的过程中，你是否遇到过这种情况： 有时候它简直是神，几秒钟就能重构一个复杂的模块；但有时候它又蠢得让人抓狂，甚至会一本正经地写出跑不通的代码，或者把你刚刚纠正过的错误再犯一遍。 为什么？是模型不稳定吗？ 不，这通常是因为你的“打开方式”不对。 Claude Code 本质上是一个运行在 CLI 环境中的自主智能体（Agentic Coding Environment）。它受限于一个核心物理法则：上下文窗口（Context Window）。 上下文满了，它就会“失忆”。 指令不清晰，它就会“幻觉”。 没有反馈，它就会“盲目自信”。 为了帮你跨越从“新手”到“高玩”的门槛，我精读了 Anthropic 刚刚发布的官方最佳实践文档，并结合实战经验，提炼出了这 50 条核心军规。 掌握了它们，你就是指挥 AI 军团的编排者（Orchestrator）了。 基础心法——对抗熵增 (Foundational Tips) 核心逻辑： 上下文是稀缺资源，清晰度是最高杠杆。 Clear Task Framing： 开局第一句话决定成败。明确告诉它：Role（角色） + Task（任务） + Context（背景）。 Front Load Instructions： 最重要的约束（比如“绝对不要修改配置文件”），必须放在 Prompt 的最前面。 Verification (最高杠杆)： 这是最重要的 Tip。 必须给 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/claude-code-official-best-practices-50-core-rules-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules">本文永久链接</a> &#8211; https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules</p>
<p>大家好，我是Tony Bai。</p>
<p>在使用 Claude Code 的过程中，你是否遇到过这种情况：</p>
<p>有时候它简直是神，几秒钟就能重构一个复杂的模块；但有时候它又蠢得让人抓狂，甚至会一本正经地写出跑不通的代码，或者把你刚刚纠正过的错误再犯一遍。</p>
<p>为什么？是模型不稳定吗？</p>
<p>不，这通常是因为<strong>你的“打开方式”不对</strong>。</p>
<p>Claude Code 本质上是一个运行在 CLI 环境中的<strong>自主智能体（Agentic Coding Environment）</strong>。它受限于一个核心物理法则：<strong>上下文窗口（Context Window）</strong>。</p>
<ul>
<li>上下文满了，它就会“失忆”。</li>
<li>指令不清晰，它就会“幻觉”。</li>
<li>没有反馈，它就会“盲目自信”。</li>
</ul>
<p>为了帮你跨越从“新手”到“高玩”的门槛，我精读了 Anthropic 刚刚发布的<a href="https://code.claude.com/docs/en/best-practices">官方最佳实践文档</a>，并结合实战经验，提炼出了这 <strong>50 条核心军规</strong>。</p>
<p>掌握了它们，你就是指挥 AI 军团的<strong>编排者（Orchestrator）</strong>了。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/ai-app-dev-primer-qr.png" alt="" /></p>
<hr />
<h2>基础心法——对抗熵增 (Foundational Tips)</h2>
<p><strong>核心逻辑：</strong> 上下文是稀缺资源，清晰度是最高杠杆。</p>
<ol>
<li><strong>Clear Task Framing：</strong> 开局第一句话决定成败。明确告诉它：<strong>Role（角色） + Task（任务） + Context（背景）</strong>。</li>
<li><strong>Front Load Instructions：</strong> 最重要的约束（比如“绝对不要修改配置文件”），必须放在 Prompt 的最前面。</li>
<li><strong>Verification (最高杠杆)：</strong> <strong>这是最重要的 Tip。</strong> 必须给 Claude 一个“验证它自己工作”的方法。
<ul>
<li>❌ “修复这个 Bug。”</li>
<li>✅ “修复这个 Bug，并编写一个测试用例来验证修复是否成功。”</li>
</ul>
</li>
<li><strong>Provide Screenshots：</strong> 涉及 UI 修改，直接粘贴截图。Claude 现在的视觉能力极强，一张图胜过千言万语。</li>
<li><strong>Address Root Causes：</strong> 遇到报错，明确告诉它：“不要仅仅消除报错（Suppress），要解决根本原因。”</li>
<li><strong>Plan Mode (Shift+Tab)：</strong> 复杂任务（涉及 >2 个文件）必须先进 Plan 模式。
<ul>
<li><strong>Explore -> Plan -> Implement</strong>。</li>
</ul>
</li>
<li><strong>Review the Plan：</strong> 在它动手写代码前，先 Review 它的计划。这时候纠偏成本最低。</li>
<li><strong>One-shot vs Plan：</strong> 改个拼写错误？直接干。重构模块？必须 Plan。</li>
<li><strong>Specific Context：</strong> 不要让它通读整个仓库。用 @ 引用具体文件。</li>
<li><strong>Rich Content：</strong> 善用 cat error.log | claude，直接把日志管道喂给它。</li>
<li><strong>Clear Context：</strong> 任务做完了？立刻运行 /clear。不要在垃圾堆里盖新楼。</li>
<li><strong>Summarize Before Clear：</strong> 如果想保留记忆，先让它 /compact（压缩上下文），再继续。</li>
</ol>
<hr />
<h2>工程化配置——给 AI 立规矩 (Configuration)</h2>
<p><strong>核心逻辑：</strong> 不要每次都手动教，把规则固化到文件里。</p>
<ol>
<li><strong>CLAUDE.md 是宪法：</strong> 在根目录创建 CLAUDE.md，这是它每次启动必读的“员工手册”。</li>
<li><strong>Use /init：</strong> 运行 /init 命令，让 Claude 自动分析项目并生成初始的 CLAUDE.md。</li>
<li><strong>Prune Ruthlessly：</strong> <strong>CLAUDE.md 不要写废话！</strong>
<ul>
<li>❌ “请写出优雅的代码。”（浪费 Token）</li>
<li>✅ “使用 npm run test:unit 运行单元测试。”（高价值信息）</li>
</ul>
</li>
<li><strong>Bash Commands：</strong> 在文档里告诉它项目特有的命令（如构建、部署脚本）。</li>
<li><strong>Code Style：</strong> 明确约定：用 Tab 还是 Space？用 TypeScript 还是 JS？</li>
<li><strong>Import Rules：</strong> 告诉它 @src/ 别名指向哪里，避免它瞎猜路径。</li>
<li><strong>Child Directories：</strong> 对于 Monorepo，可以在子目录放单独的 CLAUDE.md，它会自动继承。</li>
<li><strong>Permissions Allowlist：</strong> 别做“点点点”工程师。用 /permissions 把 ls, grep, npm test 加入白名单。</li>
<li><strong>Sandbox Mode：</strong> 对于不信任的任务，开启 /sandbox，让它在隔离环境中撒欢。</li>
<li><strong>Dangerously Skip：</strong> 只有在完全可控（断网/沙箱）时，才使用 &#8211;dangerously-skip-permissions。</li>
<li><strong>CLI Tools：</strong> 安装 gh (GitHub CLI)，让 Claude 能直接提 PR、看 Issue。</li>
<li><strong>MCP Connect：</strong> 使用 claude mcp add 连接 Postgres 或 Notion。数据不再是孤岛。</li>
<li><strong>Learn CLI：</strong> 不知道怎么用某个工具？让 Claude 先运行 tool &#8211;help 自学。</li>
</ol>
<hr />
<h2>技能与自动化——扩展能力 (Skills &amp; Automation)</h2>
<p><strong>核心逻辑：</strong> 把重复的流程封装成“技能”，把 AI 集成到流水线。</p>
<ol>
<li><strong>Skills Definition：</strong> 在 .claude/skills/ 下创建 SKILL.md，定义可复用的能力。</li>
<li><strong>Domain Knowledge：</strong> 把复杂的业务逻辑（如“订单状态流转规则”）封装成 Skill，用到时才加载。</li>
<li><strong>Disable Model Invocation：</strong> 对于高风险 Skill，设置 disable-model-invocation: true，强制人工确认。</li>
<li><strong>Custom Subagents：</strong> 定义专门的 .claude/agents/security-reviewer.md。
<ul>
<li>让它扮演“安全专家”，只负责 Review，不负责写代码。</li>
</ul>
</li>
<li><strong>Delegate to Subagents：</strong> 在主会话中说：“用 security-reviewer 检查刚才的代码。”</li>
<li><strong>Install Plugins：</strong> 运行 /plugin，去市场找现成的技能包（如 Python 代码分析）。</li>
<li><strong>Code Intelligence Plugin：</strong> 必装！给 Claude 提供“跳转定义”和“查找引用”的能力（基于 LSP）。</li>
<li><strong>Hooks：</strong> 设置 .claude/settings.json 中的 Hooks。
<ul>
<li>例如：每次 Auto-fix 后自动运行 Lint。</li>
</ul>
</li>
<li><strong>Headless Mode：</strong> claude -p “prompt”。这是自动化的神器。</li>
<li><strong>CI Integration：</strong> 在 GitHub Actions 里用 Headless Mode 自动 Review PR。</li>
<li><strong>Structured Output：</strong> 使用 &#8211;output-format json，让脚本能解析 Claude 的回答。</li>
<li><strong>Fan-out Pattern：</strong> 批量修改 100 个文件？写个 Shell 脚本循环调用 claude -p。</li>
</ol>
<hr />
<h2>避坑指南——反模式 (Anti-patterns)</h2>
<p><strong>核心逻辑：</strong> 识别“失败的味道”，及时止损。</p>
<ol>
<li><strong>The Kitchen Sink Session：</strong> 试图在一个 Session 里修 Bug、写新功能、又写文档。
<ul>
<li><strong>后果：</strong> 上下文污染，智商直线下降。</li>
<li><strong>解法：</strong> 一事一议，做完就 /clear。</li>
</ul>
</li>
<li><strong>Over-correcting：</strong> 纠正了两次还不对？
<ul>
<li><strong>后果：</strong> 错误路径被强化，越改越错。</li>
<li><strong>解法：</strong> 别纠缠！直接 /clear，优化 Prompt 后重来。</li>
</ul>
</li>
<li><strong>The Trust-then-Verify Gap：</strong> 还没测试就觉得“看起来是对的”。
<ul>
<li><strong>后果：</strong> 生产环境事故。</li>
<li><strong>解法：</strong> 没有 Pass 测试的代码，一行都别信。</li>
</ul>
</li>
<li><strong>The Infinite Exploration：</strong> 让它“调查一下代码库”，不给范围。
<ul>
<li><strong>后果：</strong> 读了几百个文件，Token 耗尽，还没开始干活。</li>
<li><strong>解法：</strong> 限制搜索范围，或者用 Subagent 去做调研。</li>
</ul>
</li>
<li><strong>Vague Error Reporting：</strong> 只说“不行”或“报错了”。
<ul>
<li><strong>后果：</strong> Claude 只能瞎猜。</li>
<li><strong>解法：</strong> 粘贴完整的 Stack Trace。</li>
</ul>
</li>
</ol>
<hr />
<h2>高阶操作——神级技巧 (Pro Moves)</h2>
<ol>
<li><strong>Resume Session：</strong> 昨天没干完？claude &#8211;resume 接着聊。</li>
<li><strong>Rename Session：</strong> 用 /rename 给会话起个好名字（如 feat-login-oauth），方便找回。</li>
<li><strong>Rewind (Esc+Esc)：</strong> 走错方向了？双击 Esc 回滚到上一步，比改代码快。</li>
<li><strong>Let Claude Interview You：</strong> 不知道怎么写 Spec？
<ul>
<li>Prompt：<em>“我想做个 X 功能。请作为一个资深架构师，向我提问，直到你觉得信息足够写出 Spec 为止。”</em></li>
</ul>
</li>
<li><strong>Self-Correction Loop：</strong> 让它自己改自己的作业。
<ul>
<li>Prompt：<em>“分析你刚才生成的代码，找出 3 个潜在的 Edge Case，并修复它们。”</em></li>
</ul>
</li>
<li><strong>Model Tier Selection：</strong> 简单的 Lint 修复用 Haiku（快且便宜），架构设计用 Opus（聪明但贵）。</li>
<li><strong>Parallel Sessions：</strong> 开两个终端。一个写代码（Writer），一个做 Review（Reviewer）。左右互搏，质量倍增。</li>
<li><strong>Develop Intuition：</strong> 最后的建议——多用。建立对“上下文容量”和“模型能力边界”的体感。</li>
</ol>
<hr />
<h2>小结：从 直觉 到 方法论</h2>
<p>刚开始使用 Claude Code，你可能靠的是<strong>直觉</strong>。但要在大规模工程中稳定产出，你必须依靠<strong>方法论</strong>。</p>
<p>这 50 条军规，就是从“抽盲盒”走向“工业化生产”的桥梁。掌握了它们，你就不再是被动的 User，而是这支硅基军团的 <strong>Commander</strong>。</p>
<p>资料链接：https://code.claude.com/docs/en/best-practices</p>
<hr />
<p><strong>深度实战：构建你的“AI 原生工作流”</strong></p>
<p>Tip 只是冰山一角。真正的威力在于将这些技巧组合成一套<strong>“开发工作流”</strong>。</p>
<p>在我的极客时间专栏<strong>《<a href="http://gk.link/a/12EPd">AI 原生开发工作流实战</a>》</strong>中，我将带你实战演示：</p>
<ul>
<li>CLAUDE.md 实战：如何从零编写一个完美的、模块化的项目宪法？</li>
<li>驾驭Claude Code：实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p><strong>别再用蛮力写代码了。扫描下方二维码，学会用 AI 的杠杆。</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 考古：Go 官方如何决定支持你的 CPU 和 OS？</title>
		<link>https://tonybai.com/2026/01/01/go-archaeology-porting-policy/</link>
		<comments>https://tonybai.com/2026/01/01/go-archaeology-porting-policy/#comments</comments>
		<pubDate>Thu, 01 Jan 2026 05:16:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AlpineLinux]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[BlockReleases]]></category>
		<category><![CDATA[BrokenPorts]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[FirstClassPorts]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GOARCH]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoogleGoTeam]]></category>
		<category><![CDATA[GOOS]]></category>
		<category><![CDATA[GoPortingPolicy]]></category>
		<category><![CDATA[musl]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[port]]></category>
		<category><![CDATA[proposal]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[SecondaryPorts]]></category>
		<category><![CDATA[x/sys]]></category>
		<category><![CDATA[一等公民]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[提案]]></category>
		<category><![CDATA[机器码]]></category>
		<category><![CDATA[构建机器]]></category>
		<category><![CDATA[次要组合]]></category>
		<category><![CDATA[治理智慧]]></category>
		<category><![CDATA[硬件消亡]]></category>
		<category><![CDATA[稳定性承诺]]></category>
		<category><![CDATA[系统调用]]></category>
		<category><![CDATA[线程调度]]></category>
		<category><![CDATA[维护成本]]></category>
		<category><![CDATA[跨平台编译]]></category>
		<category><![CDATA[运行时]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5647</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/01/01/go-archaeology-porting-policy 大家好，我是Tony Bai。 当我们津津乐道于 Go 语言强大的跨平台编译能力——只需一个 GOOS=linux GOARCH=amd64 就能在 Mac 上编译出 Linux Go程序时，你是否想过，这些操作系统和 CPU 架构的组合（Port）是如何被选入 Go 核心代码库的？ 为什么 linux/amd64 稳如泰山，而 darwin/386 却消失在历史长河中？为什么新兴的 linux/riscv64 或 linux/loong64 能被接纳？ 这一切的背后，都遵循着一份严谨的 Go Porting Policy。今天，我们就来翻开这份“法典”，一探究竟。 什么是“Port”？ 在 Go 的语境下，一个 Port 指的是 操作系统 (OS) 与 处理器架构 (Architecture) 的特定组合。例如： linux/amd64：运行在 64 位 x86 处理器上的 Linux。 windows/arm64：运行在 ARM64 处理器上的 Windows。 每一个 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/go-archaeology-porting-policy-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/01/01/go-archaeology-porting-policy">本文永久链接</a> &#8211; https://tonybai.com/2026/01/01/go-archaeology-porting-policy</p>
<p>大家好，我是Tony Bai。</p>
<p>当我们津津乐道于 Go 语言强大的跨平台编译能力——只需一个 GOOS=linux GOARCH=amd64 就能在 Mac 上编译出 Linux Go程序时，你是否想过，这些操作系统和 CPU 架构的组合（Port）是如何被选入 Go 核心代码库的？</p>
<p>为什么 linux/amd64 稳如泰山，而 darwin/386 却消失在历史长河中？为什么新兴的 linux/riscv64 或 linux/loong64 能被接纳？</p>
<p>这一切的背后，都遵循着一份严谨的 <strong><a href="https://go.dev/wiki/PortingPolicy">Go Porting Policy</a></strong>。今天，我们就来翻开这份“法典”，一探究竟。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/system-programming-in-go-pr.png" alt="" /></p>
<h2>什么是“Port”？</h2>
<p>在 Go 的语境下，一个 <strong>Port</strong> 指的是 <strong>操作系统 (OS)</strong> 与 <strong>处理器架构 (Architecture)</strong> 的特定组合。例如：</p>
<ul>
<li>linux/amd64：运行在 64 位 x86 处理器上的 Linux。</li>
<li>windows/arm64：运行在 ARM64 处理器上的 Windows。</li>
</ul>
<p>每一个 Port 的引入，都意味着 Go 编译器后端需要生成对应的机器码，运行时（Runtime）需要处理特定的系统调用、内存管理和线程调度。这是一项巨大的工程。</p>
<h2>等级森严：First-Class Ports (一等公民)</h2>
<p>Go 官方将 Ports 分为两类，这并非歧视，而是基于<strong>稳定性承诺</strong>和<strong>维护成本</strong>的考量。</p>
<p><strong>First-Class Ports</strong> 是 Go 官方（Google Go Team）承诺全力支持的平台。它们享有最高级别的待遇，也承担着最重的责任：</p>
<ol>
<li><strong>阻断发布 (Block Releases)</strong>：如果任何一个 First-Class Port 的构建或测试失败，Go 的新版本（包括 Beta 和 RC）就<strong>绝对不会发布</strong>。</li>
<li><strong>官方兜底</strong>：Google 的 Go 团队负责维护这些平台的构建机器（Builder），并对任何破坏这些平台的代码变更负责。</li>
</ol>
<p>目前的 <strong>First-Class Ports</strong> 名单（极少，只有核心的几个）：<br />
*   linux/amd64, linux/386, linux/arm, linux/arm64<br />
*   darwin/amd64, darwin/arm64 (macOS)<br />
*   windows/amd64, windows/386</p>
<blockquote>
<p><strong>冷知识</strong>：Linux 下只有使用 glibc 的系统才算 First-Class。使用 musl (如 Alpine Linux) 的并不在这个名单里，虽然它们通常也能工作得很好。</p>
</blockquote>
<h2>社区的力量：Secondary Ports (次要组合)</h2>
<p>除了上述几个“亲儿子”，Go 支持的几十种其他平台（如 freebsd/*, openbsd/*, netbsd/*, aix/*, illumos/*, plan9/*, js/wasm 等）都属于 <strong>Secondary Ports</strong>。</p>
<p>它们的生存法则完全不同：</p>
<ol>
<li><strong>社区维护制</strong>：必须至少有<strong>两名</strong>活跃的社区开发者签名画押，承诺维护这个 Port。</li>
<li><strong>不阻碍发布</strong>：如果一个次要 Port 的构建挂了，Go 官方<strong>不会</strong>为了它推迟版本发布。它可能会在 Release Note 中被标记为“Broken”甚至“Unsupported”。</li>
<li><strong>自备干粮</strong>：维护者必须提供并维护构建机器，接入 Go 的 CI 系统。</li>
</ol>
<p>这意味着，如果你想让 Go 支持一个冷门的嵌入式系统，你不仅要贡献代码，还得长期确保持续集成（CI）是绿的。</p>
<h2>优胜劣汰：如何新增与移除？</h2>
<h3>新增一个 Port</h3>
<p>想让 Go 支持一个新的芯片架构（比如龙芯 LoongArch）？流程是严格的：</p>
<ol>
<li><strong>提交 Proposal</strong>：论证这个 Port 的价值（潜在用户量）与维护成本的平衡。</li>
<li><strong>找人</strong>：指定至少两名维护者。</li>
<li><strong>先行</strong>：可以在 x/sys 库中先行验证对新Port系统调用的支持，甚至在构建机器跑通之前，代码不能合入主分支。</li>
</ol>
<h3>移除一个 Port (Broken Ports)</h3>
<p>Go 不会无限制地背负历史包袱。一个 Port 如果满足以下条件，可能会被移除：</p>
<ul>
<li><strong>构建失败且无人修</strong>：如果一个 Secondary Port 长期构建失败，且维护者失联，它会被标记为 Broken。如果在下一个大版本（1.N+1）发布前还没修好，就会被移除。</li>
<li><strong>硬件消亡</strong>：如果硬件都停产了（例如 IBM POWER5），Go 也没必要支持了。</li>
<li><strong>厂商放弃</strong>：如果 OS 厂商都不支持了（例如老版本的 macOS），Go 也会跟随弃用。</li>
</ul>
<p>这就是为什么 Go 在某个版本后不再支持 Windows XP 或 macOS 10.12 的原因——<strong>为了让有限的开发资源聚焦在更广泛使用的系统上。</strong></p>
<h2>小结</h2>
<p>Go 的 Porting Policy 展示了一个成熟开源项目的治理智慧：<strong>核心聚焦，边界开放，权责对等</strong>。</p>
<p>它保证了 Go 在主流平台上的坚如磐石，同时也通过社区机制，让 Go 的触角延伸到了无数小众和新兴的领域。下次当你为一个冷门平台编译 Go 程序成功时，别忘了感谢那些默默维护 Builder 的社区志愿者们。</p>
<p>参考资料：https://go.dev/wiki/PortingPolicy</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/01/01/go-archaeology-porting-policy/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>AI 让代码产出速度提升 10 倍，为什么我们的软件交付成功率却停滞不前？</title>
		<link>https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai/</link>
		<comments>https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai/#comments</comments>
		<pubDate>Sat, 18 Oct 2025 11:15:28 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Agentic开发系统]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AI编程助手]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[ContinuousIntegration]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[ExtremeProgramming]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言]]></category>
		<category><![CDATA[LargeLanguageModel]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[PairProgramming]]></category>
		<category><![CDATA[StandishCHAOS]]></category>
		<category><![CDATA[TDD]]></category>
		<category><![CDATA[TestDrivenDevelopment]]></category>
		<category><![CDATA[XP]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[产出的幻觉]]></category>
		<category><![CDATA[人类协作]]></category>
		<category><![CDATA[代码产出速度]]></category>
		<category><![CDATA[价值交付]]></category>
		<category><![CDATA[共享上下文]]></category>
		<category><![CDATA[刻意的摩擦]]></category>
		<category><![CDATA[反馈循环]]></category>
		<category><![CDATA[大语言模型]]></category>
		<category><![CDATA[失控的熵增]]></category>
		<category><![CDATA[小批量发布]]></category>
		<category><![CDATA[意大利面条式]]></category>
		<category><![CDATA[技术债]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[极限编程]]></category>
		<category><![CDATA[核心悖论]]></category>
		<category><![CDATA[测试驱动开发]]></category>
		<category><![CDATA[目标对齐]]></category>
		<category><![CDATA[结对编程]]></category>
		<category><![CDATA[自动化代码生成]]></category>
		<category><![CDATA[软件交付成功率]]></category>
		<category><![CDATA[软件垃圾]]></category>
		<category><![CDATA[软件生产力革命]]></category>
		<category><![CDATA[速度陷阱]]></category>
		<category><![CDATA[高价值交付]]></category>
		<category><![CDATA[麦肯锡]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5269</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai 大家好，我是Tony Bai。 AI 编程助手、自动化代码生成、Agentic 开发系统……我们正目睹一场由 AI 引领的软件生产力革命。代码的产出速度正以 5 倍、10 倍甚至更高的倍率疯狂增长。理论上，我们应该能更快、更好地交付软件。但现实却给了我们一记响亮的耳光：我们的软件交付成功率，数十年来几乎毫无寸进，甚至有所倒退。 这就是 AI 时代软件开发的核心悖论：我们获得了前所未有的“产出”速度，却未能将其转化为更高的“成功”概率。最近，一篇题为《我们是否应该在 AI 时代重温极限编程？》的文章深入探讨了这一现象。文章作者尖锐地指出，我们可能正陷入一个“速度陷阱”，用最先进的工具去解决一个早已不是瓶颈的问题。 本文将和大家一起解读一下这篇文章的核心论点，探讨为何“速度”本身无法带来成功，以及为什么作者认为，那条通往高价值交付的道路，可能需要我们重温极限编程（Extreme Programming, XP）的智慧。 产出的幻觉：我们一直在加速，却在原地打转 文章的核心论点始于一个简单而深刻的观察：代码的生成速度，从来就不是软件开发的根本瓶颈。作者回顾了过去几十年的技术演进，从高级语言到 DevOps，再到云原生，每一次变革都极大地提升了代码产出效率，而 AI 只是将这条“加速”之路推向了极致。 为了支撑这一观点，文章引用了多项权威数据，揭示了一个残酷的现实： 根据长期运行的 Standish CHAOS 研究报告和麦肯锡的分析，超过 70% 的数字化项目仍以失败告终。 从 1994 年到 2020 年，尽管工具链发生了翻天覆地的变化，但项目按时、按预算成功交付的比例净增长微乎其微。 作者由此得出结论：我们只是在更快地制造砖块，却不知道如何用它们建起一座坚固、美观且符合用户需求的房子。当 AI 将制造砖块的成本降至接近于零时，设计的蓝图、工匠的协作和地基的稳固，就成了决定成败的唯一关键。 失控的熵增：AI 如何放大我们最坏的习惯 在文章的分析中，最一针见血的部分莫过于其对 AI 风险的论述。作者认为，当代码生成变得毫不费力时，一个更致命的风险随之而来：我们生产软件垃圾的速度，远远超过了我们验证和清理它的速度。 在没有严格约束的情况下，文章指出 AI 会成为“坏习惯”的放大器： 快速堆积技术债: AI 可以迅速生成大量未经深思熟虑的逻辑，形成一个无人能懂、难以维护的“意大利面条式”代码迷宫。 固化错误的假设: 作者引用了近期研究，表明大语言模型（LLM）的准确性会随着上下文窗口的增长而下降。这意味着 AI [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/revisit-extreme-programming-in-the-age-of-ai-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai">本文永久链接</a> &#8211; https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai</p>
<p>大家好，我是Tony Bai。</p>
<p>AI 编程助手、自动化代码生成、Agentic 开发系统……我们正目睹一场由 AI 引领的软件生产力革命。代码的产出速度正以 5 倍、10 倍甚至更高的倍率疯狂增长。理论上，我们应该能更快、更好地交付软件。但现实却给了我们一记响亮的耳光：<strong>我们的软件交付成功率，数十年来几乎毫无寸进，甚至有所倒退。</strong></p>
<p>这就是 AI 时代软件开发的核心悖论：我们获得了前所未有的“产出”速度，却未能将其转化为更高的“成功”概率。最近，一篇题为《<a href="https://www.hyperact.co.uk/blog/should-we-revisit-xp-in-the-age-of-ai">我们是否应该在 AI 时代重温极限编程？</a>》的文章深入探讨了这一现象。<strong>文章作者尖锐地指出</strong>，我们可能正陷入一个“速度陷阱”，用最先进的工具去解决一个早已不是瓶颈的问题。</p>
<p>本文将和大家一起解读一下这篇文章的核心论点，探讨为何“速度”本身无法带来成功，以及为什么作者认为，那条通往高价值交付的道路，可能需要我们重温<a href="http://www.extremeprogramming.org/">极限编程（Extreme Programming, XP）</a>的智慧。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-micro-column-2025-pr.png" alt="" /></p>
<h2>产出的幻觉：我们一直在加速，却在原地打转</h2>
<p>文章的核心论点始于一个简单而深刻的观察：代码的生成速度，从来就不是软件开发的根本瓶颈。作者回顾了过去几十年的技术演进，从高级语言到 DevOps，再到云原生，每一次变革都极大地提升了代码产出效率，而 AI 只是将这条“加速”之路推向了极致。</p>
<p><strong>为了支撑这一观点，文章引用了多项权威数据</strong>，揭示了一个残酷的现实：</p>
<ul>
<li>根据长期运行的 <strong>Standish CHAOS</strong> 研究报告和麦肯锡的分析，<strong>超过 70% 的数字化项目仍以失败告终</strong>。</li>
<li>从 1994 年到 2020 年，尽管工具链发生了翻天覆地的变化，但项目按时、按预算成功交付的比例净增长微乎其微。</li>
</ul>
<p><strong>作者由此得出结论</strong>：我们只是在更快地制造砖块，却不知道如何用它们建起一座坚固、美观且符合用户需求的房子。当 AI 将制造砖块的成本降至接近于零时，设计的蓝图、工匠的协作和地基的稳固，就成了决定成败的唯一关键。</p>
<h2>失控的熵增：AI 如何放大我们最坏的习惯</h2>
<p>在文章的分析中，最一针见血的部分莫过于其对 AI 风险的论述。作者认为，当代码生成变得毫不费力时，一个更致命的风险随之而来：<strong>我们生产软件垃圾的速度，远远超过了我们验证和清理它的速度。</strong></p>
<p>在没有严格约束的情况下，<strong>文章指出 AI 会成为“坏习惯”的放大器</strong>：</p>
<ol>
<li><strong>快速堆积技术债:</strong> AI 可以迅速生成大量未经深思熟虑的逻辑，形成一个无人能懂、难以维护的“<a href="https://tonybai.com/2025/07/16/when-spaghetti-code-knocks">意大利面条式</a>”代码迷宫。</li>
<li><strong>固化错误的假设:</strong> <strong>作者引用了近期研究</strong>，表明大语言模型（LLM）的准确性会随着上下文窗口的增长而下降。这意味着 AI 极易在长链条的生成中引入微小错误，并基于这些错误继续构建，最终导致整个系统的脆弱性。</li>
<li><strong>绕过人类协作:</strong> <strong>文章还表达了一种担忧</strong>，即开发者可能会倾向于“与 AI 结对”，而不是与同事协作，这将严重削弱团队的<strong>共享上下文（Shared Context）</strong>——这是解决复杂问题、确保软件长期健康的最宝贵资产。</li>
</ol>
<p><strong>文章的观点是，AI 让我们以前所未有的速度，构建出我们自己都无法理解和控制的复杂系统，而这恰恰是极限编程（XP）从诞生之日起就致力于解决的“失控的熵增”问题。</strong></p>
<h2>XP 的反向智慧：唯一的出路是“刻意放慢”</h2>
<p>面对这种由 AI 加剧的困境，<strong>文章提出了一个看似有悖常理的解决方案：拥抱极限编程（XP）的反向智慧，即通过“刻意的摩擦”来“刻意放慢”。</strong></p>
<p><strong>作者对 XP 的核心实践进行了重新解读：</strong></p>
<ul>
<li><strong>结对编程 (Pair Programming):</strong> 它被描述为一种内置的实时代码审查、知识传递和风险对冲机制，其目的不是减慢速度，而是强制建立共享上下文。</li>
<li><strong>测试驱动开发 (TDD):</strong> <strong>文章强调</strong>，TDD 强迫我们将关注点从“实现”拉回到“意图”，在写任何功能代码前，先定义清楚“我们到底想让系统做什么”。</li>
<li><strong>持续集成 (CI) 与小批量发布:</strong> 这些实践被视为创建短而快的反馈循环的关键，使团队能以最小的成本发现错误、验证假设并调整方向。</li>
</ul>
<p>在作者看来，XP 的所有实践都在为一个终极目标服务：通过极致的沟通、简约的设计和快速的反馈，来对抗软件开发中固有的不确定性。</p>
<h2>小结：答案在人，不在代码</h2>
<p>回到最初的问题：<strong>AI 带来了 10 倍的速度，为何成功率停滞不前？</strong></p>
<p>《我们是否应该在 AI 时代重温极限编程？》这篇文章给出的答案清晰而坚定：因为我们错误地将“代码产出”等同于“价值交付”。作者在文末总结道，软件开发的真正瓶颈，从来都不是写代码的速度，而是：</p>
<ul>
<li>我们是否在构建正确的东西？（目标对齐）</li>
<li>团队成员是否对目标和现状有共同的理解？（共享上下文）</li>
<li>我们能否快速、低成本地验证我们的想法？（反馈循环）</li>
</ul>
<p>AI 无法自动解决这些问题，甚至可能使它们恶化。因此，<strong>文章的最终呼吁是</strong>，在 AI 时代，最具竞争力的团队，不是那些使用 AI 写代码最快的团队，而是那些能将 AI 的强大生产力，置于一个高度纪律化、以人为本的协作框架之下的团队。</p>
<p>这篇充满洞察力的文章提醒我们：软件的终点是为人服务，它的过程也必须围绕人来构建。这或许才是打破“速度陷阱”，实现真正成功的唯一途径。</p>
<p>资料链接：https://www.hyperact.co.uk/blog/should-we-revisit-xp-in-the-age-of-ai</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>超越时间的智慧：重读那些定义了现代软件开发的经典文章</title>
		<link>https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me/</link>
		<comments>https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me/#comments</comments>
		<pubDate>Fri, 03 Oct 2025 23:20:37 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AlexisKing]]></category>
		<category><![CDATA[API设计]]></category>
		<category><![CDATA[BradFitzpatrick]]></category>
		<category><![CDATA[Bug数据库]]></category>
		<category><![CDATA[DanMcKinley]]></category>
		<category><![CDATA[DRY原则]]></category>
		<category><![CDATA[ErikKuefler]]></category>
		<category><![CDATA[FredBrooks]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofix指令]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go开发者]]></category>
		<category><![CDATA[JoelSpolsky]]></category>
		<category><![CDATA[Joel测试]]></category>
		<category><![CDATA[JuliaEvans]]></category>
		<category><![CDATA[RaymondChen]]></category>
		<category><![CDATA[serverless]]></category>
		<category><![CDATA[TerenceEden]]></category>
		<category><![CDATA[一步构建]]></category>
		<category><![CDATA[人月神话]]></category>
		<category><![CDATA[代码哲学]]></category>
		<category><![CDATA[健壮性]]></category>
		<category><![CDATA[偶然复杂性]]></category>
		<category><![CDATA[具名类型]]></category>
		<category><![CDATA[决策]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[创新代币]]></category>
		<category><![CDATA[前端框架]]></category>
		<category><![CDATA[单点故障]]></category>
		<category><![CDATA[原生JavaScript]]></category>
		<category><![CDATA[同理心]]></category>
		<category><![CDATA[向后兼容性]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[安全检查]]></category>
		<category><![CDATA[宽进严出]]></category>
		<category><![CDATA[工程文化]]></category>
		<category><![CDATA[并发模型]]></category>
		<category><![CDATA[技术选型]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[故障转移]]></category>
		<category><![CDATA[数字生活]]></category>
		<category><![CDATA[无聊技术]]></category>
		<category><![CDATA[本质复杂性]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[每日构建]]></category>
		<category><![CDATA[测试代码]]></category>
		<category><![CDATA[清晰性]]></category>
		<category><![CDATA[源码控制]]></category>
		<category><![CDATA[灾难恢复计划]]></category>
		<category><![CDATA[用户体验]]></category>
		<category><![CDATA[类型系统]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[表驱动测试]]></category>
		<category><![CDATA[规格说明文档]]></category>
		<category><![CDATA[路径依赖]]></category>
		<category><![CDATA[软件工程]]></category>
		<category><![CDATA[软件开发]]></category>
		<category><![CDATA[输入验证]]></category>
		<category><![CDATA[选项模式]]></category>
		<category><![CDATA[需求规格说明]]></category>
		<category><![CDATA[静态分析工具链]]></category>
		<category><![CDATA[静态类型语言]]></category>
		<category><![CDATA[韧性]]></category>
		<category><![CDATA[风险意识]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5223</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me 大家好，我是Tony Bai。 二十年前，一位年轻的程序员在还未踏入职场时，便开始沉浸于软件开发的博客文章与深刻思考之中。二十年后，他已成为一名资深工程师，回首望去，成千上万的文字中，只有寥寥数篇真正沉淀下来，如基石般塑造了他的思维方式和职业生涯。 这份由 Michael Lynch 精心筛选出的“思想塑造清单”，本身就是一次对软件工程领域永恒智慧的巡礼。清单中的每一篇文章，都如同一个思想的火种，点燃了关于工程文化、代码哲学、乃至技术选型的深刻辩论。 今天，也让我们重新打开这些经典，逐一剖析其中的智慧，看看它们在瞬息万变的当下，能为我们——尤其是追求简约与高效的 Go 开发者——带来怎样历久弥新的启示。 1. Joel 测试：衡量开发者幸福感的 12 条黄金标准 (“The Joel Test: 12 Steps to Better Code” by Joel Spolsky, 2000) Joel Spolsky 的这 12 个问题，与其说是对代码质量的测试，不如说是一面镜子，映照出一家公司是否真正尊重开发者的时间和心智。二十多年过去了，这些问题依然是衡量一个工程团队成熟度的“试金石”。 Do you use source control? (你用源码控制吗？) Can you make a build in one step? (你能一步构建吗？) Do you make daily builds? [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/the-software-essays-that-shaped-me-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me">本文永久链接</a> &#8211; https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me</p>
<p>大家好，我是Tony Bai。</p>
<p>二十年前，一位年轻的程序员在还未踏入职场时，便开始沉浸于软件开发的博客文章与深刻思考之中。二十年后，他已成为一名资深工程师，回首望去，成千上万的文字中，只有寥寥数篇真正沉淀下来，如基石般塑造了他的思维方式和职业生涯。</p>
<p>这份由 Michael Lynch 精心筛选出的“<a href="https://refactoringenglish.com/blog/software-essays-that-shaped-me/">思想塑造清单</a>”，本身就是一次对软件工程领域永恒智慧的巡礼。清单中的每一篇文章，都如同一个思想的火种，点燃了关于工程文化、代码哲学、乃至技术选型的深刻辩论。</p>
<p>今天，也让我们重新打开这些经典，逐一剖析其中的智慧，看看它们在瞬息万变的当下，能为我们——尤其是追求简约与高效的 Go 开发者——带来怎样历久弥新的启示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-micro-column-2025-pr.png" alt="" /></p>
<h2>1. Joel 测试：衡量开发者幸福感的 12 条黄金标准</h2>
<p><em>(“The Joel Test: 12 Steps to Better Code” by Joel Spolsky, 2000)</em></p>
<p>Joel Spolsky 的这 12 个问题，与其说是对代码质量的测试，不如说是一面镜子，映照出一家公司<strong>是否真正尊重开发者的时间和心智</strong>。二十多年过去了，这些问题依然是衡量一个工程团队成熟度的“试金石”。</p>
<ol>
<li><strong>Do you use source control? (你用源码控制吗？)</strong></li>
<li><strong>Can you make a build in one step? (你能一步构建吗？)</strong></li>
<li><strong>Do you make daily builds? (你每天都构建吗？)</strong></li>
<li><strong>Do you have a bug database? (你有 Bug 数据库吗？)</strong></li>
<li><strong>Do you fix bugs before writing new code? (你先修 Bug 再写新代码吗？)</strong></li>
<li><strong>Do you have an up-to-date schedule? (你有最新的排期吗？)</strong></li>
<li><strong>Do you have a spec? (你有需求规格说明吗？)</strong></li>
<li><strong>Do programmers have quiet working conditions? (程序员有安静的工作环境吗？)</strong></li>
<li><strong>Do you use the best tools money can buy? (你用钱能买到的最好工具吗？)</strong></li>
<li><strong>Do you have testers? (你有测试人员吗？)</strong></li>
<li><strong>Do new candidates write code during their interview? (新候选人在面试时会写代码吗？)</strong></li>
<li><strong>Do you do hallway usability testing? (你做“走廊可用性测试”吗？)</strong></li>
</ol>
<p>虽然“每日构建”在今天已被“持续集成”(CI) 所取代，“Bug 数据库”也演变成了 Jira 或 Linear，但其精神内核——<strong>减少摩擦、自动化、系统化地管理混乱</strong>——从未过时。对于 Go 开发者而言，go build 的一步构建、go test 的内置测试、以及强大的静态分析工具链，都是对“Joel 测试”精神的现代回应。当你评估一个团队或项目时，不妨在心中过一遍这 12 个问题，它的得分，往往比任何花哨的技术栈更能说明问题。</p>
<h2>2. 解析，而非验证：用类型系统构建“安全默认”的代码</h2>
<p><em>(“Parse, don&#8217;t validate” by Alexis King, 2019)</em></p>
<p>这篇文章的核心论点，对于任何一个使用静态类型语言（如 Go）的开发者来说，都具有革命性的意义：<strong>“每当你验证一段数据时，你应该将它转换成一个新的类型。”</strong></p>
<p><strong>传统（脆弱的）做法：</strong></p>
<pre><code class="go">// 每次使用前，都得记得调用它
func validateUsername(username string) error { ... }
</code></pre>
<p>这种做法的问题在于，它将验证的责任推给了开发者。你必须在代码的每一个角落，都记得去调用 validateUsername，一旦遗漏，就可能导致安全漏洞或数据损坏。</p>
<p><strong>“解析，而非验证”的哲学：</strong></p>
<pre><code class="go">// 定义一个全新的、无法被随意创建的类型
type Username string

// 唯一的入口：一个“解析”函数，它在内部执行验证
func ParseUsername(raw string) (Username, error) {
    if err := validate(raw); err != nil {
        return "", err
    }
    return Username(raw), nil
}

// 后续的业务逻辑，只接受这个被“祝福”过的类型
func GreetUser(u Username) { ... }
</code></pre>
<p>这种模式利用<strong>类型系统</strong>，将安全检查从一种“需要开发者时刻牢记的纪律”，转变为一种<strong>“由编译器强制执行的保证”</strong>。一旦你有了一个 Username 类型的变量，你就拥有了一个<strong>不可辩驳的证明</strong>——它必然是合法的。这在 Go 中极易实现，通过创建新的具名类型，我们可以轻松地在代码中构建起一道道安全的“防火墙”，让非法状态<strong>根本没有机会存在</strong>。</p>
<h2>3. 无银弹：正视软件开发的“本质复杂性”</h2>
<p><em>(“No Silver Bullet” by Fred Brooks, 1986)</em></p>
<p>这篇来自《人月神话》作者的经典文章，将软件开发工作划分为两个核心部分：</p>
<ul>
<li><strong>本质复杂性 (Essential Complexity)</strong>：与问题领域本身固有的、不可简化的复杂逻辑作斗争。例如，设计一套复杂的保险计价公式。</li>
<li><strong>偶然复杂性 (Accidental Complexity)</strong>：与工具、环境和实现细节作斗争。例如，处理内存泄漏、等待编译、配置构建系统。</li>
</ul>
<p>Brooks 的核心论点是：过去几十年软件开发效率的巨大提升，主要来自于对“偶然复杂性”的削减。但无论工具如何发展，我们永远无法消除“本质复杂性”。因此，<strong>不存在任何能够带来数量级生产力提升的“银弹”</strong>。</p>
<p>这篇文章是对抗技术领域“炒作周期”的最佳解毒剂。无论是微服务、Serverless、还是当下的 AI，它们在很大程度上解决的都是“偶然复杂性”。Go 语言的诞生，其核心目标——极快的编译速度、简单的并发模型、自动的垃圾回收——本身就是对 C++ 等语言“偶然复杂性”的一次宣战。</p>
<p>Brooks 的理论让我们保持清醒：即使 AI 能为我们编写代码，但<strong>定义需求、设计系统、测试复杂交互</strong>这些“本质复杂性”的工作，依然是人类工程师不可替代的价值所在。</p>
<h2>4. 选择的代价：为用户做明智的决定</h2>
<p><em>(“Choices” by Joel Spolsky, 2000)</em></p>
<p>Joel Spolsky 敏锐地指出：“<strong>你每提供一个选项，就是在要求用户做一次决策。</strong>” 过多的选择，尤其是那些用户并不具备足够信息来做出的选择，会中断用户的心流，带来挫败感。</p>
<p>他以 Windows 98 中一个荒谬的帮助搜索设置为例，痛斥了将底层技术决策（如“最小化数据库大小”或“最大化搜索能力”）推给普通用户的设计懒政。</p>
<p>这个原则不仅适用于 GUI，更适用于我们编写的任何 API 和命令行工具。当你的函数需要一大堆配置参数时，问问自己：</p>
<ul>
<li>这些选项真的都是必需的吗？</li>
<li>我是否可以根据大多数场景，提供一个明智的、开箱即用的默认行为？</li>
<li>对于必须暴露的选项，我能否通过 Go 的<strong>选项模式 (Options Pattern)</strong> 来组织它们，让简单的使用保持简单，让复杂的配置成为可能？</li>
</ul>
<p>一个优秀的 API 设计者，应该是一个“仁慈的独裁者”，敢于为用户承担决策的责任，只在真正必要时，才将选择的权力交还给他们。</p>
<h2>5. 兼容性是为用户，而非为程序</h2>
<p><em>(“Application compatibility layers are there for the customer, not for the program” by Raymond Chen, 2010)</em></p>
<p>Raymond Chen 用一个尖刻的比喻，讽刺了那些期望操作系统为他们的旧软件提供无限向后兼容性的开发者。然而，文章作者 Michael Lynch 反思后认为，这个比喻的背后，其实蕴含着一个更深刻的用户行为洞察：<strong>用户永远会选择阻力最小的路径</strong>。</p>
<p>如果你发现用户在以一种“错误”但“有效”的方式使用你的系统（比如，依赖一个 Bug 来实现某个功能），那么你的责任不是嘲笑他们，而是去理解他们为何这么做，并提供一条更简单、更正确的路径来引导他们。</p>
<p>这条规则对我们如今进行API设计也是大有借鉴意义的，这意味着我们需要时刻保持<strong>同理心</strong>。如果你发布了一个有 Bug 的 v1 版本，并且发现大量用户已经围绕这个 Bug 构建了他们的系统，那么在 v2 版本中，简单地“修复”这个 Bug 可能会导致大规模的破坏。</p>
<p>一个更负责任的做法可能是：</p>
<ol>
<li>在 v2 中提供一个新的、行为正确的 API。</li>
<li>保留 v1 的旧 API，但将其标记为<strong>废弃</strong>，并在文档中清晰地解释其错误行为和迁移路径。</li>
<li>（在 Go 1.26+ 中）甚至可以利用 //go:fix 指令，为用户提供自动化的迁移工具。</li>
</ol>
<h2>6. 不要在测试中引入逻辑</h2>
<p><em>(“Don&#8217;t Put Logic in Tests” by Erik Kuefler, 2014)</em></p>
<p>我们通常被教导要在生产代码中遵循 DRY (Don&#8217;t Repeat Yourself) 原则。但 Erik Kuefler 指出，将这一原则盲目地应用到测试代码中，可能是一场灾难。</p>
<p><strong>糟糕的测试：</strong></p>
<pre><code class="go">// 为了“ DRY ”，我们拼接了 URL
assertEquals(baseUrl + "/u/0/photos", nav.getCurrentUrl());
</code></pre>
<p>这段代码隐藏了一个微小的 Bug（多了一个斜杠），因为它需要读者在脑中进行一次字符串拼接运算才能发现问题。</p>
<p><strong>优秀的测试：</strong></p>
<pre><code class="go">// 清晰、直白，一眼就能看出期望的结果
assertEquals("http://plus.google.com//u/0/photos", nav.getCurrentUrl());
</code></pre>
<p>虽然存在字符串冗余，但它的意图是<strong>一目了然</strong>的。</p>
<p>测试代码的首要目标是<strong>清晰性</strong>，而非优雅或无冗余。测试代码没有它自己的测试，验证其正确性的唯一方式就是<strong>人工审查</strong>。因此，一段好的测试，应该像一篇优秀的规格说明文档，让任何一个读者都能毫不费力地理解它在断言什么。在 Go 的<strong>表驱动测试 (Table-Driven Tests)</strong> 中，这一点体现得尤为重要：绝大多数情况下，输入和期望的输出应该被清晰地、并排地列出，而不是通过复杂的辅助函数动态生成。</p>
<h2>7. 一点原生 JavaScript 就能做很多事</h2>
<p><em>(“A little bit of plain Javascript can do a lot” by Julia Evans, 2020)</em></p>
<p>Julia Evans 曾分享了她从一个坚定的“前端框架拥护者”转变为“原生 JavaScript 爱好者”的心路历程。在饱受了 Angular, React, Vue 等框架带来的依赖问题和复杂性的折磨后，她决定尝试只用原生 JavaScript（现代的 ES2018 标准）来构建一个 Web 界面。</p>
<p>结果令她震惊：没有框架、没有构建步骤、没有 Node.js，她依然能完成 90% 的工作，而开发体验的“头痛程度”只有 5%。当出现运行时错误时，她看到的不再是经过压缩、转换的“天书”，而是她自己写的、清晰可辨的代码。</p>
<p>这篇文章是对现代软件开发中“框架至上”文化的一次有力反思。它提醒我们，在引入任何一个大型框架或库之前，都应该先问自己：<strong>我真的需要这个吗？标准库或语言本身的能力是否已经足够？</strong></p>
<p>对于 Go 开发者而言，这种思想更是与语言的哲学不谋而合。Go 拥有一个极其强大的标准库（特别是 net/http），在许多场景下，你完全不需要引入像 Gin 或 Echo 这样的 Web 框架，就能构建出高性能、可维护的 Web 服务。</p>
<p>Julia 的经历鼓励我们，要敢于挑战对框架的“路径依赖”，重新审视并信任我们手中工具（无论是 JavaScript 还是 Go 标准库）的内建能力。有时候，最简单的解决方案，恰恰就在我们眼前。</p>
<h2>8. 选择无聊的技术</h2>
<p><em>(“Choose Boring Technology” by Dan McKinley, 2015)</em></p>
<p>这篇经典文章的标题本身，就是其全部智慧的浓缩。Dan McKinley 警告我们，在启动一个新项目时，要警惕那些闪亮、前沿、充满炒作的新技术的诱惑。</p>
<ul>
<li><strong>新技术</strong>：有未知的 Bug 和弱点，当你遇到问题时，社区可能还没有解决方案，你将孤立无援。</li>
<li><strong>“无聊”的技术</strong>（如 Postgres, Java, Go）：虽然有其自身的问题，但经过数十年（或多年）的实战检验，它们几乎所有可能遇到的问题，都有成熟的、有据可查的解决方案。</li>
</ul>
<p>McKinley 提出了一个有趣的模型：每个公司都有三枚<strong>“创新代币” (innovation tokens)</strong>。如果你想在一个项目中使用一项未经充分验证的新技术，你就必须花掉一枚代币。请明智地使用它们。</p>
<p>Go 语言本身，在许多方面，已经成为了“无聊技术”的典范。它稳定、向后兼容、拥有强大的标准库和成熟的生态。当我们进行技术选型时，应该问自己：我们当前的核心问题，真的需要一个全新的、我们团队不熟悉的“闪亮新事物”来解决吗？还是说，用我们已经精通的“无聊”工具，就足以应对挑战？选择“无聊”，往往是通往项目成功最可靠的路径。</p>
<h2>9. 我把自己锁在了数字生活之外</h2>
<p><em>(“I&#8217;ve locked myself out of my digital life” by Terence Eden, 2022)</em></p>
<p>这篇文章以一个引人入胜的思想实验开场：如果一道闪电击中了你的房子，摧毁了你所有的电子设备，你将如何恢复你的数字生活？</p>
<p>作者 Terence Eden 意识到，尽管他有密码管理器、硬件密钥和多重备份，但所有这些安全措施的“入口”，都依赖于他手边的某个设备。如果所有设备同时被毁，他将无法访问密码管理器，也无法使用硬件密钥，从而陷入一个无法恢复的死循环。</p>
<p>这个故事迫使读者思考一个被我们常常忽略的问题：<strong>我们的灾难恢复计划，是否本身就依赖于那些可能会在灾难中一同消失的东西？</strong></p>
<p>这篇文章的教训，超越了个人数字安全，直指系统设计的核心——<strong>韧性 (Resilience)</strong> 和 <strong>避免单点故障</strong>。</p>
<p>当我们设计一个分布式系统时，我们是否考虑过最坏的情况？</p>
<ul>
<li>我们的备份恢复流程，是否依赖于某个中心化的、可能会一同宕机的认证服务？</li>
<li>我们的配置中心如果不可用，应用是否能以一种“降级”但仍可用的模式启动？</li>
<li>在多云或混合云部署中，我们的跨区域故障转移方案，是否隐藏了对某个单一 DNS 提供商或证书颁发机构的隐式依赖？</li>
</ul>
<p>Terence 的故事提醒我们，真正的系统韧性，不仅仅是拥有备份和冗余，更是要<strong>反复审视和测试我们的恢复路径</strong>，确保在极端情况下，我们不会发现自己“被锁在门外”。</p>
<h2>10. Bonus：Brad Fitzpatrick 论输入验证的“咆哮”</h2>
<p><em>(Brad Fitzpatrick on parsing user input, 2009)</em></p>
<p>最后，是一段来自 Go 社区大神、Memcached 和 LiveJournal 的创造者 Brad Fitzpatrick 的“咆哮”，这段话源于一本访谈录《Coders at Work》。当被问及软件工程的伦理时，他将矛头直指糟糕的输入验证：</p>
<blockquote>
<p>“我希望每个人在他们的信用卡表单上都能保持一致，让我TMD能输入空格或连字符。计算机很擅长移除那些狗屎。别告诉我该如何格式化我的数字。”</p>
</blockquote>
<p>这段充满激情的“粗口”，完美地概括了一个核心的用户体验原则：<strong>宽进严出 (Be liberal in what you accept, be conservative in what you produce)</strong>。</p>
<p>作为 API 或 UI 的设计者，我们的责任是<strong>尽可能地减轻用户的负担</strong>。计算机是用来处理繁琐、重复性工作的。如果用户输入了一个带空格的电话号码，或者一个全角的逗号，我们的程序应该默默地、智能地将其清理和格式化，而不是粗暴地拒绝并抛出一个错误。</p>
<p>Fitzpatrick 的“咆哮”时刻提醒着我们：每一次当你设计一个输入字段时，都要站在用户的角度思考，并记住那句话——<strong>“计算机很擅长移除那些狗屎。”</strong></p>
<h2>小结：构建衡量“好”与“坏”的永恒坐标系</h2>
<p>从 Joel Spolsky 对工程文化的拷问，到 Fred Brooks 对复杂性的深刻剖析；从 Alexis King 对类型安全的精妙论证，到 Dan McKinley 对技术选型的务实忠告…… 当我们跟随 Michael Lynch 的脚步，完成这次跨越四十年的思想巡礼后，我们收获的远不止是一份“书单”。</p>
<p>技术浪潮来了又去，今天我们手中的工具，明天可能就会过时。但这些围绕着“人”的根本原则——<strong>清晰性、简单性、健壮性、同理心、风险意识</strong>——却是永恒的。它们是区分一名普通的“代码实现者”与一位真正的“软件工程师”的分水岭。</p>
<p>这份清单，最终为我们构建的，是一个内心深处的、用以衡量“好”与“坏”的永恒坐标系。在未来的职业生涯中，无论面对何种炫目的新技术或棘手的工程问题，这个坐标系都将指引我们，做出更明智、更持久、也更具价值的决策。</p>
<p>资料链接：https://refactoringenglish.com/blog/software-essays-that-shaped-me</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p><strong>想系统学习Go，构建扎实的知识体系？</strong></p>
<p>我的新书《<a href="https://book.douban.com/subject/37499496/">Go语言第一课</a>》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-4.png" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>千呼万唤始出来？Go 1.25解决Git仓库子目录作为模块根路径难题</title>
		<link>https://tonybai.com/2025/06/07/allow-serving-module-under-subdir/</link>
		<comments>https://tonybai.com/2025/06/07/allow-serving-module-under-subdir/#comments</comments>
		<pubDate>Sat, 07 Jun 2025 00:36:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[bazel]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[critique]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[gitflow]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[gitlab]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-import]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.work]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[govulncheck]]></category>
		<category><![CDATA[internal]]></category>
		<category><![CDATA[main]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[meta]]></category>
		<category><![CDATA[module-root]]></category>
		<category><![CDATA[monorepo]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PR]]></category>
		<category><![CDATA[PullRequest]]></category>
		<category><![CDATA[replace]]></category>
		<category><![CDATA[require]]></category>
		<category><![CDATA[tag]]></category>
		<category><![CDATA[trunk]]></category>
		<category><![CDATA[vanity-module-path]]></category>
		<category><![CDATA[代码审查]]></category>
		<category><![CDATA[单一仓库]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[流水线]]></category>
		<category><![CDATA[白盒交付]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4793</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/06/07/allow-serving-module-under-subdir 大家好，我是Tony Bai。 对于许多 Go 项目维护者而言，如何优雅地组织一个包含多种语言或多个独立 Go 模块的 Git 仓库一直是个不大不小的难题。将 Go 模块置于仓库根目录虽然直接，但有时会导致根目录文件列表臃肿，影响项目整体的清爽度。而将 Go 模块移至子目录，则面临着导入路径、版本标签以及 Go 工具链支持等一系列挑战。近日，一个旨在解决这一痛点的提案 (Issue #34055) 在历经数年讨论后，终于被 Go 团队正式接受，并将在 Go 1.25 版本中落地。这一变化预示着 Go 模块的管理将迎来更高的灵活性。 在这篇文章中，我就来介绍一下这个Go模块管理的变化，各位读者也可以评估一下该功能是否会给你带来更多的便利。 痛点：子目录模块的困境 提案发起者 @nhooyr 在其 websocket 项目 (nhooyr.io/websocket) 中遇到了典型的问题：当 Go 模块文件直接放在 Git 仓库根目录时，根目录显得非常杂乱。他尝试将 Go 模块移至子目录（例如 ./mod），希望 nhooyr.io/websocket 这个导入路径能直接指向该子目录，而不是变成 nhooyr.io/websocket/mod 这样“丑陋”的路径。 现有的 go-import meta 标签虽然允许自定义导入路径到 VCS 仓库的映射，但在处理子目录模块时存在局限： 直接指定仓库： [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/allow-serving-module-under-subdir-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/06/07/allow-serving-module-under-subdir">本文永久链接</a> &#8211; https://tonybai.com/2025/06/07/allow-serving-module-under-subdir</p>
<p>大家好，我是Tony Bai。</p>
<p>对于许多 Go 项目维护者而言，如何优雅地组织一个包含多种语言或多个独立 Go 模块的 Git 仓库一直是个不大不小的难题。将 Go 模块置于仓库根目录虽然直接，但有时会导致根目录文件列表臃肿，影响项目整体的清爽度。而将 Go 模块移至子目录，则面临着导入路径、版本标签以及 Go 工具链支持等一系列挑战。近日，一个旨在解决这一痛点的提案 (<a href="https://github.com/golang/go/issues/34055">Issue #34055</a>) 在历经数年讨论后，终于被 Go 团队正式接受，并将在 Go 1.25 版本中落地。这一变化预示着 Go 模块的管理将迎来更高的灵活性。</p>
<p>在这篇文章中，我就来介绍一下这个Go模块管理的变化，各位读者也可以评估一下该功能是否会给你带来更多的便利。</p>
<h2>痛点：子目录模块的困境</h2>
<p>提案发起者 @nhooyr 在其 websocket 项目 (nhooyr.io/websocket) 中遇到了典型的问题：当 Go 模块文件直接放在 Git 仓库根目录时，根目录显得非常杂乱。他尝试将 Go 模块移至子目录（例如 ./mod），希望 nhooyr.io/websocket 这个导入路径能直接指向该子目录，而不是变成 nhooyr.io/websocket/mod 这样“丑陋”的路径。</p>
<p>现有的 go-import meta 标签虽然允许自定义导入路径到 VCS 仓库的映射，但在处理子目录模块时存在局限：</p>
<ul>
<li><strong>直接指定仓库：</strong> 会导致导入路径需要包含子目录名，这与期望的简洁导入路径相悖。</li>
<li><strong>运行自定义模块服务器：</strong> 虽然可以实现精确映射，但这增加了维护成本，并非所有开发者都愿意承担。</li>
<li><strong>版本标签问题：</strong> 当模块位于子目录时，如何正确识别和使用 Git 标签（如 v1.0.0）成为一个棘手的问题。开发者期望的是使用仓库级别的全局标签，而不是为子目录模块创建特殊前缀的标签（如 mod/v1.0.0）。</li>
<li><strong>godoc.org 等工具的兼容性：</strong> 早期 godoc.org 对子目录模块的支持也不完善(注：该提案提出于2019年，那时godoc.org尚未关闭)。</li>
</ul>
<p>Apache Thrift 项目也遇到了类似问题，其 Go 库位于 github.com/apache/thrift/lib/go/thrift。如果 go.mod 放在子目录下，导入路径会变长，且无法直接使用项目级别的 Git 标签；如果 go.mod 放在顶层，则会受到仓库中其他语言测试代码的影响，使得 go mod tidy 等操作变得复杂(注：<a href="https://tonybai.com/2025/05/22/go-mod-ignore-directive">Go 1.25的go.mod增加ignore指令</a>，一定称度上可以缓解该影响)。</p>
<h2>提案核心：go-import 的扩展与版本标签约定</h2>
<p>经过社区的广泛讨论和 Go 团队的审慎考虑，最终被接受的方案聚焦于扩展 go-import meta 标签，并明确了版本标签的约定：</p>
<h3><strong>扩展 go-import Meta 标签</strong></h3>
<p>在现有的 go-import meta 标签的三个字段（import-prefix vcs vcs-url）基础上，增加第四个可选字段，用于指定模块在仓库中的实际子目录。</p>
<p>例如，对于 nhooyr.io/websocket 这个导入路径，如果其模块代码位于 github.com/nhooyr/websocket 仓库的 mod 子目录下，其 go-import meta 标签可以这样设置：</p>
<pre><code class="html">&lt;meta name="go-import" content="nhooyr.io/websocket git https://github.com/nhooyr/websocket mod"&gt;
</code></pre>
<p>当 Go 工具（如 go get）解析这个自定义导入路径时，它会识别到第四个字段 mod，并知道真正的模块代码位于该 Git 仓库的 mod 子目录中。旧版本的 Go 工具会因为字段数量不匹配而忽略此标签，这保证了向后兼容性（旧版本 Go 无法处理子目录，忽略标签是合理的行为）。</p>
<h3><strong>版本标签约定</strong></h3>
<p>对于位于子目录中的模块，其版本标签<strong>必须</strong>包含该子目录作为前缀。</p>
<p>继续上面的例子，如果 nhooyr.io/websocket 发布 v1.0.0 版本，其在 github.com/nhooyr/websocket 仓库中对应的 Git 标签应该是 mod/v1.0.0。</p>
<p>Go 工具在解析 nhooyr.io/websocket@v1.0.0 时，会结合 go-import 标签中的子目录信息，去查找 mod/v1.0.0 这个 Git 标签。</p>
<p>对于嵌套更深的子目录模块，例如 nhooyr.io/websocket/example 位于仓库的 mod/example 子目录下，其 v1.0.0 版本的标签则应为 mod/example/v1.0.0。</p>
<p>我们这里用一张示意图来直观展示一下这个约定的工作原理：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/allow-serving-module-under-subdir-2.png" alt="" /></p>
<p>这一约定确保了版本标签的唯一性和明确性，避免了不同子目录模块可能存在的标签冲突，以及全局标签与特定子目录模块版本之间的模糊性。Go团队也强调了避免使用全局标签作为回退的重要性，因为这可能导致版本含义随时间变化而产生不一致和校验和错误。</p>
<h2>为何选择此方案？</h2>
<ul>
<li><strong>最小化改动与兼容性：</strong> 扩展 go-import 标签是对现有机制的平滑增强，对旧版本 Go 工具影响可控。</li>
<li><strong>明确性与一致性：</strong> 子目录前缀的版本标签确保了版本指向的唯一性，与 Go 模块系统中对子目录模块版本控制的既有逻辑保持一致。</li>
<li><strong>解决了核心痛点：</strong> 允许开发者使用简洁的自定义导入路径，同时将 Go 模块代码组织在 Git 仓库的子目录中，保持了仓库根目录的整洁。</li>
<li><strong>避免复杂性：</strong> 相较于引入新的 go.mod 指令（如有开发者曾建议的别名机制）或其他更复杂的仓库结构约定，此方案更为直接和易于理解。</li>
</ul>
<p>值得注意的是，此提案主要针对使用<strong>自定义导入路径</strong>（通过 go-import meta 标签声明）的场景。对于直接使用如 github.com/user/repo/subdir 这样的导入路径，当前Go 工具链已经能够处理，但版本标签也需要遵循子目录前缀的规则。此提案并不能改变像 github.com 这类不依赖 go-import 元数据的托管平台的行为。</p>
<h2>对 Go Monorepo 实践的深远影响</h2>
<p>该提案的接受，不仅仅是对自定义导入路径和子目录模块管理的技术细节改进，更深层次上，它将对 <a href="https://tonybai.com/2025/06/06/go-monorepo">Go 社区中 Monorepo（单一代码仓库）策略的采纳和实践</a>产生积极且重要的推动作用。</p>
<h3>Monorepo 的吸引力与 Go 的挑战</h3>
<p>Monorepo 模式因其在促进代码共享、实现<strong>原子化变更</strong>、简化跨组件重构以及统一构建和测试流程等方面的优势，在大型项目和追求高效协作的团队中越来越受欢迎。Google 的大规模 Monorepo 实践以及 etcd 等开源项目所采用的“单一仓库，多 Go 模块”模式，都展示了其价值。</p>
<p>然而，在 Go 语言生态中，原生工具链对 Monorepo 内子目录模块缺乏优雅的支持，一直是制约其广泛应用的一个因素。开发者常常需要在“整洁的仓库结构”与“简洁的模块导入路径及清晰的版本管理”之间做出权衡。</p>
<h3>该提案如何赋能 Go Monorepo？</h3>
<p>Go 1.25 引入的对 go-import 子目录的直接支持，恰好解决了这一核心痛点：</p>
<ul>
<li><strong>降低多模块 Monorepo 的实现门槛</strong></li>
</ul>
<p>通过扩展 go-import meta 标签，开发者可以轻松地将位于 Git 仓库任意子目录下的 Go 模块映射到期望的、简洁的自定义导入路径。这意味着，一个 Monorepo 可以更自然地容纳多个逻辑上独立但可能共享代码的 Go 服务或库，而无需担心导入路径变得冗长或依赖复杂的代理服务器。</p>
<ul>
<li><strong>标准化子目录模块的版本控制</strong></li>
</ul>
<p>结合提案中明确的“版本标签需包含子目录前缀”（如 sub_module/v1.0.0）的约定，使得在 Monorepo 中对不同模块进行独立的版本发布和精确的依赖管理成为可能。这与 etcd 项目展示的模式高度一致，为其他希望效仿的项目提供了清晰的指导。</p>
<ul>
<li><strong>提升代码组织灵活性与可维护性</strong></li>
</ul>
<p>大型项目或包含多种技术栈的仓库，可以将 Go 代码更合理地组织在符合项目整体架构的子目录中，例如 components/auth_service/go/ 或 libs/go/common_utils/，而这些子目录下的模块依然可以拥有如 my-org.com/auth 或 my-org.com/utils 这样干净的导入路径。</p>
<ul>
<li><strong>促进更广泛的 Monorepo 采纳</strong></li>
</ul>
<p>随着这一关键技术障碍的扫除，那些因统一工程标准、简化依赖管理（尤其是内部依赖）、提升CI/CD效率或满足特定交付需求（如白盒交付）而考虑 Monorepo 的团队，将更有信心和理由在 Go 项目中实践这一策略。Go 语言正变得越来越适合构建和管理大规模、多组件的复杂系统。</p>
<p>可以预见，Go 1.25 的这一特性将成为 Go 开发者工具箱中的一个重要补充，它不仅解决了单个模块的组织问题，更为 Go 生态系统拥抱和发展 Monorepo 实践提供了坚实的基础。</p>
<h2>进展与展望</h2>
<p>该提案已被 Go 团队接受，相关的实现工作也已完成。最初计划在 Go 1.24 发布，后因时间原因推迟至 <strong>Go 1.25</strong>。</p>
<p>一旦此特性随着Go 1.25发布，Go 开发者在组织单仓库多模块（monorepo）或包含非 Go 代码的大型项目时，将拥有更大的灵活性：</p>
<ul>
<li>可以更清晰地分离不同语言或项目的代码，同时为 Go 模块提供简洁、稳定的自定义导入路径。</li>
<li>例如，一个项目可以有 docs/、python_scripts/、go_module/ 等子目录，而 mycompany.com/myproject 可以直接指向 go_module/。</li>
</ul>
<p>当然，这也要求模块维护者在发布版本时，正确地创建带有子目录前缀的 Git 标签。</p>
<h2>小节</h2>
<p>34055 提案的接受和即将落地，是 Go 模块系统在灵活性和易用性上的又一次重要进步。它回应了社区长期以来关于改善子目录模块管理体验的呼声，提供了一个相对简单且兼容性良好的解决方案。虽然它不能解决所有场景下的问题（尤其是对于 github.com 等直接路径），但对于使用自定义导入路径(vanity import path)的开发者来说，无疑是一个值得期待的积极变化。我们期待在 Go 1.25 中看到这一特性的正式落地，并观察它将如何被社区广泛应用。</p>
<hr />
<p><strong>您是否也曾为 Git 仓库子目录中的 Go 模块管理而烦恼？您认为 #34055 提案的解决方案是否满足您的需求？欢迎在评论区分享您的项目组织经验和对这一新特性的看法！</strong></p>
<p><strong>想深入理解 Go 模块的工作原理、版本管理、依赖解析以及更多企业级 Go 项目架构实践吗？不要错过我们的《Go语言进阶课》专栏，系统提升您的 Go 工程能力！</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>各位读者，我计划在我的微信公众号上，陆续推出一些付费的“微专栏”系列。  这些微专栏通常会围绕一个特定的、值得深入探讨的技术点或主题（无论是 Go 语言的进阶技巧、AI 开发的某个具体环节，还是某个工具的深度剖析等），以 3 篇左右的篇幅进行集中解析和分享。为什么尝试“微专栏”？主要是希望能针对一些值得深挖、但又不足以支撑一个完整大课程的“小而美”的主题，进行更系统、更透彻的分享。</p>
<p>《征服Go并发测试》微专栏就是我的首次尝试！欢迎大家订阅学习。</p>
<p>** 并发测试不再“玄学”！与 Go 1.25 testing/synctest 共舞 **</p>
<p>你是否也曾被 Go 并发测试中的不确定性、缓慢执行和难以调试所困扰？time.Sleep 带来的 flaky tests 是否让你在 CI 上提心吊胆？现在，Go 1.25 带来的官方并发测试利器——testing/synctest 包，将彻底改变这一切！</p>
<p>本系列文章（共三篇）带你从并发测试的痛点出发，深入剖析 testing/synctest 的设计理念、核心 API 与实现原理，并通过丰富的实战案例，手把手教你如何运用它构建可靠、高效的并发测试。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-concurrent-test-qr.png" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/06/07/allow-serving-module-under-subdir/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go项目该拥抱Monorepo吗？Google经验、etcd模式及白盒交付场景下的深度剖析</title>
		<link>https://tonybai.com/2025/06/06/go-monorepo/</link>
		<comments>https://tonybai.com/2025/06/06/go-monorepo/#comments</comments>
		<pubDate>Fri, 06 Jun 2025 00:12:47 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[bazel]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[critique]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[gitflow]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[gitlab]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.work]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[govulncheck]]></category>
		<category><![CDATA[internal]]></category>
		<category><![CDATA[main]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[monorepo]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PR]]></category>
		<category><![CDATA[PullRequest]]></category>
		<category><![CDATA[replace]]></category>
		<category><![CDATA[require]]></category>
		<category><![CDATA[tag]]></category>
		<category><![CDATA[trunk]]></category>
		<category><![CDATA[代码审查]]></category>
		<category><![CDATA[单一仓库]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[流水线]]></category>
		<category><![CDATA[白盒交付]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4789</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/06/06/go-monorepo 大家好，我是Tony Bai。 在Go语言的生态系统中，我们绝大多数时候接触到的项目都是遵循“一个代码仓库（Repo），一个Go模块（Module）”的模式。这种清晰、独立的组织方式，在很多场景下都运作良好。然而，当我们放眼业界，特别是观察像Google这样的技术巨头，或者深入研究etcd这类成功的开源项目时，会发现另一种代码组织策略——Monorepo（单一代码仓库）——也在扮演着越来越重要的角色。 与此同时，Go语言的依赖管理从早期的GOPATH模式（其设计深受Google内部Monorepo实践的影响）演进到如今的Go Modules，我们不禁要问：在现代Go工程实践中，尤其是面对日益复杂的项目协作和特殊的交付需求（如国内甲方普遍要求的“白盒交付”），传统的Single Repo模式是否依然是唯一的最佳选择？Go项目是否也应该，或者在何种情况下，考虑拥抱Monorepo？ 这篇文章，就让我们一起深入探讨Go与Monorepo的“前世今生”，解读不同形态的Go Monorepo实践（包括etcd模式），借鉴Google的经验，剖析其在现代软件工程，特别是白盒交付场景下的价值，并探讨相关的最佳实践与挑战。 Go Monorepo的形态解读：不仅仅是“大仓库” 首先，我们需要明确什么是Monorepo。它并不仅仅是简单地把所有代码都堆放在一个巨大的Git仓库里。一个真正意义上的Monorepo，通常还伴随着统一的构建系统、版本控制策略、代码共享机制以及与之配套的工具链支持，旨在促进大规模代码库的协同开发和管理。 在Go的世界里，Monorepo可以呈现出几种不同的形态： 形态1：单一仓库，单一主模块 这是我们最熟悉的一种“大型Go项目”组织方式。整个代码仓库的根目录下有一个go.mod文件，定义了一个主模块。项目内部通过Go的包（package）机制来组织不同的功能或子系统。 优点： 依赖管理相对简单直接，所有代码共享同一套依赖版本。 缺点： 对于逻辑上可以独立部署或版本化的多个应用/服务，这种方式可能会导致不必要的耦合。一个服务的变更可能需要整个大模块重新构建和测试，灵活性稍差。 形态2：单一仓库，多Go模块 —— 以etcd为例 这种形态更接近我们通常理解的“Go Monorepo”。etcd-io/etcd项目就是一个很好的例子。它的代码仓库顶层有一个go.mod文件，定义了etcd项目的主模块。但更值得关注的是，在其众多的子目录中（例如 client/v3, server/etcdserver/api, raft/raftpb 等），也包含了各自独立的go.mod文件，这些子目录本身也构成了独立的Go模块。 etcd为何采用这种模式？ 独立的版本演进与发布： 像client/v3这样的客户端库，其API稳定性和版本发布节奏可能与etcd服务器本身不同。将其作为独立模块，可以独立打版本标签（如client/v3.5.0），方便外部项目精确依赖特定版本的客户端。 清晰的API边界与可引用性： 子模块化使得每个组件的公共API更加明确。外部项目可以直接go get etcd仓库中的某个子模块，而无需引入整个庞大的etcd主项目。 更细粒度的依赖管理： 每个子模块只声明自己真正需要的依赖，避免了将所有依赖都集中在顶层go.mod中。 那么，一个Repo下有多个Go Module是Monorepo的一种形式吗？ 答案是肯定的。这是一种更结构化、更显式地声明了内部模块边界和依赖关系的Monorepo形式(即便规模较小，内部的模块不多)。它们之间通常通过go.mod中的replace指令（尤其是在本地开发或特定构建场景）或Go 1.18引入的go.work工作区模式来协同工作。比如下面etcd/etcdutl这个子目录下的go.mod就是一个典型的使用replace指令的例子： module go.etcd.io/etcd/etcdutl/v3 go 1.24 toolchain go1.24.3 replace ( go.etcd.io/etcd/api/v3 =&#62; ../api go.etcd.io/etcd/client/pkg/v3 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-monorepo-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/06/06/go-monorepo">本文永久链接</a> &#8211; https://tonybai.com/2025/06/06/go-monorepo</p>
<p>大家好，我是Tony Bai。</p>
<p>在Go语言的生态系统中，我们绝大多数时候接触到的项目都是遵循“一个代码仓库（Repo），一个Go模块（Module）”的模式。这种清晰、独立的组织方式，在很多场景下都运作良好。然而，当我们放眼业界，特别是观察像Google这样的技术巨头，或者深入研究etcd这类成功的开源项目时，会发现另一种代码组织策略——Monorepo（单一代码仓库）——也在扮演着越来越重要的角色。</p>
<p>与此同时，Go语言的依赖管理从早期的GOPATH模式（其设计深受Google内部Monorepo实践的影响）演进到如今的Go Modules，我们不禁要问：在现代Go工程实践中，尤其是面对日益复杂的项目协作和特殊的交付需求（如国内甲方普遍要求的“白盒交付”），传统的Single Repo模式是否依然是唯一的最佳选择？Go项目是否也应该，或者在何种情况下，考虑拥抱Monorepo？</p>
<p>这篇文章，就让我们一起深入探讨Go与Monorepo的“前世今生”，解读不同形态的Go Monorepo实践（包括etcd模式），借鉴Google的经验，剖析其在现代软件工程，特别是白盒交付场景下的价值，并探讨相关的最佳实践与挑战。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<h2>Go Monorepo的形态解读：不仅仅是“大仓库”</h2>
<p>首先，我们需要明确<strong>什么是Monorepo</strong>。它并不仅仅是简单地把所有代码都堆放在一个巨大的Git仓库里。一个真正意义上的Monorepo，通常还伴随着统一的构建系统、版本控制策略、代码共享机制以及与之配套的工具链支持，旨在促进大规模代码库的协同开发和管理。</p>
<p>在Go的世界里，Monorepo可以呈现出几种不同的形态：</p>
<h3>形态1：单一仓库，单一主模块</h3>
<p>这是我们最熟悉的一种“大型Go项目”组织方式。整个代码仓库的根目录下有一个go.mod文件，定义了一个主模块。项目内部通过Go的包（package）机制来组织不同的功能或子系统。</p>
<ul>
<li><strong>优点：</strong> 依赖管理相对简单直接，所有代码共享同一套依赖版本。</li>
<li><strong>缺点：</strong> 对于逻辑上可以独立部署或版本化的多个应用/服务，这种方式可能会导致不必要的耦合。一个服务的变更可能需要整个大模块重新构建和测试，灵活性稍差。</li>
</ul>
<h3>形态2：单一仓库，多Go模块 —— 以etcd为例</h3>
<p>这种形态更接近我们通常理解的“Go Monorepo”。etcd-io/etcd项目就是一个很好的例子。它的代码仓库顶层有一个go.mod文件，定义了etcd项目的主模块。但更值得关注的是，在其众多的子目录中（例如 client/v3, server/etcdserver/api, raft/raftpb 等），也包含了各自独立的go.mod文件，这些子目录本身也构成了独立的Go模块。</p>
<p><strong>etcd为何采用这种模式？</strong></p>
<ul>
<li><strong>独立的版本演进与发布：</strong> 像client/v3这样的客户端库，其API稳定性和版本发布节奏可能与etcd服务器本身不同。将其作为独立模块，可以独立打版本标签（如client/v3.5.0），方便外部项目精确依赖特定版本的客户端。</li>
<li><strong>清晰的API边界与可引用性：</strong> 子模块化使得每个组件的公共API更加明确。外部项目可以直接go get etcd仓库中的某个子模块，而无需引入整个庞大的etcd主项目。</li>
<li><strong>更细粒度的依赖管理：</strong> 每个子模块只声明自己真正需要的依赖，避免了将所有依赖都集中在顶层go.mod中。</li>
</ul>
<p><strong>那么，一个Repo下有多个Go Module是Monorepo的一种形式吗？</strong> 答案是肯定的。这是一种更结构化、更显式地声明了内部模块边界和依赖关系的Monorepo形式(即便规模较小，内部的模块不多)。它们之间通常通过go.mod中的replace指令（尤其是在本地开发或特定构建场景）或Go 1.18引入的go.work工作区模式来协同工作。比如下面etcd/etcdutl这个子目录下的go.mod就是一个典型的使用replace指令的例子：</p>
<pre><code>module go.etcd.io/etcd/etcdutl/v3

go 1.24

toolchain go1.24.3

replace (
    go.etcd.io/etcd/api/v3 =&gt; ../api
    go.etcd.io/etcd/client/pkg/v3 =&gt; ../client/pkg
    go.etcd.io/etcd/client/v3 =&gt; ../client/v3
    go.etcd.io/etcd/pkg/v3 =&gt; ../pkg
    go.etcd.io/etcd/server/v3 =&gt; ../server
)

// Bad imports are sometimes causing attempts to pull that code.
// This makes the error more explicit.
replace (
    go.etcd.io/etcd =&gt; ./FORBIDDEN_DEPENDENCY
    go.etcd.io/etcd/v3 =&gt; ./FORBIDDEN_DEPENDENCY
    go.etcd.io/tests/v3 =&gt; ./FORBIDDEN_DEPENDENCY
)

require (
    github.com/coreos/go-semver v0.3.1
    github.com/dustin/go-humanize v1.0.1
    github.com/olekukonko/tablewriter v1.0.7
    github.com/spf13/cobra v1.9.1
    github.com/stretchr/testify v1.10.0
    go.etcd.io/bbolt v1.4.0
    go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
    go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
    go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
    go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0
    go.etcd.io/etcd/server/v3 v3.6.0-alpha.0
    go.etcd.io/raft/v3 v3.6.0
    go.uber.org/zap v1.27.0
)
//... ...
</code></pre>
<h3>形态3：Google规模的Monorepo (The Google Way)</h3>
<p>Google内部的超大规模Monorepo是业界典范，正如Rachel Potvin和Josh Levenberg在其经典论文《<a href="https://research.google/pubs/why-google-stores-billions-of-lines-of-code-in-a-single-repository/">Why Google Stores Billions of Lines of Code in a Single Repository</a>》中所述，这个单一仓库承载了Google绝大多数的软件资产——截至2015年1月，已包含约10亿个文件，900万个源文件，20亿行代码，3500万次提交，总计86TB的数据，被全球95%的Google软件开发者使用。</p>
<p>其核心特点包括：</p>
<ul>
<li><strong>统一版本控制系统Piper：</strong> Google自研的Piper系统，专为支撑如此规模的代码库而设计，提供分布式存储和高效访问。</li>
<li><strong>强大的构建系统Blaze/Bazel：</strong> 能够高效地构建和测试这个庞大代码库中的任何目标，并精确管理依赖关系。</li>
<li><strong>单一事实来源 (Single Source of Truth)：</strong> 所有代码都在一个地方，所有开发者都工作在主干的最新版本（Trunk-Based Development），避免了多版本依赖的困扰（如“菱形依赖问题”）。</li>
<li><strong>原子化变更与大规模重构：</strong> 开发者可以进行跨越数千个文件甚至整个代码库的原子化修改和重构，构建系统确保所有受影响的依赖都能同步更新。</li>
<li><strong>广泛的代码共享与可见性：</strong> 促进了代码复用和跨团队协作，但也需要工具（如CodeSearch）和机制（如API可见性控制）来管理复杂性。</li>
</ul>
<p>Go语言的许多设计哲学，如包路径的全局唯一性、internal包的可见性控制、甚至早期的GOPATH模式（它强制所有Go代码在一个统一的src目录下，模拟了Monorepo的开发体验），都在不同程度上受到了Google内部这种开发环境的影响。</p>
<h2>Google Monorepo的智慧：版本、分支与依赖管理的启示</h2>
<p>虽然我们无法完全复制Google内部的庞大基础设施和自研工具链，但其在超大规模Monorepo管理上积累的经验，依然能为我们带来宝贵的启示：</p>
<ol>
<li><strong>Trunk-Based Development (主干开发)：</strong> Google绝大多数开发者工作在主干的最新版本。新功能通过条件标志（feature flags）控制，而非长时间存在的特性分支，这极大地避免了传统多分支开发模式下痛苦的合并过程。发布时，从主干切出发布分支，Bug修复在主干完成后，择优（cherry-pick）到发布分支。</li>
<li><strong>统一版本与依赖管理：</strong> Monorepo的核心优势在于“单一事实来源”。所有内部依赖都是源码级的，不存在不同项目依赖同一内部库不同版本的问题。对于第三方开源依赖，Google有专门的流程进行统一引入、审查和版本管理，确保整个代码库中只有一个版本存在。这从根本上解决了“菱形依赖”等版本冲突问题。</li>
<li><strong>强大的自动化工具链是基石：</strong>
<ul>
<li><strong>构建系统 (Bazel)：</strong> 能够进行精确的依赖分析、增量构建和并行测试，是Monorepo高效运作的核心。</li>
<li><strong>代码审查 (Critique)：</strong> Google文化高度重视代码审查，所有代码提交前都必须经过Review。</li>
<li><strong>静态分析与大规模重构工具 (Tricorder, Rosie)：</strong> 自动化工具用于代码质量检查、发现潜在问题，并支持跨整个代码库的大规模、安全的自动化重构。</li>
<li><strong>预提交检查与持续集成：</strong> 强大的自动化测试基础设施，在代码提交前运行所有受影响的测试，确保主干的健康。</li>
</ul>
</li>
</ol>
<p><strong>对我们的启示：</strong></p>
<ul>
<li><strong>“单一事实来源”的价值：</strong> 即使不采用Google规模的Monorepo，在团队或组织内部，尽可能统一核心共享库的版本，减少不必要的依赖分歧，是非常有益的。</li>
<li><strong>自动化的力量：</strong> 投入自动化测试、CI/CD、代码质量检查和依赖管理工具，是管理任何规模代码库（尤其是Monorepo）的必要投资。</li>
<li><strong>主干开发与特性标志：</strong> 对于需要快速迭代和持续集成的项目，主干开发结合特性标志，可能比复杂的多分支策略更敏捷。</li>
<li><strong>对依赖的审慎态度：</strong> Google对第三方依赖的严格管控值得借鉴。任何外部依赖的引入都应经过评估。</li>
</ul>
<h2>企业级Go Monorepo的最佳实践：从理念到落地</h2>
<p>当我们的组织或项目发展到一定阶段，特别是当多个Go服务/库之间存在紧密耦合、需要频繁协同变更，或者希望统一工程标准时，Monorepo可能成为一个有吸引力的选项。</p>
<p>以下是一些在企业环境中实施Go Monorepo的最佳实践：</p>
<ol>
<li>
<p><strong>明确采用Monorepo的驱动力与目标：</strong> 是为了代码共享？原子化重构？统一CI/CD？还是像我们接下来要讨论的“白盒交付”需求？清晰的目标有助于后续的设计决策。</p>
</li>
<li>
<p><strong>项目布局与模块划分的艺术：</strong></p>
<ul>
<li><strong>清晰的顶层目录结构：</strong> 例如，使用cmd/存放所有应用入口，pkg/存放可在Monorepo内部跨项目共享的库，services/或components/用于组织逻辑上独立的服务或组件（每个服务/组件可以是一个独立的Go模块），internal/用于存放整个仓库共享但不对外暴露的内部实现。</li>
<li><strong>推荐策略：为每个可独立部署的服务或可独立发布的库建立自己的go.mod文件。</strong> 这提供了明确的依赖边界和独立的版本控制能力。</li>
<li><strong>使用go.work提升本地开发体验：</strong> 在Monorepo根目录创建go.work文件，将所有相关的Go模块加入工作区，简化本地开发时的模块间引用和构建测试。</li>
</ul>
</li>
<li>
<p><strong>依赖管理的黄金法则：</strong></p>
<ul>
<li><strong>服务级go.mod中的replace指令：</strong> 对于Monorepo内部模块之间的依赖，务必在依赖方的go.mod中使用replace指令将其指向本地文件系统路径。这是确保模块在Monorepo内部能正确解析和构建的关键，尤其是在没有go.work的CI环境或交付给客户时。<br />
<code>// In my-org/monorepo/services/service-api/go.mod<br />
module my-org/monorepo/services/service-api<br />
go 1.xx<br />
require (<br />
    my-org/monorepo/pkg/common-utils v0.1.0 // 依赖内部共享库<br />
)<br />
replace my-org/monorepo/pkg/common-utils =&gt; ../../pkg/common-utils // 指向本地</code></li>
<li><strong>谨慎管理第三方依赖：</strong> 定期使用go list -m all、go mod graph分析依赖树，使用go mod tidy清理，关注go.sum的完整性。使用govulncheck进行漏洞扫描。</li>
</ul>
</li>
<li>
<p><strong>版本控制与发布的规范：</strong></p>
<ul>
<li><strong>为每个独立发布的服务/库打上带路径前缀的Git Tag：</strong> 例如，为services/appA模块的v1.2.3版本打上services/appA/v1.2.3的Tag。这样，外部可以通过go get my-org/monorepo/services/appA@services/appA/v1.2.3来精确获取。</li>
<li><strong>维护清晰的Changelog：</strong> 无论是整个Monorepo的（如果适用），还是每个独立发布单元的，都需要有详细的变更记录。</li>
</ul>
</li>
<li>
<p><strong>分支策略的适配：</strong></p>
<ul>
<li>可以考虑简化的Gitflow（主分支、开发分支、特性分支、发布分支、修复分支）或更轻量的GitHub Flow / GitLab Flow。关键是确保主分支（如main或master）始终保持可发布或接近可发布的状态。</li>
<li>特性开发在独立分支进行，通过Merge Request / Pull Request进行代码审查后合入主开发分支。</li>
</ul>
</li>
<li>
<p><strong>CI/CD的智能化与效率：</strong></p>
<ul>
<li><strong>按需构建与测试：</strong> CI/CD流水线应能识别出每次提交所影响的模块/服务，仅对受影响的部分进行构建和测试，避免不必要的全量操作。</li>
<li><strong>并行化：</strong> 利用Monorepo的结构，并行执行多个独立模块/服务的构建和测试任务。</li>
<li><strong>统一构建环境：</strong> 使用Docker等技术确保CI/CD环境与开发环境的一致性。</li>
</ul>
</li>
</ol>
<h2>Go Monorepo与白盒交付：相得益彰的“黄金搭档”</h2>
<p>现在，让我们回到一个非常具体的、尤其在国内甲方项目中常见的需求——<strong>白盒交付</strong>。白盒交付通常意味着乙方需要将项目的完整源码（包括所有依赖的内部库）、构建脚本、详细文档等一并提供给甲方，并确保甲方能在其环境中独立、可复现地构建出与乙方交付版本完全一致的二进制产物，同时甲方也可能需要在此基础上进行二次开发或长期维护。</p>
<p>在这种场景下，如果乙方的原始项目是分散在多个Repo中（特别是还依赖了乙方内部无法直接暴露给甲方的私有库），那么采用<strong>为客户定制一个整合的Monorepo进行交付</strong>的策略，往往能带来诸多益处：</p>
<ol>
<li>
<p><strong>解决内部私有库的访问与依赖问题：</strong><br />
我们可以将乙方原先的内部私有库代码，作为模块完整地复制到交付给客户的这个Monorepo的特定目录下（例如libs/或internal_libs/）。然后，在这个Monorepo内部，所有原先依赖这些私有库的服务模块，在其各自的go.mod文件中通过replace指令，将依赖路径指向Monorepo内部的本地副本。这样，客户在构建时就完全不需要访问乙方原始的、可能无法从客户环境访问的私有库地址了。</p>
</li>
<li>
<p><strong>提升可复现构建的成功率：</strong></p>
<ul>
<li><strong>集中的依赖管理：</strong> 所有交付代码及其内部依赖都在一个统一的Monorepo中，通过服务级的go.mod和replace指令明确了版本和本地路径，极大降低了因依赖版本不一致或依赖源不可达导致的构建失败。</li>
<li><strong>统一构建环境易于实现：</strong> 针对单一Monorepo提供标准化的构建脚本和Dockerfile（如果使用容器构建），比为多个分散Repo分别提供和维护要简单得多。</li>
<li>结合-trimpath、版本信息注入等技巧，更容易在客户环境中构建出与乙方环境内容一致的二进制文件。</li>
</ul>
</li>
<li>
<p><strong>简化后续的协同维护与Patch交付：</strong></p>
<ul>
<li><strong>集中的代码基：</strong> 即使后续乙方仅以Patch形式向甲方提供Bug修复或功能升级，这些Patch也是针对这个统一Monorepo的特定路径的变更。甲方应用Patch、进行代码审查和版本追溯都更为集中和方便。</li>
<li><strong>清晰的项目布局与版本管理：</strong> 在Monorepo内部，通过良好的目录组织和为每个独立服务打上带路径前缀的版本标签，使得甲乙双方对代码结构、版本演进和变更范围都有清晰的认知。</li>
</ul>
</li>
<li>
<p><strong>便于客户搭建统一的CI/CD与生成SBOM：</strong></p>
<ul>
<li>甲方可以在这个统一的Monorepo基础上，更容易地搭建自己的CI/CD流水线，并实现按需构建。</li>
<li>为Monorepo中的每个独立服务生成其专属的软件物料清单（SBOM）也更为规范和便捷。</li>
</ul>
</li>
</ol>
<p>可见，对于复杂的、涉及多服务和内部依赖的Go项目白盒交付场景，精心设计的客户侧Monorepo策略，可以显著提升交付的透明度、可控性、可维护性和客户满意度。**</p>
<h2>小结</h2>
<p>Monorepo并非没有代价。正如Google的论文中所指出的，它对工具链（特别是构建系统）、版本控制实践（如分支管理、Code Review）、以及团队的协作模式都提出了更高的要求。仓库体积的膨胀、潜在的构建时间增加（如果CI/CD优化不当）、以及更细致的权限管理需求，都是采用Monorepo时需要认真评估和应对的挑战。Google为其Monorepo投入了巨大的工程资源来构建和维护支撑系统，这对大多数组织来说是难以复制的。</p>
<p>然而，在特定场景下——例如拥有多个紧密关联的Go服务、希望促进代码共享与原子化重构、或者面临像白盒交付这样的特殊工程需求时——Monorepo展现出的优势，如“单一事实来源”、简化的依赖管理、原子化变更能力等，是难以替代的。</p>
<p>Go语言本身的设计，从早期的GOPATH到如今Go Modules对工作区（go.work）和子目录模块版本标签的支持，都在逐步提升其在Monorepo环境下的开发体验。虽然Go不像Bazel那样提供一个“大一统”的官方Monorepo构建解决方案，但其工具链的灵活性和社区的实践，已经为我们探索和实施Go Monorepo提供了坚实的基础。</p>
<p><strong>最终，Go项目是否应该拥抱Monorepo，并没有一刀切的答案。</strong> 它取决于项目的具体需求、团队的规模与成熟度、以及愿意为之投入的工程成本。但毫无疑问，理解Monorepo的理念、借鉴Google等先行者的经验（既要看到其优势，也要理解其巨大投入）、掌握etcd等项目的实践模式，并思考其在如白盒交付等现代工程场景下的应用价值，将极大地拓展我们作为Go开发者的视野，并为我们的技术选型和架构设计提供宝贵的参考。</p>
<p>Go的生态在持续进化，我们对更优代码组织和工程实践的探索也永无止境。</p>
<hr />
<p><strong>聊聊你的Monorepo实践与困惑</strong></p>
<p>Go语言项目，是坚守传统的“一Repo一Module”，还是拥抱Monorepo的集中管理？你在实践中是如何权衡的？特别是面对etcd这样的多模块仓库，或者类似Google的超大规模Monorepo理念，你有哪些自己的思考和经验？在白盒交付场景下，Monorepo又为你带来了哪些便利或新的挑战？</p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/06/06/go-monorepo/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>透视软件供应链安全：SBOM标准解读与Go项目生成指南</title>
		<link>https://tonybai.com/2025/05/22/go-sbom-practice/</link>
		<comments>https://tonybai.com/2025/05/22/go-sbom-practice/#comments</comments>
		<pubDate>Wed, 21 May 2025 22:20:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[cli]]></category>
		<category><![CDATA[CycloneDX]]></category>
		<category><![CDATA[DSDX]]></category>
		<category><![CDATA[excel]]></category>
		<category><![CDATA[Gin]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[GOBIN]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[gradle]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JS]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[LinuxFoundation]]></category>
		<category><![CDATA[Maven]]></category>
		<category><![CDATA[npm]]></category>
		<category><![CDATA[NTIA]]></category>
		<category><![CDATA[OWASP]]></category>
		<category><![CDATA[SBOM]]></category>
		<category><![CDATA[SPDX]]></category>
		<category><![CDATA[spdx2.3]]></category>
		<category><![CDATA[SWID]]></category>
		<category><![CDATA[syft]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[yaml]]></category>
		<category><![CDATA[yarn]]></category>
		<category><![CDATA[信通院]]></category>
		<category><![CDATA[持续交付]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[许可证]]></category>
		<category><![CDATA[软件供应链]]></category>
		<category><![CDATA[软件工程]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4741</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/22/go-sbom-practice 大家好，我是Tony Bai。 近年来，软件供应链安全事件频发，从 SolarWinds 到 Log4Shell，每一次都给业界敲响了警钟。在这样的背景下，软件物料清单 (SBOM, Software Bill of Materials) 的重要性日益凸显。无论是甲方爸爸的硬性要求（尤其是在2B软件交付和白盒交付场景），还是我们自身对软件透明度和安全性的追求，SBOM 都已成为现代软件开发不可或缺的一环。 那么，SBOM 究竟是什么？它为何如此重要？市面上有哪些主流的 SBOM 标准？我们又该如何为自己的 Go 项目（当然，也适用于 Java、JS 等其他语言项目）生成和使用 SBOM 呢？ 今天，我们就来一起深入探讨这些问题，为你揭开 SBOM 的神秘面纱。 SBOM：你的软件“配料表”，为何如此重要？ 想象一下，我们购买食品时会关注配料表，了解其成分、产地和营养信息。SBOM 之于软件，就如同食品的配料表。它是一份正式的、结构化的清单，详细列出了构成某个软件产品的所有组件及其依赖关系。 SBOM 的核心价值在于提升软件供应链的透明度和可管理性，从而增强安全性： 透明度与可追溯性： 清晰展示软件由哪些“零件”（开源库、第三方组件、内部模块等）组装而成，包括直接依赖和传递依赖，让软件的构成不再是“黑盒”。 高效的漏洞管理： 当某个组件爆出新的安全漏洞时，通过 SBOM 可以快速定位所有受影响的软件产品，及时采取修复或缓解措施，大大缩短应急响应时间。 许可证合规性审计： 准确识别所有组件的开源许可证类型，确保符合合规要求，避免潜在的法律风险。 供应链风险评估： 了解组件的来源、版本、维护状态等信息，有助于评估整个软件供应链的潜在风险。 提升软件质量与可信度： 向客户和合作伙伴提供 SBOM，能够证明你对软件安全和质量的重视，建立信任。 可以说，SBOM 是构筑现代软件供应链安全防线的基石。 SBOM 标准巡礼：SPDX、CycloneDX、SWID 与 DSDX 要让 SBOM [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-sbom-practice-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/22/go-sbom-practice">本文永久链接</a> &#8211; https://tonybai.com/2025/05/22/go-sbom-practice</p>
<p>大家好，我是Tony Bai。</p>
<p>近年来，软件供应链安全事件频发，从 SolarWinds 到 Log4Shell，每一次都给业界敲响了警钟。在这样的背景下，软件物料清单 (SBOM, Software Bill of Materials) 的重要性日益凸显。无论是甲方爸爸的硬性要求（尤其是在2B软件交付和白盒交付场景），还是我们自身对软件透明度和安全性的追求，SBOM 都已成为现代软件开发不可或缺的一环。</p>
<p>那么，SBOM 究竟是什么？它为何如此重要？市面上有哪些主流的 SBOM 标准？我们又该如何为自己的 Go 项目（当然，也适用于 Java、JS 等其他语言项目）生成和使用 SBOM 呢？</p>
<p>今天，我们就来一起深入探讨这些问题，为你揭开 SBOM 的神秘面纱。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<h2>SBOM：你的软件“配料表”，为何如此重要？</h2>
<p>想象一下，我们购买食品时会关注配料表，了解其成分、产地和营养信息。SBOM 之于软件，就如同食品的配料表。它是一份正式的、结构化的清单，详细列出了构成某个软件产品的所有组件及其依赖关系。</p>
<p><strong>SBOM 的核心价值在于提升软件供应链的透明度和可管理性，从而增强安全性：</strong></p>
<ol>
<li><strong>透明度与可追溯性：</strong> 清晰展示软件由哪些“零件”（开源库、第三方组件、内部模块等）组装而成，包括直接依赖和传递依赖，让软件的构成不再是“黑盒”。</li>
<li><strong>高效的漏洞管理：</strong> 当某个组件爆出新的安全漏洞时，通过 SBOM 可以快速定位所有受影响的软件产品，及时采取修复或缓解措施，大大缩短应急响应时间。</li>
<li><strong>许可证合规性审计：</strong> 准确识别所有组件的开源许可证类型，确保符合合规要求，避免潜在的法律风险。</li>
<li><strong>供应链风险评估：</strong> 了解组件的来源、版本、维护状态等信息，有助于评估整个软件供应链的潜在风险。</li>
<li><strong>提升软件质量与可信度：</strong> 向客户和合作伙伴提供 SBOM，能够证明你对软件安全和质量的重视，建立信任。</li>
</ol>
<p>可以说，SBOM 是构筑现代软件供应链安全防线的基石。</p>
<h2>SBOM 标准巡礼：SPDX、CycloneDX、SWID 与 DSDX</h2>
<p>要让 SBOM 真正发挥作用，统一的标准至关重要。目前，业界存在多个 SBOM 标准，各有侧重。我们重点关注几个主流和新兴的规范：</p>
<p><img src="../BlogImages/2025/go-sbom-practice-2.png" alt="" /></p>
<p><strong>1. SPDX (Software Package Data Exchange):</strong></p>
<ul>
<li><strong>定位与特点：</strong> 由 <strong>Linux Foundation</strong> 主导，是国际公认的 <a href="https://www.iso.org/standard/81870.html">SBOM 开放标准 (ISO/IEC 5962:2021)</a>。SPDX 最初更侧重于<strong>许可证合规性</strong>，但其规范已发展得非常全面，能够详细描述软件包、文件、代码片段及其之间的关系。</li>
<li><strong>核心数据字段：</strong> 包含包信息（名称、版本、供应商、下载位置、校验和）、文件信息（名称、类型、许可证、校验和）、许可证信息（SPDX许可证列表中的标准标识符、自定义许可证）、以及组件之间的关系（依赖、包含、生成等）。</li>
<li><strong>格式：</strong> 支持多种格式，如 Tag-Value、JSON、YAML、RDF/XML 等。</li>
<li><strong>适用场景：</strong> 许可证合规审计、知识产权管理、软件溯源、大型复杂项目的详细清单管理、漏洞管理等。<strong>由于其全面性和国际标准化地位，SPDX 是本次我们实战演练的重点。</strong></li>
</ul>
<p><strong>2. CycloneDX:</strong></p>
<ul>
<li><strong>定位与特点：</strong> 由 <strong>OWASP（开放式Web应用程序安全项目）</strong> 社区驱动，更侧重于<strong>安全用例和运营需求</strong>。它设计轻量、易于自动化生成和消费，非常适合在 CI/CD 流程中集成。</li>
<li><strong>核心数据字段：</strong> 关注组件（名称、版本、供应商、PURL、CPE）、依赖关系图谱、已知漏洞信息（或指向漏洞数据库的链接如 VEX）、服务信息、许可证信息等。</li>
<li><strong>格式：</strong> 主要支持 JSON 和 XML。</li>
<li><strong>适用场景：</strong> 漏洞管理、安全审计、软件成分分析 (SCA)、CI/CD 集成等。</li>
</ul>
<p><strong>3. SWID (Software Identification) Tags:</strong></p>
<ul>
<li><strong>定位与特点：</strong> 由 <strong>ISO/IEC 19770-2:2015</strong> 标准定义，主要用于<strong>软件资产管理 (SAM)</strong> 和安全。SWID 标签提供了识别已安装软件、追踪其生命周期（安装、更新、卸载）的方法。</li>
<li><strong>核心价值：</strong> 虽然 SWID 本身不直接提供完整的依赖关系图谱，但它可以作为 SBOM 中组件身份识别的重要依据，并能与其他 SBOM 格式结合使用。</li>
<li><strong>适用场景：</strong> 软件资产管理、安全配置管理、补丁管理。</li>
</ul>
<p><strong>4. DSDX (Digital Supply-chain Data Exchange):</strong></p>
<ul>
<li><strong>定位与特点：</strong> 这是由<strong>中国信息通信研究院（CAICT）</strong>牵头，联合国内多家单位共同研究制定的<strong>数字供应链数据交换标准</strong>。它旨在规范数字供应链中各类数据的描述、交换和共享，SBOM 是其关注的重要数据类型之一。</li>
<li><strong>核心价值：</strong> DSDX 致力于构建符合中国国情和产业发展需求的数字供应链标准体系，推动国内软件供应链的透明化和安全保障。</li>
<li><strong>适用场景：</strong> 国内企业间的软件供应链数据交换、合规性要求等。目前 DSDX 仍在发展和推广阶段，值得国内开发者关注。</li>
</ul>
<p><strong>标准之间的关系与选择：</strong></p>
<p>这些标准并非完全孤立。例如，SPDX 和 CycloneDX 都被广泛用于生成 SBOM，并且都符合美国 NTIA《软件物料清单的最小元素》的要求。SWID 标签可以增强 SBOM 中组件的识别能力。DSDX 则可能在未来成为国内数字供应链数据交换的重要规范。</p>
<p><strong>在实际操作中，SPDX 和 CycloneDX 是目前最主流的 SBOM 格式选择。</strong> 许多工具都支持生成这两种格式，它们之间也可以进行一定程度的转换。本次，我们将以 <strong>SPDX</strong> 为例进行后续的实战演示。</p>
<h2>Go 项目 SPDX SBOM 生成实战：利器 anchore/syft 登场</h2>
<p>理论说了不少，我们来动手实践一下。市面上有许多优秀的 SBOM 生成工具，今天我们选用一款广受欢迎的开源工具：<strong>anchore/syft</strong>。</p>
<p>syft 是一个功能强大的 CLI 工具和 Go 库，可以从容器镜像和文件系统中生成 SBOM。它支持多种 SBOM 格式（包括我们今天重点关注的 SPDX 和另一种主流格式 CycloneDX），并且对多种编程语言和包管理器有良好的支持。</p>
<h3>安装 syft</h3>
<p>你可以从其 GitHub Release 页面下载预编译的二进制文件，或者使用 Go 工具安装：</p>
<pre><code class="bash">$go install github.com/anchore/syft/cmd/syft@latest
</code></pre>
<p>确保你的 \$GOPATH/bin 或 \$GOBIN 在你的 PATH 环境变量中。</p>
<h3>实战：为知名 Go Web 框架 gin-gonic/gin 生成 SPDX JSON SBOM</h3>
<p>让我们以一个真实的、大家熟知的 Go 开源项目 <code>gin-gonic/gin</code> 为例。首先，你需要将项目克隆到本地：</p>
<pre><code class="bash">$git clone https://github.com/gin-gonic/gin.git
$cd gin
</code></pre>
<p>然后，在 gin 项目的根目录下，运行 syft 命令生成 SPDX JSON 格式(<a href="https://spdx.github.io/spdx-spec/v2.3/">spdx 2.3规范</a>)的 SBOM：</p>
<pre><code class="bash">$syft . -o spdx-json=gin-sbom.spdx.json
 ✔ Indexed file system                                                                                                        .
 ✔ Cataloged contents                                          cdb4ee2aea69cc6a83331bbe96dc2caa9a299d21329efb0336fc02a82e1839a8
   ├── ✔ Packages                        [48 packages]
   ├── ✔ Executables                     [0 executables]
   ├── ✔ File digests                    [4 files]
   └── ✔ File metadata                   [4 locations]
[0000]  WARN no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not id
... ...
</code></pre>
<p>这里的“.”代表当前目录。syft会自动识别 Go 项目的 go.mod 文件来解析依赖，并将结果输出到 gin-sbom.spdx.json 文件中。</p>
<blockquote>
<p>注：截至目前，<a href="https://spdx.github.io/spdx-spec/v3.0.1/">spdx的最新规范版本为3.0.1</a>。</p>
</blockquote>
<p>生成的 gin-sbom.spdx.json 文件内容片段示例：</p>
<pre><code>{
    "spdxVersion": "SPDX-2.3",
    "dataLicense": "CC0-1.0",
    "SPDXID": "SPDXRef-DOCUMENT",
    "name": ".",
    "documentNamespace": "https://anchore.com/syft/dir/453d49c6-8063-46f1-9d7e-61dd7e789f6d",
    "creationInfo": {
        "licenseListVersion": "3.25",
        "creators": [
            "Organization: Anchore, Inc",
            "Tool: syft-[not provided]"
        ],
        "created": "2025-05-17T22:45:19Z"
    },
    "packages": [
        {
            "name": "actions/cache",
            "SPDXID": "SPDXRef-Package-github-action-actions-cache-422933d2a61f8d51",
            "versionInfo": "v4",
            "supplier": "Organization: GitHub",
            "originator": "Organization: GitHub",
            "downloadLocation": "NOASSERTION",
            "filesAnalyzed": false,
            "sourceInfo": "acquired package info from GitHub Actions workflow file or composite action file: /.github/workflows/gin.yml",
            "licenseConcluded": "NOASSERTION",
            "licenseDeclared": "NOASSERTION",
            "copyrightText": "NOASSERTION",
            "externalRefs": [
                {
                    "referenceCategory": "SECURITY",
                    "referenceType": "cpe23Type",
                    "referenceLocator": "cpe:2.3:a:actions\\/cache:actions\\/cache:v4:*:*:*:*:*:*:*"
                },
                {
                    "referenceCategory": "PACKAGE-MANAGER",
                    "referenceType": "purl",
                    "referenceLocator": "pkg:github/actions/cache@v4"
                }
            ]
        },
... ...
</code></pre>
<p>SPDX JSON 格式详细记录了文档信息、包信息（包括名称、版本、SPDXID、许可证、PURL等）以及它们之间的依赖关系。</p>
<h3>syft输出定制</h3>
<p>如果你觉得syft默认输出到json文件中的信息不全，你可以对syft的行为做一些配置，可以使用syft配置文件，也可以使用环境变量。</p>
<p>syft默认的配置文件位置有如下几个(优先级从高到低)：</p>
<pre><code>.syft.yaml
.syft/config.yaml
~/.syft.yaml
&lt;XDG_CONFIG_HOME&gt;/syft/config.yaml
</code></pre>
<p>如果你不知道配置文件的格式，可以执行syft config查看当前配置：</p>
<pre><code>$syft config
log:
  # suppress all logging output (env: SYFT_LOG_QUIET)
  quiet: false

  # increase verbosity (-v = info, -vv = debug) (env: SYFT_LOG_VERBOSITY)
  verbosity: 0

  # explicitly set the logging level (available: [error warn info debug trace]) (env: SYFT_LOG_LEVEL)
  level: 'warn'

  # file path to write logs to (env: SYFT_LOG_FILE)
  file: ''

dev:
  # capture resource profiling data (available: [cpu, mem]) (env: SYFT_DEV_PROFILE)
  profile: ''

# the configuration file(s) used to load application configuration (env: SYFT_CONFIG)
config: ''

# the output format(s) of the SBOM report (options: syft-table, syft-text, syft-json, spdx-json, ...)
# to specify multiple output files in differing formats, use a list:
# output:
#   - "syft-json=&lt;syft-json-output-file&gt;"
#   - "spdx-json=&lt;spdx-json-output-file&gt;" (env: SYFT_OUTPUT)
output:
  - 'syft-table'

# file to write the default report output to (default is STDOUT) (env: SYFT_LEGACYFILE)
legacyFile: ''

format:
  # default value for all formats that support the "pretty" option (default is unset) (env: SYFT_FORMAT_PRETTY)
  pretty:

  template:
    # path to the template file to use when rendering the output with the template output format.
    # Note that all template paths are based on the current syft-json schema (env: SYFT_FORMAT_TEMPLATE_PATH)
    path: ''

    # if true, uses the go structs for the syft-json format for templating.
    # if false, uses the syft-json output for templating (which follows the syft JSON schema exactly).
    #
    # Note: long term support for this option is not guaranteed (it may change or break at any time) (env: SYFT_FORMAT_TEMPLATE_LEGACY)
    legacy: false

  json:
    # transform any syft-json output to conform to an approximation of the v11.0.1 schema. This includes:
    # - using the package metadata type names from before v12 of the JSON schema (changed in https://github.com/anchore/syft/pull/1983)
    #
    # Note: this will still include package types and fields that were added at or after json schema v12. This means
    # that output might not strictly be json schema v11 compliant, however, for consumers that require time to port
    # over to the final syft 1.0 json output this option can be used to ease the transition.
    #
    # Note: long term support for this option is not guaranteed (it may change or break at any time) (env: SYFT_FORMAT_JSON_LEGACY)
    legacy: false

    # include space indentation and newlines
    # note: inherits default value from 'format.pretty' or 'false' if parent is unset (env: SYFT_FORMAT_JSON_PRETTY)
    pretty:

  spdx-json:
    # include space indentation and newlines
    # note: inherits default value from 'format.pretty' or 'false' if parent is unset (env: SYFT_FORMAT_SPDX_JSON_PRETTY)
    pretty:

  cyclonedx-json:
    # include space indentation and newlines
    # note: inherits default value from 'format.pretty' or 'false' if parent is unset (env: SYFT_FORMAT_CYCLONEDX_JSON_PRETTY)
    pretty:

  cyclonedx-xml:
    # include space indentation and newlines
    # note: inherits default value from 'format.pretty' or 'false' if parent is unset (env: SYFT_FORMAT_CYCLONEDX_XML_PRETTY)
    pretty:

# whether to check for an application update on start up or not (env: SYFT_CHECK_FOR_APP_UPDATE)
check-for-app-update: true

# enable one or more package catalogers (env: SYFT_CATALOGERS)
catalogers: []

# set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source) (env: SYFT_DEFAULT_CATALOGERS)
default-catalogers: []

# add, remove, and filter the catalogers to be used (env: SYFT_SELECT_CATALOGERS)
select-catalogers: []

package:
  # search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc)
  # note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed
  # note: for now this only applies to the java package cataloger (env: SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES)
  search-unindexed-archives: false

  # search within archives that do contain a file index to search against (zip)
  # note: for now this only applies to the java package cataloger (env: SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES)
  search-indexed-archives: true

  # allows users to exclude synthetic binary packages from the sbom
  # these packages are removed if an overlap with a non-synthetic package is found (env: SYFT_PACKAGE_EXCLUDE_BINARY_OVERLAP_BY_OWNERSHIP)
  exclude-binary-overlap-by-ownership: true

license:
  # include the content of licenses in the SBOM for a given syft scan; valid values are: [all unknown none] (env: SYFT_LICENSE_CONTENT)
  content: 'none'

  # deprecated: please use 'license-content' instead (env: SYFT_LICENSE_INCLUDE_UNKNOWN_LICENSE_CONTENT)
  include-unknown-license-content:

  # adjust the percent as a fraction of the total text, in normalized words, that
  # matches any valid license for the given inputs, expressed as a percentage across all of the licenses matched. (env: SYFT_LICENSE_COVERAGE)
  coverage: 75

  # deprecated: please use 'coverage' instead (env: SYFT_LICENSE_LICENSE_COVERAGE)
  license-coverage:

file:
  metadata:
    # select which files should be captured by the file-metadata cataloger and included in the SBOM.
    # Options include:
    #  - "all": capture all files from the search space
    #  - "owned-by-package": capture only files owned by packages
    #  - "none", "": do not capture any files (env: SYFT_FILE_METADATA_SELECTION)
    selection: 'owned-by-package'

    # the file digest algorithms to use when cataloging files (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512") (env: SYFT_FILE_METADATA_DIGESTS)
    digests:
      - 'sha1'
      - 'sha256'

  content:
    # skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes) (env: SYFT_FILE_CONTENT_SKIP_FILES_ABOVE_SIZE)
    skip-files-above-size: 256000

    # file globs for the cataloger to match on (env: SYFT_FILE_CONTENT_GLOBS)
    globs: []

  executable:
    # file globs for the cataloger to match on (env: SYFT_FILE_EXECUTABLE_GLOBS)
    globs: []

# selection of layers to catalog, options=[squashed all-layers deep-squashed] (env: SYFT_SCOPE)
scope: 'squashed'

# number of cataloger workers to run in parallel
# by default, when set to 0: this will be based on runtime.NumCPU * 4, if set to less than 0 it will be unbounded (env: SYFT_PARALLELISM)
parallelism: 0

relationships:
  # include package-to-file relationships that indicate which files are owned by which packages (env: SYFT_RELATIONSHIPS_PACKAGE_FILE_OWNERSHIP)
  package-file-ownership: true

  # include package-to-package relationships that indicate one package is owned by another due to files claimed to be owned by one package are also evidence of another package's existence (env: SYFT_RELATIONSHIPS_PACKAGE_FILE_OWNERSHIP_OVERLAP)
  package-file-ownership-overlap: true

compliance:
  # action to take when a package is missing a name (env: SYFT_COMPLIANCE_MISSING_NAME)
  missing-name: 'drop'

  # action to take when a package is missing a version (env: SYFT_COMPLIANCE_MISSING_VERSION)
  missing-version: 'stub'

# Enable data enrichment operations, which can utilize services such as Maven Central and NPM.
# By default all enrichment is disabled, use: all to enable everything.
# Available options are: all, golang, java, javascript (env: SYFT_ENRICH)
enrich: []

dotnet:
  # only keep dep.json packages which an executable on disk is found. The package is also included if a DLL is found for any child package, even if the package itself does not have a DLL. (env: SYFT_DOTNET_DEP_PACKAGES_MUST_HAVE_DLL)
  dep-packages-must-have-dll: false

  # only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk). The package is also included if any child package claims a DLL, even if the package itself does not claim a DLL. (env: SYFT_DOTNET_DEP_PACKAGES_MUST_CLAIM_DLL)
  dep-packages-must-claim-dll: true

  # treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package (env: SYFT_DOTNET_PROPAGATE_DLL_CLAIMS_TO_PARENTS)
  propagate-dll-claims-to-parents: true

  # show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack) (env: SYFT_DOTNET_RELAX_DLL_CLAIMS_WHEN_BUNDLING_DETECTED)
  relax-dll-claims-when-bundling-detected: true

golang:
  # search for go package licences in the GOPATH of the system running Syft, note that this is outside the
  # container filesystem and potentially outside the root of a local directory scan (env: SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES)
  search-local-mod-cache-licenses:

  # specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod (env: SYFT_GOLANG_LOCAL_MOD_CACHE_DIR)
  local-mod-cache-dir: '~/Go/pkg/mod'

  # search for go package licences in the vendor folder on the system running Syft, note that this is outside the
  # container filesystem and potentially outside the root of a local directory scan (env: SYFT_GOLANG_SEARCH_LOCAL_VENDOR_LICENSES)
  search-local-vendor-licenses:

  # specify an explicit go vendor directory, if unset this defaults to ./vendor (env: SYFT_GOLANG_LOCAL_VENDOR_DIR)
  local-vendor-dir: ''

  # search for go package licences by retrieving the package from a network proxy (env: SYFT_GOLANG_SEARCH_REMOTE_LICENSES)
  search-remote-licenses:

  # remote proxy to use when retrieving go packages from the network,
  # if unset this defaults to $GOPROXY followed by https://proxy.golang.org (env: SYFT_GOLANG_PROXY)
  proxy: 'https://goproxy.cn,direct'

  # specifies packages which should not be fetched by proxy
  # if unset this defaults to $GONOPROXY (env: SYFT_GOLANG_NO_PROXY)
  no-proxy: 'gomod.io,10.170.133.199'

  main-module-version:
    # look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0) (env: SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_LD_FLAGS)
    from-ld-flags: true

    # search for semver-like strings in the binary contents (env: SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_CONTENTS)
    from-contents: false

    # use the build settings (e.g. vcs.version &amp; vcs.time) to craft a v0 pseudo version
    # (e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise (env: SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_BUILD_SETTINGS)
    from-build-settings: true

java:
  # enables Syft to use the network to fetch version and license information for packages when
  # a parent or imported pom file is not found in the local maven repository.
  # the pom files are downloaded from the remote Maven repository at 'maven-url' (env: SYFT_JAVA_USE_NETWORK)
  use-network:

  # use the local Maven repository to retrieve pom files. When Maven is installed and was previously used
  # for building the software that is being scanned, then most pom files will be available in this
  # repository on the local file system. this greatly speeds up scans. when all pom files are available
  # in the local repository, then 'use-network' is not needed.
  # TIP: If you want to download all required pom files to the local repository without running a full
  # build, run 'mvn help:effective-pom' before performing the scan with syft. (env: SYFT_JAVA_USE_MAVEN_LOCAL_REPOSITORY)
  use-maven-local-repository:

  # override the default location of the local Maven repository.
  # the default is the subdirectory '.m2/repository' in your home directory (env: SYFT_JAVA_MAVEN_LOCAL_REPOSITORY_DIR)
  maven-local-repository-dir: '~/.m2/repository'

  # maven repository to use, defaults to Maven central (env: SYFT_JAVA_MAVEN_URL)
  maven-url: 'https://repo1.maven.org/maven2'

  # depth to recursively resolve parent POMs, no limit if &lt;= 0 (env: SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH)
  max-parent-recursive-depth: 0

  # resolve transient dependencies such as those defined in a dependency's POM on Maven central (env: SYFT_JAVA_RESOLVE_TRANSITIVE_DEPENDENCIES)
  resolve-transitive-dependencies: false

javascript:
  # enables Syft to use the network to fill in more detailed license information (env: SYFT_JAVASCRIPT_SEARCH_REMOTE_LICENSES)
  search-remote-licenses:

  # base NPM url to use (env: SYFT_JAVASCRIPT_NPM_BASE_URL)
  npm-base-url: ''

  # include development-scoped dependencies (env: SYFT_JAVASCRIPT_INCLUDE_DEV_DEPENDENCIES)
  include-dev-dependencies:

linux-kernel:
  # whether to catalog linux kernel modules found within lib/modules/** directories (env: SYFT_LINUX_KERNEL_CATALOG_MODULES)
  catalog-modules: true

nix:
  # enumerate all files owned by packages found within Nix store paths (env: SYFT_NIX_CAPTURE_OWNED_FILES)
  capture-owned-files: false

python:
  # when running across entries in requirements.txt that do not specify a specific version
  # (e.g. "sqlalchemy &gt;= 1.0.0, &lt;= 2.0.0, != 3.0.0, &lt;= 3.0.0"), attempt to guess what the version could
  # be based on the version requirements specified (e.g. "1.0.0"). When enabled the lowest expressible version
  # when given an arbitrary constraint will be used (even if that version may not be available/published). (env: SYFT_PYTHON_GUESS_UNPINNED_REQUIREMENTS)
  guess-unpinned-requirements: false

registry:
  # skip TLS verification when communicating with the registry (env: SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY)
  insecure-skip-tls-verify: false

  # use http instead of https when connecting to the registry (env: SYFT_REGISTRY_INSECURE_USE_HTTP)
  insecure-use-http: false

  # Authentication credentials for specific registries. Each entry describes authentication for a specific authority:
  # -   authority: the registry authority URL the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) (env: SYFT_REGISTRY_AUTH_AUTHORITY)
  #     username: a username if using basic credentials (env: SYFT_REGISTRY_AUTH_USERNAME)
  #     password: a corresponding password (env: SYFT_REGISTRY_AUTH_PASSWORD)
  #     token: a token if using token-based authentication, mutually exclusive with username/password (env: SYFT_REGISTRY_AUTH_TOKEN)
  #     tls-cert: filepath to the client certificate used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_CERT)
  #     tls-key: filepath to the client key used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_KEY)
  auth: []

  # filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate (env: SYFT_REGISTRY_CA_CERT)
  ca-cert: ''

# specify the source behavior to use (e.g. docker, registry, oci-dir, ...) (env: SYFT_FROM)
from: []

# an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux') (env: SYFT_PLATFORM)
platform: ''

source:
  # set the name of the target being analyzed (env: SYFT_SOURCE_NAME)
  name: ''

  # set the version of the target being analyzed (env: SYFT_SOURCE_VERSION)
  version: ''

  # base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory (env: SYFT_SOURCE_BASE_PATH)
  base-path: ''

  file:
    # the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512") (env: SYFT_SOURCE_FILE_DIGESTS)
    digests:
      - 'SHA-256'

  image:
    # allows users to specify which image source should be used to generate the sbom
    # valid values are: registry, docker, podman (env: SYFT_SOURCE_IMAGE_DEFAULT_PULL_SOURCE)
    default-pull-source: ''

    # (env: SYFT_SOURCE_IMAGE_MAX_LAYER_SIZE)
    max-layer-size: ''

# exclude paths from being scanned using a glob expression (env: SYFT_EXCLUDE)
exclude: []

unknowns:
  # remove unknown errors on files with discovered packages (env: SYFT_UNKNOWNS_REMOVE_WHEN_PACKAGES_DEFINED)
  remove-when-packages-defined: true

  # include executables without any identified packages (env: SYFT_UNKNOWNS_EXECUTABLES_WITHOUT_PACKAGES)
  executables-without-packages: true

  # include archives which were not expanded and searched (env: SYFT_UNKNOWNS_UNEXPANDED_ARCHIVES)
  unexpanded-archives: true

cache:
  # root directory to cache any downloaded content; empty string will use an in-memory cache (env: SYFT_CACHE_DIR)
  dir: '~/Library/Caches/syft'

  # time to live for cached data; setting this to 0 will disable caching entirely (env: SYFT_CACHE_TTL)
  ttl: '7d'

# show catalogers that have been de-selected (env: SYFT_SHOW_HIDDEN)
show-hidden: false

attest:
  # the key to use for the attestation (env: SYFT_ATTEST_KEY)
  key: ''

  # password to decrypt to given private key
  # additionally responds to COSIGN_PASSWORD env var (env: SYFT_ATTEST_PASSWORD)
  password: ''
</code></pre>
<p>也可将输出的当前配置保存为上面配置文件中的任何一个，然后做配置定制。</p>
<p>此外，我们看到对于每个重要的配置，都会有一个环境变量对应，比如：</p>
<pre><code>SYFT_FORMAT_SPDX_JSON_PRETTY - spdx json格式美化
SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES - 在本地go module cache查找license信息
SYFT_GOLANG_SEARCH_REMOTE_LICENSES - 通过GOPROXY查找go module的license信息
</code></pre>
<p>如果你对license信息比较看重，我们可以基于上述环境变量配置再重新生成一次gin的SBOM：</p>
<pre><code>$export SYFT_FORMAT_SPDX_JSON_PRETTY=true
$export SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES=true
$export SYFT_GOLANG_SEARCH_REMOTE_LICENSES=true
$syft . -o spdx-json=gin-sbom.spdx.json
</code></pre>
<h3>关于 Java 和 JavaScript 项目</h3>
<p>syft 同样能够为 Java (如 Maven, Gradle) 和 JavaScript (如 npm, yarn) 等项目生成 SPDX 或其他格式的 SBOM。其基本使用方式与 Go 项目类似，通常只需将扫描路径指向你的 Java 或 JavaScript 项目根目录即可。syft 会自动识别对应的包管理文件（如 pom.xml, package-lock.json）并解析依赖。更详细的用法和特定语言的注意事项，推荐查阅 anchore/syft 的官方文档。</p>
<h2>让 SPDX SBOM 清单“说话”：将 Go 项目的 SPDX JSON 转换为 Excel</h2>
<p>生成的 SPDX JSON 文件虽然结构清晰，便于机器处理，但对于需要提交给甲方或公司安全合规团队进行人工审计的场景，Excel 格式往往更受欢迎。</p>
<p>我们可以使用 Linux Foundation 维护的官方<a href="https://tools.spdx.org/app/convert/">SPDX online Tools</a> 来实现这个转换。</p>
<p>通过浏览器打开https://tools.spdx.org/app/convert/，选择将spdx json转换为xlsx格式，并上传gin-sbom.spdx.json文件，点击Convert：</p>
<p><img src="../BlogImages/2025/go-sbom-practice-3.png" alt="" /></p>
<p>转换成功后，下载生成的excel文件，该文件的内容如下截图：</p>
<p><img src="../BlogImages/2025/go-sbom-practice-4.png" alt="" /></p>
<p>转换后的 Excel 文档通常会包含多个工作表，例如：Document Information, Package Information, Per File Information (如果分析到文件级别), Relationships, Licensing Information 等。 通过这样的表格，团队成员可以更方便地进行许可证审计、版本检查和依赖关系梳理。</p>
<p>当然SPDX 社区和第三方也都提供了一些工具来帮助完成此类转换，有gui的，也有命令行，大家可以自己的需求使用不同的转换工具。</p>
<h2>SBOM 的更广阔图景与 Go 开发者的行动</h2>
<p>生成 SBOM 只是第一步。它的真正价值在于融入到整个软件开发生命周期中：</p>
<ul>
<li><strong>CI/CD 集成：</strong> 在构建过程中自动生成 SBOM，并进行漏洞扫描（例如与 Trivy、Grype 等工具结合）和许可证策略检查。</li>
<li><strong>VEX (Vulnerability Exploitability eXchange)：</strong> 结合 VEX 文档，可以更准确地判断 SBOM 中列出的漏洞在当前产品中是否真正可被利用，减少误报。</li>
<li><strong>持续监控：</strong> 定期重新生成 SBOM 并分析，以应对新出现的漏洞和组件更新。</li>
</ul>
<p>对于我们 Gopher 而言，掌握 SBOM（特别是 SPDX 这样被广泛认可的标准）的生成和使用，不仅是满足日益增长的合规要求，更是提升自身软件质量、安全意识和专业素养的体现。Go 语言的静态编译特性和完善的模块系统 (go.mod)，使得像 syft 这样的工具能够相对容易和准确地分析依赖关系，生成高质量的 SBOM。</p>
<h2>小结</h2>
<p>软件供应链安全是一项系统工程，而 SBOM 则是其中不可或缺的一块拼图。它为我们提供了一双“透视眼”，让我们能够清晰地了解软件的“前世今生”，从容应对潜在的风险。</p>
<p>无论是选择 SPDX、CycloneDX，还是 SWID 或 DSDX，理解并实践 SBOM 的核心理念至关重要。利用 syft 这样的工具，为你的 Go 项目（以及其他语言项目）生成并维护一份符合 SPDX 标准的 SBOM，都应该成为我们开发实践中的一项基本功。</p>
<p>现在，就动手为你的项目构建一份清晰的“软件家谱”吧！</p>
<hr />
<p><strong>聊一聊，也帮个忙：</strong></p>
<ul>
<li><strong>在你的工作中，是否已经开始被要求提供 SBOM？你主要关注 SBOM 的哪些方面（安全、合规、还是其他）？你通常使用哪种 SBOM 标准？</strong></li>
<li><strong>除了 syft，你还知道或使用过哪些优秀的 SBOM 生成或分析工具？特别是针对 SPDX 格式的。</strong></li>
<li><strong>你认为在 Go 社区，我们还可以做些什么来进一步推动 SBOM（尤其是 SPDX 标准）的普及和应用？</strong></li>
</ul>
<p>欢迎在<strong>评论区</strong>留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助，也请<strong>转发给你身边的开发者朋友们</strong>，让更多人了解和重视 SBOM！</p>
<p><strong>想与我进行更深入的 Go 语言、软件供应链安全与 AI 技术交流吗？</strong> 欢迎加入我的<strong>“Go &amp; AI 精进营”知识星球</strong>。</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p>我们星球见！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/22/go-sbom-practice/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go testing包将迎来新增强：标准化属性与持久化构件API即将落地</title>
		<link>https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir/</link>
		<comments>https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir/#comments</comments>
		<pubDate>Sun, 06 Apr 2025 22:45:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Allure]]></category>
		<category><![CDATA[Aritfact]]></category>
		<category><![CDATA[Attributes]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Jira]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[JUnit]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[Logf]]></category>
		<category><![CDATA[pprof]]></category>
		<category><![CDATA[profile]]></category>
		<category><![CDATA[subtest]]></category>
		<category><![CDATA[TB]]></category>
		<category><![CDATA[TempDir]]></category>
		<category><![CDATA[test2json]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[TestRail]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[子测试]]></category>
		<category><![CDATA[属性]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[测试]]></category>
		<category><![CDATA[测试构件]]></category>
		<category><![CDATA[测试管理]]></category>
		<category><![CDATA[键值对]]></category>
		<category><![CDATA[集成]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4542</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir Go语言的testing包即将迎来两项备受期待的增强功能：标准化的测试属性（Test Attributes）和测试构件（Test Artifacts）管理。这两项提案（#43936 和#71287）均已获得Go团队的批准或高度认可，旨在显著提升Go测试的可观测性、调试效率以及与外部工具链（如CI/CD系统、测试管理平台）的集成能力。本文将深入解读这两项提案的设计理念、核心API、应用场景及其对Go开发者的潜在影响。 1. Go测试过程中的“痛点” 长期以来，Go开发者在处理测试过程中的元数据和输出文件时，常常面临一些挑战，不得不依赖非标准的约定或变通方法，这直接影响了测试效率和工具集成的流畅性。 1.1 痛点一：脆弱且混乱的测试元数据传递 现代开发流程中，我们常常需要将测试与外部系统关联起来。例如，将自动化测试结果上报给TestRail或Allure这样的测试管理平台，或者在CI/CD报告中直接链接到相关的Jira问题、代码提交或详细日志。 在t.Attr提案（#43936）出现之前，开发者通常只能通过t.Log或t.Logf输出特定格式的字符串来实现这一目标，例如类似以下的日志行： // 示例：试图通过日志传递元数据 TESTRAIL_CASE_ID: C12345 JIRA_ISSUE: PROJ-789 这种方法的弊端显而易见： 极其脆弱: 任何对日志格式、前缀或分隔符的微小改动，都可能导致依赖这些日志的外部解析工具（如CI脚本、报告生成器）失效。 缺乏标准: 每个项目或团队可能会发明自己的格式，导致工具难以复用和维护。 信息混杂: 重要的元数据与普通的测试日志信息混合在一起，增加了提取难度和误判的可能性。 工具集成困难: 像go test -json这样的官方工具，其输出的Action: output 事件并不区分普通日志和这种“伪装”的元数据，下游消费者需要进行额外的、不可靠的字符串解析。 总之，这种方式给需要自动化处理测试结果的场景带来了持续的维护负担和不确定性。 当然痛点不限于此，我们再来看一个。 1.2 痛点二：转瞬即逝的测试构件，调试与归档的障碍 Go testing包提供了t.TempDir函数，用于创建测试期间使用的临时目录和文件，这在隔离测试状态方面非常有用。然而，t.TempDir的核心特性——在测试（无论成功或失败）结束后自动清理其内容——在某些场景下反而成了阻碍。想象以下常见情况： 调试失败 一个复杂的集成测试失败了。测试过程中可能生成了详细的调试日志、服务间通信的网络抓包、或者是对比失败的实际输出文件。当你想检查这些文件以定位问题时，却发现它们随着测试的结束一同消失了。开发者不得不采取临时措施，比如注释掉t.Cleanup调用，或者在测试失败路径上手动复制文件到其他位置，过程繁琐且容易遗漏。 CI结果归档 在CI/CD流水线中，我们通常希望在测试失败时自动收集相关的诊断信息（如core dump、截图、性能剖析文件等）作为“构件(artifact)”进行归档，以便后续分析。虽然Go提供了-cpuprofile, -memprofile等标志并将结果放入-outputdir指定的目录，但对于测试代码自身产生的其他类型构件，缺乏一个统一且可靠的机制来指示它们需要被保留。 为了解决上述这些长期存在的痛点，Go社区积极讨论并推进了t.Attr和t.ArtifactDir这两项关键提案，旨在通过标准化的API为go testing包带来现代化的测试信息管理能力。 下面我们就来正式看看这两个提案究竟给我们带来了哪些测试过程中的便利。先来看看t.Attr提案。 2. t.Attr：为测试附加结构化元数据(#43936) 状态：已接受 (Accepted) 提案#43936旨在提供一种标准化的方式，将结构化的键值对元数据与特定的测试（或子测试）关联起来，并使其在go test -json的输出中易于访问。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-testing-add-attr-and-artifactdir-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir">本文永久链接</a> &#8211; https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir</p>
<p>Go语言的testing包即将迎来两项备受期待的增强功能：标准化的测试属性（Test Attributes）和测试构件（Test Artifacts）管理。这两项提案（#43936 和#71287）均已获得Go团队的批准或高度认可，旨在显著提升Go测试的可观测性、调试效率以及与外部工具链（如CI/CD系统、测试管理平台）的集成能力。本文将深入解读这两项提案的设计理念、核心API、应用场景及其对Go开发者的潜在影响。</p>
<h2>1. Go测试过程中的“痛点”</h2>
<p>长期以来，Go开发者在处理测试过程中的元数据和输出文件时，常常面临一些挑战，不得不依赖非标准的约定或变通方法，这直接影响了测试效率和工具集成的流畅性。</p>
<h3>1.1 痛点一：脆弱且混乱的测试元数据传递</h3>
<p>现代开发流程中，我们常常需要将测试与外部系统关联起来。例如，将自动化测试结果上报给<a href="https://www.testrail.com/">TestRail</a>或<a href="https://allurereport.org/">Allure</a>这样的测试管理平台，或者在CI/CD报告中直接链接到相关的Jira问题、代码提交或详细日志。</p>
<p>在t.Attr提案（#43936）出现之前，开发者通常只能通过t.Log或t.Logf输出特定格式的字符串来实现这一目标，例如类似以下的日志行：</p>
<pre><code>// 示例：试图通过日志传递元数据
TESTRAIL_CASE_ID: C12345
JIRA_ISSUE: PROJ-789
</code></pre>
<p>这种方法的弊端显而易见：</p>
<ul>
<li><strong>极其脆弱:</strong> 任何对日志格式、前缀或分隔符的微小改动，都可能导致依赖这些日志的外部解析工具（如CI脚本、报告生成器）失效。</li>
<li><strong>缺乏标准:</strong> 每个项目或团队可能会发明自己的格式，导致工具难以复用和维护。</li>
<li><strong>信息混杂:</strong> 重要的元数据与普通的测试日志信息混合在一起，增加了提取难度和误判的可能性。</li>
<li><strong>工具集成困难:</strong> 像go test -json这样的官方工具，其输出的Action: output 事件并不区分普通日志和这种“伪装”的元数据，下游消费者需要进行额外的、不可靠的字符串解析。</li>
</ul>
<p>总之，这种方式给需要自动化处理测试结果的场景带来了持续的维护负担和不确定性。</p>
<p>当然痛点不限于此，我们再来看一个。</p>
<h3>1.2 痛点二：转瞬即逝的测试构件，调试与归档的障碍</h3>
<p>Go testing包提供了t.TempDir函数，用于创建测试期间使用的临时目录和文件，这在隔离测试状态方面非常有用。然而，t.TempDir的核心特性——在测试（无论成功或失败）结束后自动清理其内容——在某些场景下反而成了阻碍。想象以下常见情况：</p>
<ul>
<li><strong>调试失败</strong> </li>
</ul>
<p>一个复杂的集成测试失败了。测试过程中可能生成了详细的调试日志、服务间通信的网络抓包、或者是对比失败的实际输出文件。当你想检查这些文件以定位问题时，却发现它们随着测试的结束一同消失了。开发者不得不采取临时措施，比如注释掉t.Cleanup调用，或者在测试失败路径上手动复制文件到其他位置，过程繁琐且容易遗漏。</p>
<ul>
<li><strong>CI结果归档</strong> </li>
</ul>
<p>在CI/CD流水线中，我们通常希望在测试失败时自动收集相关的诊断信息（如core dump、截图、性能剖析文件等）作为“构件(artifact)”进行归档，以便后续分析。虽然Go提供了-cpuprofile, -memprofile等标志并将结果放入-outputdir指定的目录，但对于测试代码自身产生的其他类型构件，缺乏一个统一且可靠的机制来指示它们需要被保留。</p>
<p>为了解决上述这些长期存在的痛点，Go社区积极讨论并推进了t.Attr和t.ArtifactDir这两项关键提案，旨在通过标准化的API为go testing包带来现代化的测试信息管理能力。</p>
<p>下面我们就来正式看看这两个提案究竟给我们带来了哪些测试过程中的便利。先来看看t.Attr提案。</p>
<h2>2. t.Attr：为测试附加结构化元数据(#43936)</h2>
<p><strong>状态：已接受 (Accepted)</strong></p>
<p>提案#43936旨在提供一种标准化的方式，将结构化的键值对元数据与特定的测试（或<a href="https://tonybai.com/2023/03/15/an-intro-of-go-subtest">子测试</a>）关联起来，并使其在go test -json的输出中易于访问。</p>
<h3>2.1 核心API</h3>
<p>该提案在testing.TB接口中增加了Attr方法，其定义如下：</p>
<pre><code class="go">package testing

type TB interface {
    // ... 其他方法

    // Attr 发出与此测试关联的测试属性。
    //
    // key不能包含空白字符。
    // 不同属性键的含义由持续集成系统和测试框架决定。
    //
    // 测试属性会立即在测试日志中发出，但应被视为无序的。
    Attr(key, value string)
}
</code></pre>
<p>开发者可以在测试代码中调用t.Attr(“myKey”, “myValue”)来记录元数据。经过社区的深入讨论，API最终确定为接受string类型的键和值。这主要是字符串简洁，易于理解和使用；与现有的主流测试管理系统（如 JUnit XML、Google 内部的 Sponge 系统）对属性/特性的定义（通常是string-string）保持一致。同时，还避免testing包引入对encoding/json的依赖。如果需要传递复杂结构，开发者可以自行将值JSON编码为字符串。</p>
<h3>2.2 输出格式</h3>
<p>t.Attr的调用会在标准测试日志中产生如下格式的输出：</p>
<pre><code>=== ATTR  TestName &lt;key&gt; &lt;value&gt;
</code></pre>
<p>当使用go test -json运行时，<a href="https://pkg.go.dev/cmd/test2json">test2json工具</a>会将其转换为结构化的JSON事件：</p>
<pre><code>{"Time": "...", "Action": "attr", "Package": "package/path", "Test": "TestName", "Key": "key", "Value": "value"}
</code></pre>
<p>go testing包增加了Attr后，在测试管理中，集成Go测试与系统如TestRail和Allure变得更加轻松，通过t.Attr可传递测试用例ID、特性标签和故事标签等信息。此外，测试输出中可以嵌入指向外部资源的链接，如日志系统、问题跟踪器（如Jira）、构建产物和文档。这种方式增强了CI/CD流程，使CI系统能够解析这些属性，以便于测试结果的分类、过滤和报告生成，或触发特定工作流，例如通过t.Attr(“environment”, “staging”)标记测试运行环境或关联代码提交哈希。最终，这种标准化的方法告别了脆弱的日志解析，提供了一种可靠的方式来提取测试元数据，取代了过去依赖特定日志前缀或格式的做法。</p>
<p>接下来，我们再来看看另外一个增强项：t.ArtifactDir。</p>
<h2>3. t.ArtifactDir：持久化测试构件(#71287)</h2>
<p><strong>状态：很可能接受(Likely Accept)</strong></p>
<p>提案#71287针对的是测试过程中产生的、可能需要后续检查的文件（即“测试构件(Artifact)”），它提供了一种机制，让开发者可以选择性地保留这些文件，而不是让它们被t.TempDir这种“阅后即焚”的特性自动删除。</p>
<h3>3.1 核心API与标志</h3>
<p>该提案在testing.TB接口中增加了ArtifactDir方法，其定义如下：</p>
<pre><code class="go">package testing

type TB interface {
    // ... 其他方法

    // ArtifactDir 返回一个目录供测试存储输出文件。
    // 当提供了 -artifacts 标志时，此目录将位于输出目录下。
    // 否则，ArtifactDir 返回一个临时目录，该目录在测试完成后被移除。
    //
    // 每个测试或子测试（在每个测试包内）都有一个唯一的构件目录。
    // 在同一测试或子测试中重复调用 ArtifactDir 返回相同的目录。
    // 子测试的输出不位于父测试的输出目录下。
    ArtifactDir() string
}
</code></pre>
<p>与此API配套的是一个新的go test命令行标志：-artifacts。它的行为特点如下：</p>
<ul>
<li><strong>默认行为 (未指定-artifacts)</strong> </li>
</ul>
<p>在这种情况下，t.ArtifactDir()的行为类似于t.TempDir()，返回一个临时目录，测试结束后其内容会被清理。这确保了测试行为的一致性，无论是否需要持久化构件。</p>
<ul>
<li><strong>启用持久化 (指定-artifacts)</strong> </li>
</ul>
<p>t.ArtifactDir()将返回一个位于-outputdir（默认为当前工作目录）下的特定目录，该目录及其内容在测试结束后<strong>不会</strong>被删除。</p>
<h3>3.2 目录结构与输出</h3>
<p>为了确保唯一性，尤其是在运行多个包（例如使用“./&#8230;”）或使用-count=N时，构件目录的路径结构经过了仔细考虑。最终采用的结构类似：</p>
<pre><code>&lt;outputdir&gt;/&lt;package_path&gt;/&lt;test_name&gt;/&lt;random_or_counter&gt;
</code></pre>
<p>具体的路径转换和命名规则会进行必要的处理（如路径安全化、截断长名称等），但核心目标是提供一个可预测且唯一的存储位置。</p>
<p>当启用构件存储且测试首次调用ArtifactDir() 时，会输出类似信息：</p>
<pre><code>=== ARTIFACTS TestName/subtest_name /path/to/actual/artifact/dir
</code></pre>
<p>在go test -json模式下，对应事件为：</p>
<pre><code>{"Time":"...", "Action":"artifacts", "Package":"package/path", "Test":"TestName/subtest_name", "Path":"/path/to/actual/artifact/dir"}
</code></pre>
<p>其中Path字段包含了实际的构件目录路径。</p>
<p>综上，有了t.ArtifactDir()后，在调试失败的测试时，用户可以轻松检查测试生成的实际输出文件、对比文件、日志、核心 dump、网络抓包和性能剖析数据，而无需修改测试代码以阻止临时目录清理。此外，CI系统可以通过设置-artifacts和-outputdir标志，自动收集所有测试产生的构件，并将其存档或用于后续分析。在测试代码生成时，生成的代码可以输出到t.ArtifactDir()返回的目录，方便在验证失败时与预期的黄金文件进行对比。这种方法提供了一种官方推荐的方式来处理测试产物，减少了各个项目自行实现此类机制的需求。</p>
<h2>4. 协同效应：属性与构件的强强联合</h2>
<p>t.Attr和t.ArtifactDir这两个提案并非孤立存在，它们可以协同工作，提供更强大的测试信息管理能力。</p>
<p>最典型的场景是：使用t.ArtifactDir管理构件文件的存储，并使用t.Attr记录指向这些构件的元数据。</p>
<p>例如，一个测试可能会：</p>
<ul>
<li>调用dir := t.ArtifactDir()获取构件目录。</li>
<li>在该目录中生成一个重要的日志文件，假设名为trace.log。</li>
<li>调用t.Attr(“trace_log_path”, filepath.Join(dir, “trace.log”))来记录这个日志文件的确切路径。</li>
<li>或者，如果CI系统会将构件上传到对象存储，测试可以记录其访问URL：t.Attr(“trace_log_url”, “s3://bucket/&#8230;”)。</li>
</ul>
<p>这样，外部工具不仅知道测试产生了构件（通过Action: artifacts事件），还能通过解析Action: attr事件找到访问或描述这些构件的具体信息，实现了端到端的关联。</p>
<h2>5. 小结</h2>
<p>t.Attr和t.ArtifactDir的引入，标志着Go标准测试库在满足现代软件开发流程需求方面迈出了重要一步。它们通过提供标准化的API和工具链支持，极大地增强了测试的透明度、可调试性以及与自动化系统的集成深度。</p>
<p>随着这两个提案的落地（预计在未来的Go版本中），我们期待看到Go社区能够更轻松地构建健壮、可观测的测试体系，并与各种先进的开发运维工具无缝集成。这无疑将进一步巩固Go在构建可靠、高效软件系统方面的优势。开发者应密切关注这些新特性，并考虑如何在自己的项目中利用它们来改进测试实践。</p>
<h2>6. 参考资料</h2>
<ul>
<li><a href="https://github.com/golang/go/issues/43936">testing: structured output for test attributes</a> &#8211; https://github.com/golang/go/issues/43936</li>
<li><a href="https://github.com/golang/go/issues/71287">proposal: testing: store test artifacts</a> &#8211; https://github.com/golang/go/issues/71287</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
