<?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; CMake</title>
	<atom:link href="http://tonybai.com/tag/cmake/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 08 Jun 2026 23:32:23 +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>百万行依赖的“恐惧”：一位Rust开发者的深度反思与Go的启示</title>
		<link>https://tonybai.com/2025/05/10/rust-dependencies-scare-me/</link>
		<comments>https://tonybai.com/2025/05/10/rust-dependencies-scare-me/#comments</comments>
		<pubDate>Sat, 10 May 2025 01:36:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[axum]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[Coding-Rules]]></category>
		<category><![CDATA[crates.io]]></category>
		<category><![CDATA[dotenv]]></category>
		<category><![CDATA[dotenvy]]></category>
		<category><![CDATA[features]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[HackerNews]]></category>
		<category><![CDATA[Macbook]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[rustc]]></category>
		<category><![CDATA[SBOM]]></category>
		<category><![CDATA[tokei]]></category>
		<category><![CDATA[tokio]]></category>
		<category><![CDATA[vendor]]></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=4672</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/10/rust-dependencies-scare-me 大家好，我是Tony Bai。 在现代软件开发中，高效的包管理系统和繁荣的开源生态极大地加速了我们的开发进程。Rust语言的Cargo及其crates.io生态便是其中的佼佼者，为开发者带来了前所未有的便捷。然而，这种便捷性是否也伴随着一些潜在的“代价”？ 近期，一位名叫Vincent的国外Rust开发者在其博客文章《Rust Dependencies scare Me》中，就真诚地抒发了他对Rust依赖管理的深切忧虑。这篇博文在Hacker News等社区引发了热烈讨论，其指出的问题——从依赖的维护性到惊人的代码体积——或许也值得我们每一位使用现代包管理系统的开发者深思。 今天，我们就来一起解读Vincent的这篇文章，看看他遇到了哪些具体问题，并结合社区的智慧与我们的经验，探讨这些现象背后的启示。 Cargo的魅力：作者眼中的“美好一面” 在这位开发者看来，Cargo无疑是Rust生态的巨大优势。他强调，Cargo极大地提升了生产力，开发者无需像使用CMake(多用于C++项目)那样手动管理和链接文件。这使得在不同架构和操作系统（如他的M1 MacBook和Debian桌面）之间切换变得异常顺畅。 他坦言，在大部分情况下，Cargo让他几乎可以不必过多思考包管理本身，从而能更专注于核心代码的编写。这种“无感”的便捷体验，与上世纪80年代开发者需要为节省软盘空间而精打细算地“手动挑选和集成库代码”形成了鲜明对比，无疑是现代包管理系统追求的目标，也是Rust吸引开发者的重要原因之一。 当便捷遭遇“意外”：dotenv引发的警惕 然而，文章作者也指出，正是这种“不用思考”的便捷，可能让人变得“草率”。 他在一个生产项目中使用了许多Rust开发者都用过的dotenv库（用于加载.env文件）。项目平稳运行数周后，他偶然发现一则Rust安全通告指出，他所使用的dotenv版本已无人维护，并推荐了替代方案dotenvy。 这个小插曲让他开始反思：这个依赖真的必不可少吗？他尝试后发现，仅仅35行代码便实现了他所需的核心功能。他由此提出一个普遍性的问题：当依赖项（尤其是那些看似“微不足道”的）不再维护或出现安全漏洞时，我们该如何应对？那些我们真正“需要”的复杂依赖，又隐藏着哪些风险？这不仅仅是功能问题，更关乎依赖的信任链和维护者的责任。 百万行代码的“冲击波”：一个“小项目”的真实体积 Vincent的忧虑不止于此。他以一个自认为“微不足道”的Web服务项目为例——该项目使用广受好评的异步运行时tokio和Web框架axum，主要功能是处理请求、解压文件和记录日志。 当他尝试使用cargo vendor将所有依赖项本地化时，并用代码行数统计工具tokei进行分析，结果令他大吃一惊：总代码行数高达360万行！而他自己编写的业务代码仅有约1000行。 他将此与Linux内核的2780万行代码进行对比，发现他这个“小项目”的依赖代码量已接近后者的七分之一。他不禁发问：如何审计如此庞大的代码量？我们引入的重量级依赖，其绝大部分功能是否是我们项目真正需要的？ Vincent的经历并非个案。Hacker News社区的讨论中，有开发者（如kion）指出，现代软件开发中‘库叠库’的现象十分普遍，每一层依赖可能只用到其功能的冰山一角，但最终却可能导致简单的应用膨胀到数百MB。更有甚者（如jiggawatts）通过计算发现，仅三层依赖的层层叠加，就可能导致最终应用中88%的代码是“死代码”或从未被真实业务逻辑触及的“幽灵代码”。 Rust依赖困境的“求解”：作者的困惑与社区的多元声音 面对如此庞大的依赖代码和潜在风险，该博主坦诚自己“没有答案”。他提及了社区中一些常见的讨论方向，例如扩展标准库的利弊、开发者自身的责任以及业界大厂的实践等。 Hacker News社区的讨论进一步丰富了这些思考： 编译时优化是否足够？ 许多评论提到了链接时优化（LTO）、Tree Shaking等技术在剔除未使用代码方面的作用。Rust基于LLVM的优化确实能在这方面做出贡献。然而，正如一些评论者指出的，这些优化并非“银弹”，对于动态分发或包含大量可选编译特性的复杂依赖，完美剥离未使用部分仍充满挑战。 更细粒度的依赖控制： Rust的features机制为选择性编译提供了可能，但社区也在探索更根本的解决方案。有开发者甚至提出了“超细粒度符号和依赖”的设想，即每个语言构造都声明其精确依赖，按需构建最小代码集，尽管这在实现上极具颠覆性。 工具链的局限与期望： Vincent指出Cargo目前难以精确追踪最终编译产物包含的代码。社区也期待更强大的工具来分析依赖树、识别冗余、评估安全风险。 最终，文章作者将问题抛给了社区：我们应该怎么办？ 我们的启示：从Rust的“依赖之忧”看现代软件供应链 Vincent的博文真实地反映了现代软件开发中普遍存在的“依赖困境”——我们享受着开源生态带来的便利，但也面临着供应链安全、代码膨胀、维护性等一系列挑战。 从他的分享和社区的热烈讨论中，我们可以得到以下几点启示： 审慎评估依赖，警惕“依赖膨胀”的陷阱，拥抱适度“复制”： “不要为了碟醋包饺子”。在引入任何依赖前，都应评估其必要性、维护状态、社区活跃度以及潜在的安全风险。正如Go社区所倡导的“A little copying is better than a little dependency. (一点复制代码胜过一点点依赖)”，有时为了避免引入一个庞大或不稳定的依赖，适度复制代码，或者自己实现一个轻量级的核心功能，可能是更明智的选择。Go语言设计者之一的 Rob Pike [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/rust-dependencies-scare-me-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/10/rust-dependencies-scare-me">本文永久链接</a> &#8211; https://tonybai.com/2025/05/10/rust-dependencies-scare-me</p>
<p>大家好，我是Tony Bai。</p>
<p>在现代软件开发中，高效的包管理系统和繁荣的开源生态极大地加速了我们的开发进程。Rust语言的Cargo及其crates.io生态便是其中的佼佼者，为开发者带来了前所未有的便捷。然而，这种便捷性是否也伴随着一些潜在的“代价”？</p>
<p>近期，一位名叫Vincent的国外Rust开发者在其博客文章《Rust Dependencies scare Me》中，就真诚地抒发了他对Rust依赖管理的深切忧虑。<strong>这篇博文</strong>在Hacker News等社区引发了热烈讨论，其指出的问题——从依赖的维护性到惊人的代码体积——或许也值得我们每一位使用现代包管理系统的开发者深思。</p>
<p>今天，我们就来一起解读Vincent的这篇文章，看看他遇到了哪些具体问题，并结合社区的智慧与我们的经验，探讨这些现象背后的启示。</p>
<h2>Cargo的魅力：作者眼中的“美好一面”</h2>
<p>在这位开发者看来，Cargo无疑是Rust生态的巨大优势。他强调，Cargo极大地提升了生产力，开发者无需像使用CMake(多用于C++项目)那样手动管理和链接文件。这使得在不同架构和操作系统（如他的M1 MacBook和Debian桌面）之间切换变得异常顺畅。</p>
<p>他坦言，在大部分情况下，Cargo让他几乎可以不必过多思考包管理本身，从而能更专注于核心代码的编写。这种“无感”的便捷体验，与上世纪80年代开发者需要为节省软盘空间而精打细算地“手动挑选和集成库代码”形成了鲜明对比，无疑是现代包管理系统追求的目标，也是Rust吸引开发者的重要原因之一。</p>
<h2>当便捷遭遇“意外”：dotenv引发的警惕</h2>
<p>然而，文章作者也指出，正是这种“不用思考”的便捷，可能让人变得“草率”。</p>
<p>他在一个生产项目中使用了许多Rust开发者都用过的dotenv库（用于加载.env文件）。项目平稳运行数周后，他偶然发现一则Rust安全通告指出，他所使用的dotenv版本已无人维护，并推荐了替代方案dotenvy。</p>
<p>这个小插曲让他开始反思：这个依赖真的必不可少吗？他尝试后发现，仅仅35行代码便实现了他所需的核心功能。他由此提出一个普遍性的问题：当依赖项（尤其是那些看似“微不足道”的）不再维护或出现安全漏洞时，我们该如何应对？那些我们真正“需要”的复杂依赖，又隐藏着哪些风险？这不仅仅是功能问题，更关乎依赖的信任链和维护者的责任。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/rust-dependencies-scare-me-2.png" alt="" /></p>
<h2>百万行代码的“冲击波”：一个“小项目”的真实体积</h2>
<p>Vincent的忧虑不止于此。他以一个自认为“微不足道”的Web服务项目为例——该项目使用广受好评的异步运行时tokio和Web框架axum，主要功能是处理请求、解压文件和记录日志。</p>
<p>当他尝试使用cargo vendor将所有依赖项本地化时，并用代码行数统计工具tokei进行分析，结果令他大吃一惊：<strong>总代码行数高达360万行</strong>！而他自己编写的业务代码仅有约1000行。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/rust-dependencies-scare-me-3.png" alt="" /></p>
<p>他将此与Linux内核的2780万行代码进行对比，发现他这个“小项目”的依赖代码量已接近后者的七分之一。他不禁发问：如何审计如此庞大的代码量？我们引入的重量级依赖，其绝大部分功能是否是我们项目真正需要的？</p>
<p>Vincent的经历并非个案。Hacker News社区的讨论中，有开发者（如kion）指出，现代软件开发中‘库叠库’的现象十分普遍，每一层依赖可能只用到其功能的冰山一角，但最终却可能导致简单的应用膨胀到数百MB。更有甚者（如jiggawatts）通过计算发现，仅三层依赖的层层叠加，就可能导致最终应用中88%的代码是“死代码”或从未被真实业务逻辑触及的“幽灵代码”。</p>
<h2>Rust依赖困境的“求解”：作者的困惑与社区的多元声音</h2>
<p>面对如此庞大的依赖代码和潜在风险，该博主坦诚自己“没有答案”。他提及了社区中一些常见的讨论方向，例如扩展标准库的利弊、开发者自身的责任以及业界大厂的实践等。</p>
<p>Hacker News社区的讨论进一步丰富了这些思考：</p>
<ul>
<li><strong>编译时优化是否足够？</strong> 许多评论提到了链接时优化（LTO）、Tree Shaking等技术在剔除未使用代码方面的作用。Rust基于LLVM的优化确实能在这方面做出贡献。然而，正如一些评论者指出的，这些优化并非“银弹”，对于动态分发或包含大量可选编译特性的复杂依赖，完美剥离未使用部分仍充满挑战。</li>
<li><strong>更细粒度的依赖控制：</strong> Rust的features机制为选择性编译提供了可能，但社区也在探索更根本的解决方案。有开发者甚至提出了“超细粒度符号和依赖”的设想，即每个语言构造都声明其精确依赖，按需构建最小代码集，尽管这在实现上极具颠覆性。</li>
<li><strong>工具链的局限与期望：</strong> Vincent指出Cargo目前难以精确追踪最终编译产物包含的代码。社区也期待更强大的工具来分析依赖树、识别冗余、评估安全风险。</li>
</ul>
<p>最终，文章作者将问题抛给了社区：我们应该怎么办？</p>
<h2>我们的启示：从Rust的“依赖之忧”看现代软件供应链</h2>
<p>Vincent的博文真实地反映了现代软件开发中普遍存在的“依赖困境”——我们享受着开源生态带来的便利，但也面临着供应链安全、代码膨胀、维护性等一系列挑战。</p>
<p>从他的分享和社区的热烈讨论中，我们可以得到以下几点启示：</p>
<ol>
<li>
<p><strong>审慎评估依赖，警惕“依赖膨胀”的陷阱，拥抱适度“复制”：</strong> “不要为了碟醋包饺子”。在引入任何依赖前，都应评估其必要性、维护状态、社区活跃度以及潜在的安全风险。正如Go社区所倡导的“<strong>A little copying is better than a little dependency.</strong> (一点复制代码胜过一点点依赖)”，有时为了避免引入一个庞大或不稳定的依赖，适度复制代码，或者自己实现一个轻量级的核心功能，可能是更明智的选择。Go语言设计者之一的 <strong>Rob Pike 在其著名的演讲<a href="https://mp.weixin.qq.com/s/4B5TDVhLfs1bYk343UfQxQ">《On Bloat》</a>中也曾深刻地警示过软件膨胀的危害，其中就包括了因过度或不必要依赖导致的复杂性增加和性能下降。Pike强调，真正的简洁和高效往往来自于对问题本质的深刻理解和对引入外部因素的克制。</strong></p>
</li>
<li>
<p><strong>理解依赖的“冰山效应”与供应链安全——真实的威胁就在身边：</strong> 一个看似简单的库，背后可能隐藏着庞大的间接依赖。我们需要关注整个依赖树的健康状况。更重要的是，正如Hacker News上一些开发者强调的，依赖的真正“恐惧”更多在于供应链安全和代码的可审查性。当我们的项目依赖数百万行来自互联网的未知代码时，如何确保没有恶意代码或严重漏洞被悄然引入？这绝非危言耸听！就在最近，Socket威胁研究团队便披露了三个恶意的Go模块 (github.com/truthfulpharm/prototransform, github.com/blankloggia/go-mcp, github.com/steelpoor/tlsproxy)。这些模块通过命名空间混淆或伪装诱导开发者引入，其内部包含高度混淆的恶意代码，在特定条件（目前主要针对Linux系统）下会下载并执行毁灭性的“磁盘擦除”脚本 (done.sh)，直接向主磁盘写入零，导致数据被完全清零且无法恢复！这个案例血淋淋地提醒我们，供应链安全是每一个开发者都必须严肃对待的现实威胁。 这需要我们对信任链和维护者责任有更清醒的认识。</p>
</li>
<li>
<p><strong>寻求更精细的控制与工具支持：</strong> 无论是语言特性（如Go的build tags、Rust的features）、包管理工具（如更智能的tree shaking），还是库本身的模块化设计，都应朝着让开发者能更精细控制最终产物的方向努力。同时，自动化工具在依赖分析、漏洞扫描、许可证合规等方面扮演着越来越重要的角色。</p>
</li>
<li>
<p><strong>标准库与生态的平衡：</strong> Go语言的“大标准库”策略在一定程度上缓解了对外部依赖的过度渴求，但也带来了标准库自身迭代和灵活性的挑战。Rust选择了更小的标准库和更繁荣的社区生态。Hacker News上的讨论也反映了这种分歧：一部分开发者期望Rust能拥有更丰富的标准库，以减少对外部“寻寻觅觅”的困扰；而另一部分则担心这会扼杀生态活力，导致标准库“僵化”。这两种模式各有其历史成因和现实取舍，值得我们持续观察和学习，或许未来会出现一种更优的“官方认证扩展库”或“元库”的形态。</p>
</li>
</ol>
<h2>讨论：你如何看待现代软件的“依赖管理”？</h2>
<p><strong>这篇文章所转述的思考与社区的热议</strong>无疑为我们敲响了警钟。<strong>你在日常开发中（无论是Rust、Go还是其他语言），是否也曾遇到过类似的依赖管理难题？你认为当前包管理生态面临的最大挑战是什么？又有哪些值得推广的最佳实践或工具？</strong></p>
<p>非常欢迎在评论区留下你的宝贵见解和经验分享！</p>
<ul>
<li>原文链接：https://vincents.dev/blog/rust-dependencies-scare-me</li>
<li>Socket.dev发现恶意Go模块：https://socket.dev/blog/wget-to-wipeout-malicious-go-modules-fetch-destructive-payload</li>
</ul>
<hr />
<p><strong>面对复杂的依赖与潜藏的风险，如何系统性提升你的Go安全意识与底层掌控力？</strong></p>
<p>近期Go恶意模块的“磁盘擦除”事件，再次凸显了深入理解依赖、掌握底层机制、构建安全软件的重要性。如果你渴望系统性地学习Go语言的深层原理（包括编译、链接、运行时），提升对第三方库的辨别与审计能力，并在实践中规避类似的安全“大坑”…</p>
<p>那么，我的 「Go &amp; AI 精进营」知识星球 将是你不可或缺的伙伴！这里不仅有【Go原理课】、【Go进阶课】、【Go避坑课】助你洞悉语言本质，更有针对性的安全实践讨论和案例分析。我会亲自为你解答各种疑难问题，你还可以与众多对技术安全与底层有追求的Gopher们一同交流，共同构建更安全的Go生态。</p>
<p>立即扫码加入，为你的技术栈装上“安全防火墙”，在复杂的软件世界中行稳致远！<br />
<img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></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/10/rust-dependencies-scare-me/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Gopher的Rust第一课：Rust代码组织</title>
		<link>https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code/</link>
		<comments>https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code/#comments</comments>
		<pubDate>Thu, 06 Jun 2024 15:13:51 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bazel]]></category>
		<category><![CDATA[binary]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[Cargo.lock]]></category>
		<category><![CDATA[Cargo.toml]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[crate]]></category>
		<category><![CDATA[DataFusion]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[lib.rs]]></category>
		<category><![CDATA[library]]></category>
		<category><![CDATA[main]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[repo]]></category>
		<category><![CDATA[rlib]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[rustc]]></category>
		<category><![CDATA[self]]></category>
		<category><![CDATA[Thread]]></category>
		<category><![CDATA[tokio]]></category>
		<category><![CDATA[workspace]]></category>
		<category><![CDATA[编译器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4187</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code 在上一章的讲解中，我们编写了第一个Rust示例程序”hello, world”，并给出了rustc版和cargo版本。在真实开发中，我们都会使用cargo来创建和管理Rust包。不过，Hello, world示例非常简单，仅仅由一个Rust源码文件组成，而且所有源码文件都在同一个目录中。但真实世界中的实用Rust程序，无论是公司商业项目，还是一些知名的开源项目，甚至是一些稍复杂一些的供教学使用的示例程序，它们通常可不会这么简单，都有着复杂的代码结构。 Rust初学者在阅读这些项目源码时便仿佛进入了迷宫，不知道该走哪条（阅读代码的）路径，不知道每个目录代表的含义，也不知道自己想看的源码究竟在哪个目录下。但目前市面上的Rust入门教程大多没有重视初学者的这一问题，要么没有对Rust项目代码组织结构进行针对性的讲解，要么是将讲解放到书籍的后面章节。 根据我个人的学习经验来看，理解一个实用Rust项目的代码组织结构越早，对后续的Rust学习越有益处。同时，掌握Rust项目的代码组织结构也是Rust开发者走向编写复杂Rust程序的必经的一步。并且，初学者在了解项目的代码组织结构后，便可以自主阅读一些复杂的Rust项目的源码，可提高Rust学习的效率，提升学习效果。因此，我决定在介绍Rust基础语法之前先在本章中系统地介绍Rust的代码组织结构，以满足很多Rust初学者的述求。 但在介绍Rust代码组织结构之前，我们需要先来系统说明一下Rust代码组织结构中的几个重要概念，它们是了解Rust项目代码组织结构的前提。 4.1 回顾Go代码组织 Go项目代码组织由module和package两级组成。通常来说，每个Go repo就是一个module，由repo根目录下的go.mod定义，go.mod文件所在目录也被称为module root。go.mod中典型内容如下： // go.mod module github.com/user/mymodule[/vN] go 1.22.1 ... ... go.mod中的module directive一行后面的github.com/user/mymodule/[vN]是module path。module path一来可以反映该module的具体网络位置，同时也是该module下面的Go package导入(import)路径的组成部分。module root下的子目录中通常存放着该module下面的Go package，比如module root/foo目录下存放的Go包的导入路径为github.com/user/mymodule[/vN]/foo。 Go package是Go的编译单元，也是功能单元，代码内外部导入和引用的单位也都是包。而go module是后加入的，更多用于管理包的版本（一个module下的所有包都统一进行版本管理）以及构建时第三方依赖和版本的管理。 更多关于Go module和package管理以及Go项目布局的内容，可以详见我的极客时间《Go语言第一课》专栏。 个人认为Go的module和package的两级管理还是很好理解和管理的，在这方面Rust的代码组织形式又是怎样的呢？接下来，我们就来正式看看Rust的代码组织。 4.2 rustc-only的Rust项目 Rust是系统编程语言，这让我想起了当初在Go成为我个人主力语言之前使用C/C++进行开发的岁月。C/C++是没有像go或Rust的cargo那样的统一的包依赖管理器和项目构建管理工具的。编译器(如gcc等)是核心工具，而项目构建管理则经常由其他工具负责，如Makefile、CMake，或者是Google的Bazel等。在Windows上开发应用的，则往往使用微软或其他开发者工具公司提供的IDE，如当年炙手可热的Visual Studio系列。 下面表格展示了各语言的编译器/链接器和构建管理工具的关系： 像cargo、go这样的“一站式”工具链都旨在为开发者提供体验更为友好的交互接口的，在幕后，它们仍然依赖于底层的编译器和链接器（如rustc和go tool compile/link）来执行实际的代码编译。 不过，像cargo这样的高级工具也给开发人员带来了额外的抽象，或是叫“掩盖”了一些真相，这有时候让人看不清构建过程的本质，比如：很多Gopher用了很多年Go，但却不知道go tool compile/link的存在。 本着只有in hard way，才能看到和抓住本质的思路，以及之前学习用系统编程语言C/C++时经验，这里我们先来看一些rustc-only的Rust项目。Rustc-only的Rust项目是指不使用Cargo创建和管理的Rust项目，而是直接使用rustc编译器来编译和构建项目。这意味着开发者需要编写自己的构建脚本，例如使用Makefile或其他构建工具来管理项目的构建过程。 不过，请注意：这类项目极少用于生产，即便是那些不需要复杂的依赖管理的小型项目。这里使用rustc-only的Rust项目仅仅是为了学习和了解Rustc编译器的主要功能机制以及Rust语言在代码组织上的一些抽象，比如module等。 下面我们就从最简单的rustc-only项目开始，先来看看只有一个Rust源文件且无其他依赖项的“最简项目”。 4.2.1 单文件项目 所谓单文件项目，即只有一个Rust源文件，例如前面章节中的hello_world.rs，这种项目可以直接使用rustc编译器来编译和运行： [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/gopher-rust-first-lesson-organizing-rust-code-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code">本文永久链接</a> &#8211; https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code</p>
<p>在上一章的讲解中，我们编写了<a href="https://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program">第一个Rust示例程序”hello, world”</a>，并给出了rustc版和cargo版本。在真实开发中，我们都会使用cargo来创建和管理Rust包。不过，Hello, world示例非常简单，仅仅由一个Rust源码文件组成，而且所有源码文件都在同一个目录中。但真实世界中的实用Rust程序，无论是公司商业项目，还是一些知名的开源项目，甚至是一些稍复杂一些的供教学使用的示例程序，它们通常可不会这么简单，都有着复杂的代码结构。</p>
<p>Rust初学者在阅读这些项目源码时便仿佛进入了迷宫，不知道该走哪条（阅读代码的）路径，不知道每个目录代表的含义，也不知道自己想看的源码究竟在哪个目录下。但目前市面上的Rust入门教程大多没有重视初学者的这一问题，要么没有对Rust项目代码组织结构进行针对性的讲解，要么是将讲解放到书籍的后面章节。</p>
<p>根据我个人的学习经验来看，理解一个实用Rust项目的代码组织结构越早，对后续的Rust学习越有益处。同时，掌握Rust项目的代码组织结构也是Rust开发者走向编写复杂Rust程序的必经的一步。并且，初学者在了解项目的代码组织结构后，便可以自主阅读一些复杂的Rust项目的源码，可提高Rust学习的效率，提升学习效果。因此，我决定在介绍Rust基础语法之前先在本章中系统地介绍Rust的代码组织结构，以满足很多Rust初学者的述求。</p>
<p>但在介绍Rust代码组织结构之前，我们需要先来系统说明一下Rust代码组织结构中的几个重要概念，它们是了解Rust项目代码组织结构的前提。</p>
<h2>4.1 回顾Go代码组织</h2>
<p>Go项目代码组织由module和package两级组成。通常来说，每个Go repo就是一个module，由repo根目录下的go.mod定义，go.mod文件所在目录也被称为module root。go.mod中典型内容如下：</p>
<pre><code>// go.mod
module github.com/user/mymodule[/vN]

go 1.22.1

... ...
</code></pre>
<p>go.mod中的module directive一行后面的github.com/user/mymodule/[vN]是module path。module path一来可以反映该module的具体网络位置，同时也是该module下面的Go package导入(import)路径的组成部分。module root下的子目录中通常存放着该module下面的Go package，比如module root/foo目录下存放的Go包的导入路径为github.com/user/mymodule[/vN]/foo。</p>
<p>Go package是Go的编译单元，也是功能单元，代码内外部导入和引用的单位也都是包。而go module是后加入的，更多用于管理包的版本（一个module下的所有包都统一进行版本管理）以及构建时第三方依赖和版本的管理。</p>
<blockquote>
<p>更多关于Go module和package管理以及Go项目布局的内容，可以详见我的极客时间<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>专栏。</p>
</blockquote>
<p>个人认为Go的module和package的两级管理还是很好理解和管理的，在这方面Rust的代码组织形式又是怎样的呢？接下来，我们就来正式看看Rust的代码组织。</p>
<h2>4.2 rustc-only的Rust项目</h2>
<p>Rust是系统编程语言，这让我想起了当初在Go成为我个人主力语言之前使用C/C++进行开发的岁月。C/C++是没有像go或Rust的cargo那样的统一的包依赖管理器和项目构建管理工具的。编译器(如gcc等)是核心工具，而项目构建管理则经常由其他工具负责，如Makefile、CMake，或者是Google的<a href="https://github.com/bazelbuild/bazel">Bazel</a>等。在Windows上开发应用的，则往往使用微软或其他开发者工具公司提供的IDE，如当年炙手可热的Visual Studio系列。</p>
<p>下面表格展示了各语言的编译器/链接器和构建管理工具的关系：</p>
<p><img src="https://tonybai.com/wp-content/uploads/gopher-rust-first-lesson-organizing-rust-code-2.png" alt="" /></p>
<p>像cargo、go这样的“一站式”工具链都旨在为开发者提供体验更为友好的交互接口的，在幕后，它们仍然依赖于底层的编译器和链接器（如rustc和go tool compile/link）来执行实际的代码编译。</p>
<p>不过，像cargo这样的高级工具也给开发人员带来了额外的抽象，或是叫“掩盖”了一些真相，这有时候让人看不清构建过程的本质，比如：很多Gopher用了很多年Go，但却不知道go tool compile/link的存在。</p>
<p>本着只有in hard way，才能看到和抓住本质的思路，以及之前学习用系统编程语言C/C++时经验，这里我们先来看一些<strong>rustc-only的Rust项目</strong>。Rustc-only的Rust项目是指不使用Cargo创建和管理的Rust项目，而是直接使用rustc编译器来编译和构建项目。这意味着开发者需要编写自己的构建脚本，例如使用Makefile或其他构建工具来管理项目的构建过程。</p>
<p>不过，请注意：<strong>这类项目极少用于生产</strong>，即便是那些不需要复杂的依赖管理的小型项目。这里使用rustc-only的Rust项目仅仅是为了学习和了解Rustc编译器的主要功能机制以及Rust语言在代码组织上的一些抽象，比如module等。</p>
<p>下面我们就从最简单的rustc-only项目开始，先来看看只有一个Rust源文件且无其他依赖项的“最简项目”。</p>
<h3>4.2.1 单文件项目</h3>
<p>所谓单文件项目，即只有一个Rust源文件，例如前面章节中的hello_world.rs，这种项目可以直接使用rustc编译器来编译和运行：</p>
<pre><code>// rust-guide-for-gopher/organizing-rust-code/rustc-only/single/hello-world/hello_world.rs
fn main() {
    println!("Hello, world!");
}
</code></pre>
<p>对于顶层带有main函数的源文件，rustc会默认将其视为binary crate类型的源文件，并将其编译为可执行二进制文件hello_world。</p>
<p>我们当然也可以强制的让rustc将该源文件视为library crate类型的源文件，并将其编译为其他类型的crate输出文件，rustc支持多种crate type：</p>
<pre><code>      --crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]
                        Comma separated list of types of crates
                        for the compiler to emit
</code></pre>
<p>在<a href="https://doc.rust-lang.org/rustc/what-is-rustc.html">rustc的文档</a>中，各种crate类型的含义如下：</p>
<pre><code>lib — Generates a library kind preferred by the compiler, currently defaults to rlib.
rlib — A Rust static library.
staticlib — A native static library.
dylib — A Rust dynamic library.
cdylib — A native dynamic library.
bin — A runnable executable program.
proc-macro — Generates a format suitable for a procedural macro library that may be loaded by the compiler.
</code></pre>
<p>不过，如果强制将带有顶层main函数的rust源文件视为lib crate型的，那么rustc将会报warning，提醒你函数main将是死代码，永远不会被用到：</p>
<pre><code>$rustc --crate-type lib hello_world.rs
warning: function `main` is never used
 --&gt; hello_world.rs:1:4
  |
1 | fn main() {
  |    ^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: 1 warning emitted
</code></pre>
<p>但即便如此，一个名为libhello_world.rlib的文件依然会被rustc生成出来！（目前&#8211;crate-type lib等同于&#8211;create-type rlib)。</p>
<h3>4.2.2 有外部依赖项的单文件项目</h3>
<p>日常开发中，像上面的Hello, World级别的trivial应用是极其少见的，一个non-trivial的Rust应用或多或少都会有一些依赖。这里我们也来看一下如何基于rustc来构建带有外部依赖的单文件项目。下面是一个带有外部依赖的示例：</p>
<pre><code>// organizing-rust-code/rustc-only/single/hello-world-with-deps/hello_world.rs
extern crate rand;

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let num: u32 = rng.gen();
    println!("Random number: {}", num);
}
</code></pre>
<p>这个示例程序依赖一个名为rand的crate，要编译该程序，我们必须先手动下载rand的crate源码，并在本地将rand源码编译为示例程序所需的rust library。下面步骤展示了如何下载和构建rand crate：</p>
<pre><code>$curl -LO https://crates.io/api/v1/crates/rand/0.8.5/download
$tar -xvf download
</code></pre>
<p>解压后，我们将看到rand-0.8.5这样的一个crate目录，进入该目录，我们执行cargo build来构建rand crate：</p>
<pre><code>$cd rand-0.8.5
$cargo build
... ...
   Finished dev [unoptimized + debuginfo] target(s) in 0.19s
</code></pre>
<p>cargo构建出的librand.rlib就在rand-0.8.5/target/debug下。</p>
<blockquote>
<p>注：rlib的命名方式：lib+{crate_name}.rlib</p>
</blockquote>
<p>接下来，我们就来构建一下依赖rand crate的hello_world.rs：</p>
<pre><code>// 在organizing-rust-code/rustc-only/single/hello-world-with-deps下面执行

$rustc --verbose  -L ./rand-0.8.5/target/debug  --extern rand=librand.rlib hello_world.rs
error[E0463]: can't find crate for `rand_core` which `rand` depends on
 --&gt; hello_world.rs:1:1
  |
1 | extern crate rand;
  | ^^^^^^^^^^^^^^^^^^ can't find crate

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0463`.
</code></pre>
<p>我们看到rustc的编译错误提示：无法找到rand crate依赖的rand_core crate！也就是说我们除了向rustc提供hello_world.rs依赖的rand crate之外，还要向rustc提供rand crate的各种依赖！</p>
<p>rand crate的各种依赖在哪里呢？我们在构建rand crate时，cargo build将各种依赖都放在了rand-0.8.5/target/debug/deps目录下了：</p>
<pre><code>$ls -l|grep ".rlib"
-rw-r--r--   1 tonybai  staff     6896  4 29 06:45 libcfg_if-cd6bebf18fb9c234.rlib
-rw-r--r--   1 tonybai  staff   204072  4 29 06:45 libgetrandom-df6a8e95e188fc56.rlib
-rw-r--r--   1 tonybai  staff  1651320  4 29 06:45 liblibc-f16531562d07b476.rlib
-rw-r--r--   1 tonybai  staff   959408  4 29 06:45 libppv_lite86-f1d97d485bc43617.rlib
-rw-r--r--   1 tonybai  staff  1784376  4 29 06:45 librand-9a91ea8db926e840.rlib
-rw-r--r--   1 tonybai  staff   987936  4 29 06:45 librand_chacha-6fe22bd8b3bb228c.rlib
-rw-r--r--   1 tonybai  staff   256768  4 29 06:45 librand_core-fc905f6ca5f8533b.rlib
</code></pre>
<p>我们看到其中还包含了librand自身：librand-9a91ea8db926e840.rlib。我们来试试基于deps目录下的这些依赖rlib编译一下：</p>
<pre><code>$rustc --verbose  --extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib -L rand-0.8.5/target/debug/deps  --extern rand_core=librand_core-fc905f6ca5f8533b.rlib --extern getrandom=libgetrandom-df6a8e95e188fc56.rlib --extern cfg_if=libcfg_if-cd6bebf18fb9c234.rlib --extern libc=liblibc-f16531562d07b476.rlib --extern rand_chacha=librand_chacha-6fe22bd8b3bb228c.rlib --extern ppv_lite86=libppv_lite86-f1d97d485bc43617.rlib  hello_world.rs
</code></pre>
<p>我们用rustc成功编译了带有外部依赖的Rust源码。不过这里要注意的是rustc对直接依赖和间接依赖的crate的定位方式有所不同。</p>
<p>对于直接依赖的crate，比如这里的rand crate，我们需要给出具体路径，它不依赖-L的位置指示，所以这里我们使用了&#8211;extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib。</p>
<p>对于间接依赖的crate，比如rand crate依赖的rand_core，rust会结合-L指示的位置以及&#8211;extern一起来定位，这里-L指示路径为rand-0.8.5/target/debug/deps，&#8211;extern rand_core=librand_core-fc905f6ca5f8533b.rlib，那么rustc就会在rand-0.8.5/target/debug/deps下面搜索librand_core-fc905f6ca5f8533b.rlib是否存在。</p>
<p>我们运行rustc构建出的可执行文件，输出如下：</p>
<pre><code>$./hello_world
Random number: 431751199
</code></pre>
<h3>4.2.3 有外部依赖的多文件项目</h3>
<p>在Go中，如果某个目录下有多个源文件，那么通常这几个源文件均归属于同一个Go包(可能的例外的是&#42;_test.go文件的包名)。但在Rust中，情况就会变得复杂了一些，我们来看一个例子：</p>
<pre><code>// organizing-rust-code/rustc-only/multi/multi-file-with-deps

$tree -F -L 2
.
├── main.rs
├── sub1/
│   ├── bar.rs
│   ├── foo.rs
│   └── mod.rs
└── sub2.rs

</code></pre>
<p>在这个示例中，我们看到除了main.rs之外，还有一个sub2.rs以及一个目录sub1，sub1下面还有三个rs文件。我们从main.rs开始，逐一看一下各个源文件的内容：</p>
<pre><code>// organizing-rust-code/rustc-only/multi/multi-file-with-deps/main.rs
 1 extern crate rand;
 2 use rand::Rng;
 3
 4 mod sub1;
 5 mod sub2;
 6
 7 mod sub3 {
 8     pub fn func1() {
 9         println!("called {}::func1()", module_path!());
10     }
11     pub fn func2() {
12         self::func1();
13         println!("called {}::func2()", module_path!());
14         super::func1();
15     }
16 }
17
18 fn func1() {
19     println!("called {}::func1()", module_path!());
20 }
21
22 fn main() {
23     println!("current module: {}", module_path!());
24     let mut rng = rand::thread_rng();
25     let num: u32 = rng.gen();
26     println!("Random number: {}", num);
27
28     sub1::func1();
29     sub2::func1();
30     sub3::func2();
31 }
</code></pre>
<p>在main.rs中，我们除了看到了第1~2行的对外部rand crate的依赖外，我们还看到了一种新的语法元素：<strong>rust module</strong>。这里涉及sub1~sub3三个module，我们分别来看一下。先来看一下最直观的、定义在main.rs中的sub3 module。</p>
<p>第7行~第16行的代码定义了一个名为sub3的module，它包含两个函数func1和func2，这两个函数前面的pub关键字表明他们是sub3 module的publish函数，可以被module之外的代码所访问。任何未标记为pub的函数都是私有的，只能在模块内部及其子模块中使用。</p>
<p>在sub3 module的func2函数中，我们调用了self::func1()函数，self指代是模块自身，因此这个self::func1()函数就是sub3的func1函数。而接下来调用的super::func1()调用的语义你大概也能猜到。super指代的是sub3的父模块，而super::func1()就是sub3的父模块中的func1函数。</p>
<p>sub3的父模块就是这个项目的顶层模块，我们在main函数的入口处使用module_path!宏输出了该顶层模块的名称。</p>
<p>和sub3在main.rs中定义不同，sub1和sub2也分别代表了另外两种module的定义方式。</p>
<p>当Rust编译器看到第4行mod sub1后，它会寻找当前目录下是否有名为sub1.rs的源文件或是sub1/mod.rs源文件。在这个示例中，sub1定义在sub1目录下的mod.rs中：</p>
<pre><code>// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/mod.rs

pub mod bar;
pub mod foo;

pub fn func1() {
    println!("called {}::func1()", module_path!());
    foo::func1();
    bar::func1();
}
</code></pre>
<p>我们看到sub1/mod.rs中定义了一个公共函数func1，同时也在最开始处又嵌套定义了bar和foo两个module，并在func1中调用了两个嵌套子module的函数：</p>
<p>bar和foo两个module都是使用单文件module定义的，编译器会在sub1目录下搜寻foo.rs和bar.rs：</p>
<pre><code>// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/foo.rs
pub fn func1() {
    println!("called {}::func1()", module_path!());
}

// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/bar.rs
pub fn func1() {
    println!("called {}::func1()", module_path!());
}
</code></pre>
<p>而main.rs中的sub2也是一个单文件的module，其源码位于顶层目录下的sub2.rs文件中：</p>
<pre><code>// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub2.rs
pub fn func1() {
    println!("called {}::func1()", module_path!());
}
</code></pre>
<p>现在我们来编译和执行一下这个既有外部依赖，又是多文件且有多个module的rustc-only项目：</p>
<pre><code>$rustc --verbose  --extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib -L rand-0.8.5/target/debug/deps  --extern rand_core=librand_core-fc905f6ca5f8533b.rlib --extern getrandom=libgetrandom-df6a8e95e188fc56.rlib --extern cfg_if=libcfg_if-cd6bebf18fb9c234.rlib --extern libc=liblibc-f16531562d07b476.rlib --extern rand_chacha=librand_chacha-6fe22bd8b3bb228c.rlib --extern ppv_lite86=libppv_lite86-f1d97d485bc43617.rlib  main.rs 

$./main
current module: main
Random number: 2691905579
called main::sub1::func1()
called main::sub1::foo::func1()
called main::sub1::bar::func1()
called main::sub2::func1()
called main::sub3::func1()
called main::sub3::func2()
called main::func1()
</code></pre>
<p>上面示例演示了三种rust module的定义方法：</p>
<ol>
<li>直接将定义嵌入在某个rust源文件中：</li>
</ol>
<pre><code>mod module_name {

}
</code></pre>
<ol>
<li>通过module_name.rs</li>
<li>通过module_name/mod.rs</li>
</ol>
<p>在一个单crate的项目中，通过rust module可以满足项目内部代码组织的需要。</p>
<p>最后，我们再来看一个有多个crate的项目形式。</p>
<h3>4.2.4 有多个crate的项目</h3>
<p>下面是一个有着多个crate项目的示例：</p>
<pre><code>// organizing-rust-code/rustc-only/workspace

$tree -L 2 -F
.
├── main.rs
├── my_local_crate1/
│   └── lib.rs
└── my_local_crate2/
    └── lib.rs

</code></pre>
<p>在这个示例中有三个crate，一个是顶层的binary类型的crate，入口为main.rs，另外两个都是lib类型的crate，入口都在lib.rs中，我们贴一下他们的源码：</p>
<pre><code>// organizing-rust-code/rustc-only/workspace/main.rs
extern crate my_local_crate1;
extern crate my_local_crate2;

fn main() {
    let x = 5;
    let y = my_local_crate1::add_one(x);
    let z = my_local_crate2::multiply_two(y);
    println!("Result: {}", z);
}

// organizing-rust-code/rustc-only/workspace/my_local_crate1/lib.rs
pub fn add_one(x: i32) -&gt; i32 {
    x + 1
}

// organizing-rust-code/rustc-only/workspace/my_local_crate2/lib.rs
pub fn multiply_two(x: i32) -&gt; i32 {
    x * 2
}
</code></pre>
<p>要构建这个带有三个crate的项目，我们需要首先编译my_local_crate1和my_local_crate2这两个lib crates：</p>
<pre><code>$rustc --crate-type lib --crate-name my_local_crate1 my_local_crate1/lib.rs
$rustc --crate-type lib --crate-name my_local_crate2 my_local_crate2/lib.rs
</code></pre>
<p>这会在项目顶层目录下生成两个rlib文件：</p>
<pre><code>$ls  |grep rlib
libmy_local_crate1.rlib
libmy_local_crate2.rlib
</code></pre>
<p>之后，我们就可以用之前学到的方法编译binary crate了：</p>
<pre><code>$rustc --extern my_local_crate1=libmy_local_crate1.rlib --extern my_local_crate2=libmy_local_crate2.rlib main.rs
</code></pre>
<p>上述的几个rustc-only的rust项目都是hard模式的，即一切都需要手工去做，包括下载crate、编译crate时传入各种路径等。在真正的生产中，Rustacean们是不会这么做的，而是会直接使用cargo对rust项目进行管理。接下来，我们就来系统地看一下使用cargo进行rust项目管理以及对应的rust代码组织形式。</p>
<h2>4.3 使用cargo管理的Rust项目</h2>
<p>在前面的章节中，我们见识过了：Rust的包管理器Cargo是一个强大的工具，可以帮助我们轻松地管理Rust项目，cargo才是生产类项目的项目构建管理工具标准，它可以让Rustacean避免复杂的手工rustc操作。Cargo提供了许多功能，包括依赖项管理、构建和测试等。不过在这篇文章中，我不会介绍这些功能，而是看看使用cargo管理的Rust项目都有哪些代码组织模式。</p>
<p>Rust项目的代码组织结构可以分为两类：单一package和多个package。</p>
<p>什么是package？在之前的rust-only项目中，我们可从未见到过package！package是cargo引入的一个管理单元概念，它指的是一个独立的Rust项目，包含了源代码、依赖项和配置信息。每个Package都有一个唯一的名称和版本号，用于标识和管理项目。因此，在<a href="https://doc.rust-lang.org/cargo/index.html">the cargo book</a>中，cargo也被称为“Rust package manager”，crates.io也被称为“the Rust community’s package registry”。</p>
<p>最能直观体现package存在的就是下面Cargo.toml中的配置了：</p>
<pre><code>[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

[dependencies]
</code></pre>
<p>下面我们就来看看不同类型的rust package的代码组织形式。我们先从单一package形态的项目来开始。</p>
<h3>4.3.1 单一package的rust项目</h3>
<p>单一package项目是指整个项目只有一个Cargo.toml文件。这种项目还可以进一步分为三类：</p>
<ol>
<li>单一Binary Crate</li>
<li>单一Library Crate</li>
<li>多个Binary Crate和一个Library Crate</li>
</ol>
<p>下面我们分别举例来说明一下这三类项目。</p>
<h4>4.3.1.1 单一Binary Crate</h4>
<p>我们进入organizing-rust-code/cargo/single-package/single-binary-crate，然后执行下面命令来创建一个单一Binary Crate的项目：</p>
<pre><code>$cargo new hello_world --bin
     Created binary (application) `hello_world` package
</code></pre>
<p>这个例子我们在之前的章节中也是见过的，它的结构如下：</p>
<pre><code>$tree hello_world
hello_world
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files
</code></pre>
<p>默认生成的Cargo.toml内容如下：</p>
<pre><code>[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
</code></pre>
<p>使用cargo build即可完成该项目的构建：</p>
<pre><code>$cargo build
   Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/single-package/single-binary-crate/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 1.16s
</code></pre>
<p>为了更显式地体现这是一个binary crate，我们可以在Cargo.toml增加如下内容：</p>
<pre><code>[[bin]]
name = "hello_world"
path = "src/main.rs"
</code></pre>
<p>这不会影响cargo的构建结果！</p>
<p>通过cargo run可以查看构建出的可执行文件的运行结果：</p>
<pre><code>$cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/hello_world`
Hello, world!
</code></pre>
<p>接下来，我们再来看看单一library crate的rust项目。</p>
<h4>4.3.1.2 单一Library Crate</h4>
<p>我们进入organizing-rust-code/cargo/single-package/single-library-crate，然后执行下面命令来创建一个单一Library Crate的项目：</p>
<pre><code>$cargo new my_library --lib
     Created library `my_library` package
</code></pre>
<p>创建后的my_library项目的结构如下：</p>
<pre><code>$tree
.
├── Cargo.toml
└── src
    └── lib.rs
</code></pre>
<p>默认生成的Cargo.toml如下：</p>
<pre><code>[package]
name = "my_library"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
</code></pre>
<p>和binary crate的一样，我们也可以显式指定target：</p>
<pre><code>[lib]
name = "my_library"
path = "src/lib.rs"
</code></pre>
<p>注意，这里是[lib]而不是[[lib]]，这是因为在一个carge package中最多只能存在一个library crate，但binary crate可以有多个。</p>
<p>接下来，我们就看看一个由多个binary crate和一个library crate混合构成的rust项目。</p>
<h4>4.3.1.3 多个Binary Crate和一个Library Crate</h4>
<p>我们在organizing-rust-code/cargo/single-package/hybrid-crates下面执行如下命令创建这个多crates混合项目：</p>
<pre><code>$cargo new my_project
     Created binary (application) `my_project` package
</code></pre>
<p>上述命令默认创建了一个binary crate的project，我们需要配置一下Cargo.toml，将其改造为多个crates并存的project：</p>
<pre><code>[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "cmd1"
path = "src/main1.rs"

[[bin]]
name = "cmd2"
path = "src/main2.rs"

[lib]
name = "my_library"
path = "src/lib.rs"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
</code></pre>
<p>这里定义了三个crates。两个binary crates: cmd1、cmd2以及一个library crate：my_library。</p>
<p>如果我们执行cargo build，cargo会将三个crate都构建出来：</p>
<pre><code>$cargo build
   Compiling my_project v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/single-package/hybrid-crates/my_project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.80s

</code></pre>
<p>我们可以在target/debug下找到构建出的crates：cmd1、cmd2和libmy_library.rlib：</p>
<pre><code>$ls target/debug
build/          cmd1.d          cmd2.d          examples/       libmy_library.d
cmd1*           cmd2*           deps/           incremental/        libmy_library.rlib
</code></pre>
<p>我们也可以通过cargo分别运行两个binary crate：</p>
<pre><code>$cargo run --bin cmd1
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/cmd1`
cmd1

$cargo run --bin cmd2
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/cmd2`
cmd2
</code></pre>
<h4>4.3.1.4 典型的cargo package</h4>
<p>在The cargo book中，有一个典型的cargo package的示例：</p>
<pre><code>.
├── Cargo.lock
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── main.rs
│   └── bin/
│       ├── named-executable.rs
│       ├── another-executable.rs
│       └── multi-file-executable/
│           ├── main.rs
│           └── some_module.rs
├── benches/
│   ├── large-input.rs
│   └── multi-file-bench/
│       ├── main.rs
│       └── bench_module.rs
├── examples/
│   ├── simple.rs
│   └── multi-file-example/
│       ├── main.rs
│       └── ex_module.rs
└── tests/
    ├── some-integration-tests.rs
    └── multi-file-test/
        ├── main.rs
        └── test_module.rs

</code></pre>
<p>在这样一个典型的项目中：</p>
<ul>
<li>Cargo.toml和Cargo.lock文件存储在包的根目录（包根目录）中。</li>
<li>源代码位于src目录中。</li>
<li>默认的库文件是src/lib.rs。</li>
<li>默认的可执行文件是src/main.rs。</li>
<li>其他可执行文件可以放在src/bin/目录中。</li>
<li>基准测试位于benches目录中。</li>
<li>示例位于examples目录中。</li>
<li>集成测试位于tests目录中。</li>
</ul>
<h3>4.3.2 多package的rust项目</h3>
<p>一些中大型的Rust项目都是多package的，比如rust的异步编程事实标准<a href="https://github.com/tokio-rs/tokio">tokio库</a>、刚刚升级为Apache基金会顶级项目的<a href="https://github.com/apache/datafusion">SQL查询引擎datafusion</a>等。以tokio为例，这些项目的顶层Cargo.toml都是这样的：</p>
<pre><code>// https://github.com/tokio-rs/tokio/blob/master/Cargo.toml
[workspace]
resolver = "2"
members = [
  "tokio",
  "tokio-macros",
  "tokio-test",
  "tokio-stream",
  "tokio-util",

  # Internal
  "benches",
  "examples",
  "stress-test",
  "tests-build",
  "tests-integration",
]

[workspace.metadata.spellcheck]
config = "spellcheck.toml"
</code></pre>
<p>上面这个Cargo.toml示例与我们在前面见到的Cargo.toml都不一样，它并不包含package配置，其主要的配置为workspace。我们看到workspace的members字段中配置了该项目下的其他package。正是通过这个配置，cargo可以在一个项目里管理和构建多个package。</p>
<p><a href="https://doc.rust-lang.org/cargo/reference/workspaces.html">工作空间（Workspace）</a>是一组一个或多个包（Package）的集合，这些包称为工作空间成员（Workspace Members），它们一起被管理。接下来，我们就来创建一个多package的cargo项目。</p>
<h4>4.3.2.1 cargo管理的多package项目</h4>
<p>由于cargo并没有提供cargo new my-pakcage &#8211;workspace这样的命令行参数，项目的顶层Cargo.toml需要我们手动创建和编辑。</p>
<pre><code>$cd organizing-rust-code/cargo/multi-packages
$mkdir my-workspace
$cd my-workspace
$cargo new package1 --bin
     Created binary (application) `package1` package
$cargo new package2 --lib
     Created library `package2` package
$cargo new package3 --lib
     Created library `package3` package
</code></pre>
<p>接下来，我们手工创建和编辑一下项目顶层的Cargo.toml如下：</p>
<pre><code>// organizing-rust-code/cargo/multi-packages/my-workspace/Cargo.toml
[workspace]
resolver = "2"
members = [
    "package1",
    "package2",
    "package3",
]
</code></pre>
<p>保存后，我们可以在项目顶层目录下使用下面命令检查整个工作空间（workspace）中的所有包（package），确保它们的代码正确无误，不包含任何编译错误：</p>
<pre><code>$cargo check --workspace
    Checking package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package1)
    Checking package2 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package2)
    Checking package3 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package3)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
</code></pre>
<p>在顶层目录执行cargo build，cargo会build工作空间中的所有package：</p>
<pre><code>$cargo build
   Compiling package3 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package3)
   Compiling package2 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package2)
   Compiling package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.64s
</code></pre>
<p>构建后，该项目的目录结构变成下面这个样子：</p>
<pre><code>$tree -L 2 -F
.
├── Cargo.lock
├── Cargo.toml
├── package1/
│   ├── Cargo.toml
│   └── src/
├── package2/
│   ├── Cargo.toml
│   └── src/
├── package3/
│   ├── Cargo.toml
│   └── src/
└── target/
    ├── CACHEDIR.TAG
    └── debug/

</code></pre>
<p>我们看到该项目下的所有package共享一个共同的 Cargo.lock 文件，该文件位于工作空间的根目录下。并且，所有包共享一个共同的输出目录，默认情况下是工作空间根目录下的一个名为target的目录，该target目录下的布局如下：</p>
<pre><code>$tree -F -L 2 ./target
./target
├── CACHEDIR.TAG
└── debug/
    ├── build/
    ├── deps/
    ├── examples/
    ├── incremental/
    ├── libpackage2.d
    ├── libpackage2.rlib
    ├── libpackage3.d
    ├── libpackage3.rlib
    ├── package1*
    └── package1.d
</code></pre>
<p>我们在这下面可以找到所有package的编译输出结果，比如package1、libpackage2.rlib以及libpackage3.rlib。</p>
<p>当然，你也可以指定一个package来构建或运行：</p>
<pre><code>$cargo build -p package1
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
$cargo build -p package2
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
$cargo run -p package1
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/package1`
Hello, world!
</code></pre>
<h4>4.3.2.2 带有外部依赖和内部依赖的多package项目</h4>
<p>我们复制一份my-workspace，改名为my-workspace-with-deps，修改一下package1/src/main.rs，为其增加外部依赖rand crate：</p>
<pre><code>// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rs
extern crate rand;

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let num: u32 = rng.gen();
    println!("Random number: {}", num);
}
</code></pre>
<p>接下来，我们需要修改一下package1/Cargo.toml，手工加上对rand crate的依赖配置：</p>
<pre><code>// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/Cargo.toml
[package]
name = "package1"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.5"

</code></pre>
<p>保存后，我们执行package1的构建：</p>
<pre><code>$cargo build -p package1
  Downloaded getrandom v0.2.14 (registry `rsproxy`)
  Downloaded libc v0.2.154 (registry `rsproxy`)
  Downloaded 2 crates (780.6 KB) in 1m 07s
   Compiling libc v0.2.154
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.17
   Compiling getrandom v0.2.14
   Compiling rand_core v0.6.4
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1)
    Finished dev [unoptimized + debuginfo] target(s) in 1m 46s
</code></pre>
<p>我们看到：cargo会自动下载package1的直接外部依赖以及相关间接依赖。构建成功后，可以执行一下package1的编译结果：</p>
<pre><code>$cargo run -p package1
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/package1`
Random number: 3840180495
</code></pre>
<p>接下来，我们再为package1添加内部依赖，比如依赖package2的编译结果：</p>
<pre><code>// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rs

extern crate package2;
extern crate rand;

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let num: u32 = rng.gen();
    println!("Random number: {}", num);
    let result = package2::add(2, 2);
    println!("result: {}", result);
}

// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/Cargo.toml
[package]
name = "package1"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.5"
package2 = { path = "../package2" }
</code></pre>
<p>我们看到：package1的main.rs依赖package2这个crate中的add函数，我们在package1的Cargo.toml中为package1添加了新依赖package2，由于package2仅仅存放在本地，所以这里我们使用了path方式指定package2的位置。</p>
<p>我们执行一下添加内部依赖后的package1：</p>
<pre><code>$cargo run -p package1
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/package1`
Random number: 2485645524
result: 4
</code></pre>
<h2>4.4 小结</h2>
<p>本文循序渐进地讨论了在Rust项目中如何组织代码的问题，这对于Rust初学者来说尤为有用。</p>
<p>我们首先回顾了Go语言中的代码组织方式，介绍了Go项目代码组织的两个层级：module和package。然后，我们将Rust项目可以分为两种类型：使用rustc编译器的项目和使用Cargo的项目。</p>
<p>对于rustc-only的项目，开发者需要编写自己的构建脚本来管理项目的构建过程。</p>
<p>文章从最简单的单文件rustc-only项目开始介绍，展示了如何使用rustc编译器来编译和运行这种项目，并逐步介绍了带有外部依赖的rustc-only项目以及多文件项目的情况，引出了rust module概念。</p>
<p>rustc-only项目很少用于生产环境，这种方式主要用于学习和了解Rustc编译器的功能机制以及Rust语言的代码组织抽象。</p>
<p>在实际开发中，使用Cargo来创建和管理Rust包是常见的做法。在本章的后半段，我们介绍了使用cargo管理的rust项目的代码组织情况，包括单package项目和多package项目以及如何为项目引入外部和内部依赖。</p>
<p>总体而言，本文旨在帮助初学者理解和掌握Rust项目的代码组织结构，以提高学习效率和学习效果。通过介绍rustc-only项目和cargo管理的项目，读者可以逐步了解Rust代码组织的基本概念和实践方法。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/rust-guide-for-gopher/organizing-rust-code">这里</a>下载。</p>
<h2>4.5 参考资料</h2>
<ul>
<li><a href="https://doc.rust-lang.org/book">The book</a> &#8211; https://doc.rust-lang.org/book</li>
<li><a href="https://doc.rust-lang.org/cargo/index.html">The cargo book</a> &#8211; https://doc.rust-lang.org/cargo/index.html</li>
<li><a href="https://doc.rust-lang.org/rustc/index.html">The rustc book</a> &#8211; https://doc.rust-lang.org/rustc/index.html</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的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，价格5$/月。有使用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>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go程序员拥抱C语言简明指南</title>
		<link>https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/</link>
		<comments>https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/#comments</comments>
		<pubDate>Sun, 15 May 2022 23:11:16 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ANSI-C]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[break]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C11]]></category>
		<category><![CDATA[C18]]></category>
		<category><![CDATA[C99]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[clang-format]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[Configure]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[C标准库]]></category>
		<category><![CDATA[C语言]]></category>
		<category><![CDATA[DennisRitchie]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[fallthrough]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[iso]]></category>
		<category><![CDATA[K&R]]></category>
		<category><![CDATA[KenThompson]]></category>
		<category><![CDATA[LeetCode]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[Lint]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[loccount]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[soname]]></category>
		<category><![CDATA[switch-case]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[utf-8]]></category>
		<category><![CDATA[Windows]]></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=3535</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher 本文是为于航老师的极客时间专栏《深入C语言和程序运行原理》写的加餐文章《Tony Bai：Go程序员拥抱C语言简明指南》，这里分享给大家，尤其是那些想学习C语言的Gopher们。 你好，我是Tony Bai。 也许有同学对我比较熟悉，看过我在极客时间上的专栏《Tony Bai ·Go语言第一课》，或者是关注了我的博客。那么，作为一个Gopher，我怎么跑到这个C语言专栏做分享了呢？其实，在学习Go语言并成为一名Go程序员之前，我也曾是一名地地道道的C语言程序员。 大学毕业后，我就开始从事C语言后端服务开发工作，在电信增值领域摸爬滚打了十多年。不信的话，你可以去翻翻我的博客，数一数我发的C语言相关文章是不是比关于Go的还多。一直到近几年，我才将工作中的主力语言从C切换到了Go。不过这并不是C语言的问题，主要原因是我转换赛道了。我目前在智能网联汽车领域从事面向云原生平台的先行研发，而在云原生方面，新生代的Go语言有着更好的生态。 不过作为资深C程序员，C语言已经在我身上打下了深深的烙印。虽然Go是我现在工作中的主力语言，但我仍然会每天阅读一些C开源项目的源码，每周还会写下数百行的C代码。在一些工作场景中，特别是在我参与先行研发一些车端中间件时，C语言有着资源占用小、性能高的优势，这一点是Go目前还无法匹敌的。 正因为我有着C程序员和Go程序员的双重身份，接到这个加餐邀请时，我就想到了一个很适合聊的话题——在 Gopher（泛指Go程序员）与C语言之间“牵线搭桥”。在这门课的评论区里，我看到一些同学说，“正是因为学了Go，所以我想学好C”。如果你也对Go比较熟悉，那么恭喜你，这篇加餐简直是为你量身定制的：一个熟悉Go的程序员在学习C时需要注意的问题，还有可能会遇到的坑，我都替你总结好了。 当然，我知道还有一些对Go了解不多的同学，看到这里也别急着退出去。因为C和Go这两门语言的比较，本身就是一个很有意思的话题。今天的加餐，会涉及这两门语言的异同点，通过对C与Go语言特性的比较，你就能更好地理解“C 语言为什么设计成现在这样”。 一. C语言是现代IT工业的根基 在比较C和Go之前，先说说我推荐Gopher学C的最重要原因吧：用一句话总结，C语言在IT工业中的根基地位，是Go和其他语言目前都无法动摇的。 C语言是由美国贝尔实验室的丹尼斯·里奇（Dennis Ritchie）以Unix发明人肯·汤普森（Ken Thompson）设计的B语言为基础而创建的高级编程语言。诞生于上个世纪（精确来说是1972年）的它，到今年（2022年）已到了“知天命”的半百年纪。 年纪大、设计久远一直是“C语言过时论”兴起的根源，但如果你相信这一论断，那就大错特错了。下面，我来为你分析下个中缘由。 首先，我们说说C语言本身：C语言一直在演进，从未停下过脚步。 虽然C语言之父丹尼斯·里奇不幸于2011年永远地离开了我们，但C语言早已成为ANSI（美国国家标准学会）标准以及ISO/IEC（国际标准化组织和国际电工委员会）标准，因此其演进也早已由标准委员会负责。我们来简单回顾一下C语言标准的演进过程： 1989年，ANSI发布了首个C语言标准，被称为C89，又称ANSI C。次年，ISO和IEC把ANSI C89标准定为C语言的国际标准（ISO/IEC 9899:1990），又称C90，它也是C语言的第一个官方版本； 1999年，ISO和IEC发布了C99标准(ISO/IEC 9899:1999)，它是C语言的第二个官方版本； 2011年，ISO和IEC发布了C11标准(ISO/IEC 9899:2011)，它是C语言的第三个官方版本； 2018年，ISO和IEC发布了C18标准(ISO/IEC 9899:2018)，它是C语言的第四个官方版本。 目前，ISO/IEC标准化委员会正在致力于C2x标准的改进与制定，预计它会在2023年发布。 其次，时至今日，C语言的流行度仍然非常高。 著名编程语言排行榜TIOBE的数据显示，各大编程语言年度平均排名的总位次，C语言多年来高居第一，如下图（图片来自TIOBE）所示： 这说明，无论是在过去还是现在，C语言都是一门被广泛应用的工业级编程语言。 最后，也是最重要的一点是：C语言是现代IT工业的根基，我们说C永远不会退出IT行业舞台也不为过。 如今，无论是普通消费者端的Windows、macOS、Android、苹果iOS，还是服务器端的Linux、Unix等操作系统，亦或是各个工业嵌入式领域的操作系统，其内核实现语言都是C语言。互联网时代所使用的主流Web服务器，比如 Nginx、Apache，以及主流数据库，比如MySQL、Oracle、PostgreSQL等，也都是使用C语言开发的杰作。可以说，现代人类每天都在跟由C语言实现的系统亲密接触，并且已经离不开这些系统了。回到我们程序员的日常，Git、SVN等我们时刻在用的源码版本控制软件也都是由C语言实现的。 可以说，C语言在IT工业中的根基地位，不光Go语言替代不了，C++、Rust等系统编程语言也无法动摇，而且不仅短期如此，长期来看也是如此。 总之，C语言具有紧凑、高效、移植性好、对内存的精细控制等优秀特性，这使得我们在任何时候学习它都不会过时。不过，我在这里推荐Gopher去了解和系统学习C语言，其实还有另一个原因。我们继续往下看。 二. C与Go的相通之处：Gopher拥抱C语言的“先天优势” 众所周知，Go 是在C语言的基础上衍生而来的，二者之间有很多相通之处，因此 Gopher 在学习C语言时是有“先天优势”的。接下来，我们具体看看C和Go的相通之处有哪些。 1. 简单且语法同源 Go语言以简单著称，而作为Go先祖的C语言，入门门槛同样不高：Go有25个关键字，C有32个关键字（C89标准），简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言，这与C的简单也不无关系。 和Go不同的是，C语言是一个小内核、大外延的编程语言，其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库，我们可以快速掌握它。但需要注意的是，与Go语言“开箱即用、内容丰富”的标准库不同，C标准库非常小（在C11标准之前甚至连thread库都不包含），所以掌握“小内核”后，在LeetCode平台上刷题是没有任何问题的，但要写出某一领域的工业级生产程序，我们还有很多外延知识技能要学习，比如并发原语、操作系统的系统调用，以及进程间通信等。 C语言的这种简单很容易获得Gopher们的认同感。当年Go语言之父们在设计Go语言时，也是主要借鉴了C语言的语法。当然，这与他们深厚的C语言背景不无关系：肯·汤普森（Ken [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher">本文永久链接</a> &#8211; https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher</p>
<p>本文是为于航老师的极客时间专栏<a href="http://gk.link/a/11osT">《深入C语言和程序运行原理》</a>写的加餐文章<a href="https://time.geekbang.org/column/article/500145">《Tony Bai：Go程序员拥抱C语言简明指南》</a>，这里分享给大家，尤其是那些想学习C语言的Gopher们。</p>
<hr />
<p>你好，我是Tony Bai。</p>
<p>也许有同学对我比较熟悉，看过我在极客时间上的专栏<a href="http://gk.link/a/10AVZ">《Tony Bai ·Go语言第一课》</a>，或者是关注了<a href="https://tonybai.com">我的博客</a>。那么，作为一个Gopher，我怎么跑到这个C语言专栏做分享了呢？其实，在学习Go语言并成为一名Go程序员之前，我也曾是一名地地道道的C语言程序员。</p>
<p>大学毕业后，我就开始从事C语言后端服务开发工作，在电信增值领域摸爬滚打了十多年。不信的话，你可以去翻翻<a href="https://tonybai.com/tag/c">我的博客</a>，数一数我发的C语言相关文章是不是比关于Go的还多。一直到近几年，我才将工作中的主力语言从C切换到了Go。不过这并不是C语言的问题，主要原因是我转换赛道了。我目前在智能网联汽车领域从事面向云原生平台的先行研发，而在云原生方面，新生代的Go语言有着更好的生态。</p>
<p>不过作为资深C程序员，C语言已经在我身上打下了深深的烙印。虽然Go是我现在工作中的主力语言，但我仍然会每天阅读一些C开源项目的源码，每周还会写下数百行的C代码。在一些工作场景中，特别是在我参与先行研发一些车端中间件时，C语言有着资源占用小、性能高的优势，这一点是Go目前还无法匹敌的。</p>
<p>正因为我有着C程序员和Go程序员的双重身份，接到这个加餐邀请时，我就想到了一个很适合聊的话题——在 Gopher（泛指Go程序员）与C语言之间“牵线搭桥”。在这门课的评论区里，我看到一些同学说，“正是因为学了Go，所以我想学好C”。如果你也对Go比较熟悉，那么恭喜你，这篇加餐简直是为你量身定制的：一个熟悉Go的程序员在学习C时需要注意的问题，还有可能会遇到的坑，我都替你总结好了。</p>
<p><strong>当然，我知道还有一些对Go了解不多的同学，看到这里也别急着退出去。</strong>因为C和Go这两门语言的比较，本身就是一个很有意思的话题。今天的加餐，会涉及这两门语言的异同点，通过对C与Go语言特性的比较，你就能更好地理解“C 语言为什么设计成现在这样”。</p>
<h2>一. C语言是现代IT工业的根基</h2>
<p>在比较C和Go之前，先说说我推荐Gopher学C的最重要原因吧：用一句话总结，<strong>C语言在IT工业中的根基地位，是Go和其他语言目前都无法动摇的</strong>。</p>
<p>C语言是由美国贝尔实验室的丹尼斯·里奇（Dennis Ritchie）以Unix发明人肯·汤普森（Ken Thompson）设计的B语言为基础而创建的高级编程语言。诞生于上个世纪（精确来说是1972年）的它，到今年（2022年）已到了“知天命”的半百年纪。 年纪大、设计久远一直是“C语言过时论”兴起的根源，但如果你相信这一论断，那就大错特错了。下面，我来为你分析下个中缘由。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-3.jpeg" alt="" /></p>
<p>首先，我们说说C语言本身：<strong>C语言一直在演进，从未停下过脚步</strong>。</p>
<p>虽然C语言之父丹尼斯·里奇不幸于2011年永远地离开了我们，但C语言早已成为ANSI（美国国家标准学会）标准以及ISO/IEC（国际标准化组织和国际电工委员会）标准，因此其演进也早已由标准委员会负责。我们来简单回顾一下C语言标准的演进过程：</p>
<ul>
<li>1989年，ANSI发布了首个C语言标准，被称为C89，又称ANSI C。次年，ISO和IEC把ANSI C89标准定为C语言的国际标准（ISO/IEC 9899:1990），又称C90，它也是C语言的第一个官方版本；</li>
<li>1999年，ISO和IEC发布了<a href="https://www.iso.org/standard/29237.html">C99标准(ISO/IEC 9899:1999)</a>，它是C语言的第二个官方版本；</li>
<li>2011年，ISO和IEC发布了<a href="https://www.iso.org/standard/57853.html">C11标准(ISO/IEC 9899:2011)</a>，它是C语言的第三个官方版本；</li>
<li>2018年，ISO和IEC发布了<a href="https://www.iso.org/standard/74528.html">C18标准(ISO/IEC 9899:2018)</a>，它是C语言的第四个官方版本。<br />
目前，ISO/IEC标准化委员会正在致力于C2x标准的改进与制定，预计它会在2023年发布。</li>
</ul>
<p>其次，<strong>时至今日，C语言的流行度仍然非常高</strong>。</p>
<p>著名编程语言排行榜TIOBE的数据显示，各大编程语言年度平均排名的总位次，C语言多年来高居第一，如下图（图片来自<a href="https://www.tiobe.com/tiobe-index">TIOBE</a>）所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-2.png" alt="" /></p>
<p>这说明，无论是在过去还是现在，C语言都是一门被广泛应用的工业级编程语言。</p>
<p>最后，也是最重要的一点是：<strong>C语言是现代IT工业的根基</strong>，我们说C永远不会退出IT行业舞台也不为过。</p>
<p>如今，无论是普通消费者端的Windows、macOS、Android、苹果iOS，还是服务器端的Linux、Unix等操作系统，亦或是各个工业嵌入式领域的操作系统，其内核实现语言都是C语言。互联网时代所使用的主流Web服务器，比如 Nginx、Apache，以及主流数据库，比如MySQL、Oracle、PostgreSQL等，也都是使用C语言开发的杰作。可以说，现代人类每天都在跟由C语言实现的系统亲密接触，并且已经离不开这些系统了。回到我们程序员的日常，Git、SVN等我们时刻在用的源码版本控制软件也都是由C语言实现的。</p>
<p>可以说，C语言在IT工业中的根基地位，不光Go语言替代不了，C++、Rust等系统编程语言也无法动摇，而且不仅短期如此，长期来看也是如此。</p>
<p>总之，C语言具有紧凑、高效、移植性好、对内存的精细控制等优秀特性，这使得我们在任何时候学习它都不会过时。不过，我在这里推荐Gopher去了解和系统学习C语言，其实还有另一个原因。我们继续往下看。</p>
<h2>二. C与Go的相通之处：Gopher拥抱C语言的“先天优势”</h2>
<p>众所周知，Go 是在C语言的基础上衍生而来的，二者之间有很多相通之处，因此 Gopher 在学习C语言时是有“先天优势”的。接下来，我们具体看看C和Go的相通之处有哪些。</p>
<h3>1. 简单且语法同源</h3>
<p>Go语言以简单著称，而作为<strong>Go先祖</strong>的C语言，入门门槛同样不高：Go有25个关键字，C有32个关键字（C89标准），简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言，这与C的简单也不无关系。</p>
<p>和Go不同的是，C语言是一个<strong>小内核、大外延</strong>的编程语言，其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库，我们可以快速掌握它。但需要注意的是，与Go语言“开箱即用、内容丰富”的标准库不同，<a href="https://en.wikipedia.org/wiki/C_standard_library">C标准库</a>非常小（在C11标准之前甚至连thread库都不包含），所以掌握“小内核”后，在LeetCode平台上刷题是没有任何问题的，但要写出某一领域的工业级生产程序，我们还有很多外延知识技能要学习，比如并发原语、操作系统的系统调用，以及进程间通信等。</p>
<p>C语言的这种简单很容易获得Gopher们的认同感。当年Go语言之父们在设计Go语言时，也是主要借鉴了C语言的语法。当然，这与他们深厚的C语言背景不无关系：肯·汤普森（Ken Thompson）是Unix之父，与丹尼斯·里奇共同设计了C语言；罗博·派克（Rob Pike）是贝尔实验室的资深研究员，参与了Unix系统的演进、Plan9操作系统的开发，还是UTF-8编码的发明人；罗伯特·格瑞史莫（Robert Griesemer）也是用C语言手写Java虚拟机的大神级人物。</p>
<p>Go的第一版编译器就是由肯·汤普森（Ken Thompson）用C语言实现的。并且，Go语言的早期版本中，C代码的比例还不小。以Go语言发布的第一个版本，<a href="https://github.com/golang/go/releases/tag/go1">Go 1.0版本</a>为例，我们通过<a href="https://gitlab.com/esr/loccount">loccount工具</a>对其进行分析，会得到下面的结果：</p>
<pre><code>$loccount .
all          SLOC=460992  (100.00%) LLOC=193045  in 2746 files
Go           SLOC=256321  (55.60%)  LLOC=109763  in 1983 files
C            SLOC=148001  (32.10%)  LLOC=73458   in 368 files
HTML         SLOC=25080   (5.44%)   LLOC=0       in 57 files
asm          SLOC=10109   (2.19%)   LLOC=0       in 133 files
... ...
</code></pre>
<p>这里我们看到，在1.0版本中，C语言代码行数占据了32.10%的份额，这一份额直至Go 1.5版本实现自举后，才下降为不到1%。</p>
<p>我当初对Go“一见钟情”，其中一个主要原因就是Go与C语言的<strong>语法同源。</strong>相对应地，相信这种同源的语法也会让Gopher们喜欢上C语言。</p>
<h3>2. 静态编译且基础范式相同</h3>
<p>除了语法同源，C语言与Go语言的另一个相同点是，它们都是静态编译型语言。这意味着它们都有如下的语法特性：</p>
<ul>
<li>变量与函数都要先声明后才能使用；</li>
<li>所有分配的内存块都要有对应的类型信息，并且在确定其类型信息后才能操作；</li>
<li>源码需要先编译链接后才能运行。</li>
</ul>
<p>相似的编程逻辑与构建过程，让学习C语言的Gopher可以做到无缝衔接。</p>
<p>除此之外，Go 和C的基础编程范式都是命令式编程（imperative programming），即面向算法过程，由程序员通过编程告诉计算机应采取的动作。然后，计算机按程序指令执行一系列流程，生成特定的结果，就像菜谱指定了厨师做蛋糕时应遵循的一系列步骤一样。</p>
<p>从Go看 C，没有面向对象，没有函数式编程，没有泛型（Go 1.18已加入），满眼都是类型与函数，可以说是相当亲切了。</p>
<h3>3. 错误处理机制如出一辙</h3>
<p>对于后端编程语言来说，错误处理机制十分重要。如果两种语言的错误处理机制不同，那么这两种语言的代码整体语法风格很可能大不相同。</p>
<p>在C语言中，我们通常用一个类型为整型的函数返回值作为错误状态标识，函数调用者基于值比较的方式，对这一代表错误状态的返回值进行检视。通常，当这个返回值为0时，代表函数调用成功；当这个返回值为其他值时，代表函数调用出现错误。函数调用者需根据该返回值所代表的错误状态，来决定后续执行哪条错误处理路径上的代码。</p>
<p>C语言这种简单的<strong>基于错误值比较</strong>的错误处理机制，让每个开发人员必须显式地去关注和处理每个错误。经过显式错误处理的代码会更为健壮，也会让开发人员对这些代码更有信心。另外，这些错误就是普通的值，我们不需要额外的语言机制去处理它们，只需利用已有的语言机制，像处理其他普通类型值那样去处理错误就可以了。这让代码更容易调试，我们也更容易针对每个错误处理的决策分支进行测试覆盖。</p>
<p>C语言错误处理机制的这种简单与显式，跟Go语言的设计哲学十分契合，于是Go语言设计者决定继承这种错误处理机制。因此，当Gopher们来到C语言的世界时，无需对自己的错误处理思维做出很大的改变，就可以很容易地适应C语言的风格。</p>
<h2>三. 知己知彼，来看看C与Go的差异</h2>
<p>虽说 Gopher 学习C语言有“先天优势”，但是不经过脚踏实地的学习与实践就想掌握和精通C语言，也是不可能的。而且，C 和Go还是有很大差异的，Gopher 们只有清楚这些差异，做到“知己知彼”，才能在学习过程中分清轻重，有的放矢。俗话说，“磨刀不误砍柴功”，下面我们就一起看看C与Go有哪些不同。</p>
<h3>1. 设计哲学</h3>
<p>在人类自然语言学界，有一个很著名的假说——“<a href="https://en.wikipedia.org/wiki/Linguistic_relativity">萨丕尔-沃夫假说</a>”。这个假说的内容是这样的：<strong>语言影响或决定人类的思维方式</strong>。对我来说，<strong>编程语言也不仅仅是一门工具，它还影响着程序员的思维方式</strong>。每次开始学习一门新的编程语言时，我都会先了解这门编程语言的设计哲学。</p>
<p>每种编程语言都有自己的设计哲学，即便这门语言的设计者没有将其显式地总结出来，它也真真切切地存在，并影响着这门语言的后续演进，以及这门语言程序员的思维方式。我在<a href="http://gk.link/a/10AVZ">《Tony Bai · Go语言第一课》</a>专栏里，将Go语言的设计哲学总结成了5点，分别是<strong>简单、显式、组合、并发和面向工程</strong>。</p>
<p>那么C语言的设计哲学又是什么呢？从表面上看，简单紧凑、性能至上、极致资源、全面移植，这些都可以作为C的设计哲学，但我倾向于一种更有人文气息的说法：<strong>满足和相信程序员</strong>。</p>
<p>在这样的设计哲学下，一方面，C语言提供了几乎所有可以帮助程序员表达自己意图的语法手段，比如宏、指针与指针运算、位操作、pragma指示符、goto语句，以及跳转能力更为强大的longjmp等；另一方面，C语言对程序员的行为并没有做特别严格的限定与约束，C程序员可以利用语言提供的这些语法手段，进行天马行空的发挥：访问硬件、利用指针访问内存中的任一字节、操控任意字节中的每个位（bit）等。总之，C语言假定程序员知道他们在做什么，并选择相信程序员。</p>
<p>C语言给了程序员足够的自由，可以说，在C语言世界，你几乎可以“为所欲为”。但这种哲学也是有代价的，那就是你可能会犯一些莫名其妙的错误，比如悬挂指针，而这些错误很少或不可能在其他语言中出现。</p>
<p>这里再用一个比喻来更为形象地表达下：从Go世界到C世界，就好比在动物园中饲养已久的动物被放归到野生自然保护区，有了更多自由，但周围也暗藏着很多未曾遇到过的危险。因此，学习C语言的Gopher们要有足够的心理准备。</p>
<h3>2. 内存管理</h3>
<p>接下来我们来看C与Go在内存管理方面的不同。我把这一点放在第二位，是因为这两种语言在内存管理上有很大的差异，而且这一差异会给程序员的日常编码带来巨大影响。</p>
<p>我们知道，Go是带有垃圾回收机制（俗称GC）的静态编程语言。使用Go编程时，内存申请与释放，在栈上还是在堆上分配，以及新内存块的清零等等，这一切都是自动的，且对程序员透明。</p>
<p>但在C语言中，上面说的这些都是程序员的责任。手工内存管理在带来灵活性的同时，也带来了极大的风险，其中最常见的就是内存泄露（memory leak）与悬挂指针（dangling pointer）问题。</p>
<p>内存泄露主要指的是<strong>程序员手工在堆上分配的内存在使用后没有被释放（free），进而导致的堆内存持续增加</strong>。而悬挂指针的意思是<strong>指针指向了非法的内存地址</strong>，未初始化的指针、指针所指对象已经被释放等，都是导致悬挂指针的主要原因。针对悬挂指针进行解引用（dereference）操作将会导致运行时错误，从而导致程序异常退出的严重后果。</p>
<p>Go语言带有GC，而C语言不带GC，这都是由各自语言设计哲学所决定的。GC是不符合C语言的设计哲学的，因为一旦有了GC，程序员就远离了机器，程序员直面机器的需求就无法得到满足了。并且，一旦有了GC，无论是在性能上还是在资源占用上，都不可能做到极致了。</p>
<p>在C中，手工管理内存到底是一种什么感觉呢？作为一名有着十多年C开发经验的资深C程序员，我只能告诉你：<strong>与内存斗，其乐无穷</strong>！这是在带GC的编程语言中无法体会到的。</p>
<h3>3. 语法形式</h3>
<p>虽然C语言是Go的先祖，并且Go也继承了很多C语言的语法元素，但在变量/函数声明、行尾分号、代码块是否用括号括起、标识符作用域，以及控制语句语义等方面，二者仍有较大差异。因此，对Go已经很熟悉的程序员在初学C时，受之前编码习惯的影响，往往会踩一些“坑”。基于此，我总结了Gopher学习C语言时需要特别注意的几点，接下来我们具体看看。</p>
<p><strong>第一，注意声明变量时类型与变量名的顺序</strong></p>
<p>前面说过，Go与C都是静态编译型语言，这就要求我们在使用任何变量之前，需要先声明这个变量。但Go采用的变量声明语法颇似Pascal语言，即<strong>变量名在前，变量类型在后</strong>，这与C语言恰好相反，如下所示：</p>
<pre><code>Go:

var a, b int
var p, q *int

vs.

C：
int a, b;
int *p, *q;
</code></pre>
<p>此外，Go支持短变量声明，并且由于短变量声明更短小，无需显式提供变量类型，Go编译器会根据赋值操作符后面的初始化表达式的结果，自动为变量赋予适当类型。因此，它成为了Gopher们喜爱和重度使用的语法。但短声明在C中却不是合法的语法元素：</p>
<pre><code>int main() {
    a := 5; //  error: expected expression
    printf("a = %d\n", a);
}
</code></pre>
<p>不过，和上面的变量类型与变量名声明的顺序问题一样，C编译器会发现并告知我们这个问题，并不会给程序带来实质性的伤害。</p>
<p><strong>第二，注意函数声明无需关键字前缀</strong></p>
<p>无论是C语言还是Go语言，函数都是基本功能逻辑单元，我们也可以说<strong>C程序就是一组函数的集合</strong>。实际上，我们日常的C代码编写大多集中在实现某个函数上。</p>
<p>和变量一样，函数在两种语言中都需要先声明才能使用。Go语言使用func关键字作为<strong>函数声明的前缀</strong>，并且函数返回值列表放在函数声明的最后。但在C语言中，函数声明无需任何关键字作为前缀，函数只支持单一返回值，并且返回值类型放在函数名的前面，如下所示：</p>
<pre><code>Go：
func Add(a, b int) int {
    return a+b
}

vs.

C：
int Add(int a, int b) {
    return a+b;
}
</code></pre>
<p><strong>第三，记得加上代码行结尾的分号</strong></p>
<p>我们日常编写Go代码时，<strong>极少手写分号</strong>。这是因为，Go设计者当初为了简化代码编写，提高代码可读性，选择了<strong>由编译器在词法分析阶段自动在适当位置插入分号的技术路线</strong>。如果你是一个被Go编译器惯坏了的Gopher，来到C语言的世界后，一定不要忘记代码行尾的分号。比如上面例子中的C语言Add函数实现，在return语句后面记得要手动加上分号。</p>
<p><strong>第四，补上“省略”的括号</strong></p>
<p>同样是出于简化代码、增加可读性的考虑，Go设计者最初就取消掉了条件分支语句（if）、选择分支语句（switch）和循环控制语句（for）中条件表达式外围的小括号：</p>
<pre><code>// Go代码
func f() int {
    return 5
}
func main() {
    a := 1
    if a == 1 { // 无需小括号包裹条件表达式
        fmt.Println(a)
    }

    switch b := f(); b { // 无需小括号包裹条件表达式
    case 4:
        fmt.Println("b = 4")
    case 5:
        fmt.Println("b = 5")
    default:
        fmt.Println("b = n/a")
    }

    for i := 1; i &lt; 10; i++ { // 无需小括号包裹循环语句的循环表达式
        a += i
    }
    fmt.Println(a)
}
</code></pre>
<p>这一点恰恰与C语言“背道而驰”。因此，我们在使用C语言编写代码时，务必要想着补上这些括号：</p>
<pre><code>// C代码
int f() {
        return 5;
}

int main() {
    int a = 1;
    if (a == 1) { // 需用小括号包裹条件表达式
        printf("%d\n", a);
    }

    int b = f();
    switch (b) { // 需用小括号包裹条件表达式
    case 4:
        printf("b = 4\n");
        break;
    case 5:
        printf("b = 5\n");
        break;
    default:
        printf("b = n/a\n");
    }

    int i = 0;
    for (i = 1; i &lt; 10; i++) { // 需用小括号包裹循环语句的循环表达式
        a += i;
    }
    printf("%d\n", a);
}
</code></pre>
<p><strong>第五，留意C与Go导出符号的不同机制</strong></p>
<p>C语言通过头文件来声明对外可见的符号，所以我们不用管符号是不是首字母大写的。但在Go中，只有首字母大写的包级变量、常量、类型、函数、方法才是可导出的，即对外部包可见。反之，首字母小写的则为包私有的，仅在包内使用。Gopher一旦习惯了这样的规则，在切换到C语言时，就会产生“心理后遗症”：遇到在其他头文件中定义的首字母小写的函数时，总以为不能直接使用。</p>
<p><strong>第六，记得在switch case语句中添加break</strong></p>
<p>C 语言与Go语言在选择分支语句的语义方面有所不同：C语言的 case 语句中，如果没有显式加入break语句，那么代码将向下自动掉落执行。而Go在最初设计时就重新规定了switch case的语义，默认不自动掉落（fallthrough），除非开发者显式使用fallthrough关键字。</p>
<p>适应了Go的switch case语句的语义后再回来写C代码，就会存在潜在的“风险”。我们来看一个例子：</p>
<pre><code>// C代码：
int main() {
    int a = 1;
    switch(a) {
        case 1:printf("a = 1\n");
        case 2:printf("a = 2\n");
        case 3:printf("a = 3\n");
        default:printf("a = ?\n");
    }
}
</code></pre>
<p>这段代码是按Go语义编写的switch case，编译运行后得到的结果如下：</p>
<pre><code>a = 1
a = 2
a = 3
a = ?
</code></pre>
<p>这显然不符合我们输出“a = 1”的预期。对于初学C的Gopher而言，这个问题影响还是蛮大的，因为这样编写的代码在C编译器眼中是完全合法的，但所代表的语义却完全不是开发人员想要的。这样的程序一旦流入到生产环境，其缺陷可能会引发生产故障。</p>
<p>一些Clint 工具可以检测出这样的问题，因此对于写C代码的Gopher，我建议在提交代码前使用lint工具对代码做一下检查。</p>
<h3>4. 构建机制</h3>
<p>Go与C都是静态编译型语言，它们的源码需要经过编译器和链接器处理，这个过程称为<strong>构建(build)</strong>，构建后得到的可执行文件才是最终交付给用户的成果物。</p>
<p>和Go语言略有不同的是，C语言的构建还有一个预处理（pre-processing）阶段，预处理环节的输出才是C编译器的真正输入。C语言中的宏就是在预处理阶段展开的。不过，Go没有预处理阶段。</p>
<p>C语言的编译单元是一个C源文件（.c），每个编译单元在编译过程中会对应生成一个目标文件（.o/.obj），最后链接器将这些目标文件链接在一起，形成可执行文件。</p>
<p>而Go则是以一个包（package）为编译单元的，每个包内的源文件生成一个.o文件，一个包的所有.o文件聚合（archive）成一个.a文件，链接器将这些目标文件链接在一起形成可执行文件。</p>
<p>Go语言提供了统一的Go命令行工具链，且Go编译器原生支持增量构建，源码构建过程不需要Gopher手工做什么配置。但在C语言的世界中，用于构建C程序的工具有很多，主流的包括gcc/clang，以及微软平台的C编译器。这些编译器原生不支持增量构建，为了提升工程级构建的效率，避免每次都进行全量构建，我们通常会使用第三方的构建管理工具，比如make（Makefile）或CMake。考虑移植性时，我们还会使用到configure文件，用于在目标机器上收集和设置编译器所需的环境信息。</p>
<h3>5. 依赖管理</h3>
<p>我在前面提过，C语言仅提供了一个“小内核”。像依赖管理这类的事情，C语言本身并没有提供跟Go中的Go Module类似的，统一且相对完善的解决方案。在C语言的世界中，我们依然要靠外部工具（比如CMake）来管理第三方的依赖。</p>
<p>C语言的第三方依赖通常以静态库（.a）或动态共享库（.so）的形式存在。如果你的应用要使用静态链接，那就必须在系统中为C编译器提供第三方依赖的静态库文件。但在实际工作中，完全采用静态链接有时是会遇到麻烦的。这是因为，很多操作系统在默认安装时是不带开发包的，也就是说，像 libc、libpthread 这样的系统库只提供了动态共享库版本（如/lib下提供了libc的共享库libc.so.6），其静态库版本是需要自行下载、编译和安装的（如libc的静态库libc.a在安装后是放在/usr/lib下面的)。所以<strong>多数情况下，我们是将****静态、动态****两种链接方式混合在一起使用的</strong>，比如像libc这样的系统库多采用动态链接。</p>
<p>动态共享库通常是有版本的，并且按照一定规则安装到系统中。举个例子，一个名为libfoo的动态共享库，在安装的目录下文件集合通常是这样：</p>
<pre><code>2022-03-10 12:28 libfoo.so -&gt; libfoo.so.0.0.0*
2022-03-10 12:28 libfoo.so.0 -&gt; libfoo.so.0.0.0*
2022-03-10 12:28 libfoo.so.0.0.0*
</code></pre>
<p>按惯例，每个动态共享库都有多个名字属性，包括real name、soname和linker name。下面我们来分别看下。</p>
<ul>
<li>real name：实际包含共享库代码的那个文件的名字(如上面例子中的libfoo.so.0.0.0)。动态共享库的真实版本信息就在real name中，显然real name中的版本号符合<a href="https://semver.org/">语义版本规范</a>，即major.minor.patch。当两个版本的major号一致，说明是向后兼容的两个版本；</li>
<li>soname：shared object name的缩写，也是这三个名字中最重要的一个。无论是在编译阶段还是在运行阶段，系统链接器都是通过动态共享库的soname（如上面例子中的libfoo.so.0）来唯一识别共享库的。我们看到的soname实际上是仅包含major号的共享库名字；</li>
<li>linker name：编译阶段提供给编译器的名字（如上面例子中的libfoo.so）。如果你构建的共享库的real name跟上面例子中libfoo.so.0.0.0类似，带有版本号，那么你在编译器命令中直接使用-L path -lfoo是无法让链接器找到对应的共享库文件的，除非你为libfoo.so.0.0.0提供了一个linker name（如libfoo.so，一个指向libfoo.so.0.0.0的符号链接）。linker name一般在共享库安装时手工创建。<br />
动态共享库有了这三个名称属性，依赖管理就有了依据。但由于在链接的时候使用的是linker name，而linker name并不带有版本号，真实版本与主机环境有关，因此要实现C应用的可重现构建还是比较难。在实践中，我们通常会使用专门的构建主机，项目组将该主机上的依赖管理起来，进而保证每次构建所使用的依赖版本是可控的。同时，应用部署的目标主机上的依赖版本也应该得到管理，避免运行时出现动态共享库版本不匹配的问题。</li>
</ul>
<h3>6. 代码风格</h3>
<p>Go语言是历史上首次实现了代码风格全社区统一的编程语言。它基本上消除了开发人员在代码风格上的无休止的、始终无法达成一致的争论，以及不同代码风格带来的阅读、维护他人代码时的低效。gofmt工具格式化出来的代码风格已经成为Go开发者的一种共识，融入到Go语言的开发文化当中了。所以，如果你让某个Go开发者说说gofmt后的代码风格是什么样的，多数Go开发者可能说不出，因为代码会被gofmt自动变成那种风格，大家已经不再关心风格了。</p>
<p>而在C语言的世界，代码风格仍存争议。但经过多年的演进，以及像Go这样新兴语言的不断“教育”，C社区也在尝试进行这方面的改进，涌现出了像<a href="https://clang.llvm.org/docs/ClangFormat.html">clang-format</a>这样的工具。目前，虽然还没有在全社区达成一致的代码风格（由于历史原因，这很难做到），但已经可以减少很多不必要的争论。</p>
<p>对于正在学习C语言，并进行C编码实践的Gopher，我的建议是：<strong>不要拘泥于使用什么代码风格，先用clang-format，并确定一套风格模板就好</strong>。</p>
<h2>四. 小结</h2>
<p>作为一名对Go跟随和研究了近十年的程序员，我深刻体会到，Go的简单性、性能和生产力使它成为了创建面向用户的应用程序和服务的理想语言。快速的迭代让团队能够快速地作出反应，以满足用户不断变化的需求，让团队可以将更多精力集中在保持灵活性上。</p>
<p>但Go也有缺点，比如缺少对内存以及一些低级操作的精确控制，而C语言恰好可以弥补这个缺陷。C 语言提供的更精细的控制允许更多的精确性，使得C成为低级操作的理想语言。这些低级操作不太可能发生变化，并且C相比Go还提高了性能。所以，如果你是一个有性能与低级操作需求的 Gopher ，就有充分的理由来学习C语言。</p>
<p>C 的优势体现在最接近底层机器的地方，而Go的优势在离用户较近的地方能得到最大发挥。当然，这并不是说两者都不能在对方的空间里工作，但这样做会增加“摩擦”。当你的需求从追求灵活性转变为注重效率时，用C重写库或服务的理由就更充分了。</p>
<p>总之，虽然Go和C的设计有很大的不同，但它们也有很多相似性，具备发挥兼容优势的基础。并且，当我们同时使用这二者时，就可以既有很大的灵活性，又有很好的性能，可以说是相得益彰！</p>
<h2>五. 写在最后</h2>
<p>今天的加餐中，我主要是基于C与Go的比较来讲解的，对于Go语言的特性并没有作详细展开。如果你还想进一步了解Go语言的设计哲学、语法特性、程序设计相关知识，欢迎来学习我在极客时间上的专栏<a href="http://gk.link/a/10AVZ">《Tony Bai ·Go语言第一课》</a>。在这门课里，我会用我十年Gopher的经验，带给你一条系统、完整的Go语言入门路径。</p>
<p>感谢你看到这里，如果今天的内容让你有所收获，欢迎把它分享给你的朋友。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</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><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</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; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go是否支持增量构建？我来告诉你！</title>
		<link>https://tonybai.com/2022/03/21/go-native-support-incremental-build/</link>
		<comments>https://tonybai.com/2022/03/21/go-native-support-incremental-build/#comments</comments>
		<pubDate>Mon, 21 Mar 2022 13:40:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[build-cache]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[class]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[C语言]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-build]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[GOCACHE]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[incremental-build]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JVM]]></category>
		<category><![CDATA[link]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[增量构建]]></category>
		<category><![CDATA[目标文件]]></category>
		<category><![CDATA[链接]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3472</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/03/21/go-native-support-incremental-build Go语言以编译速度快闻名于码农界。这缘于Go在设计之初就选择抛弃其祖辈C语言的头文件包含机制，选择了以包(package)作为基本编译单元。Go语言的这种以包为基本构建单元的构建模型使得依赖分析变得十分简单，避免了C语言那种通过头文件分析依赖的巨大开销。在我的《Go语言精进之路》一书中，我也给出了Go编译速度快的三点具体原因，包括： Go要求每个源文件在开头处显式地列出所有依赖的包导入，这样Go编译器不必读取和处理整个文件就可以确定其依赖的包列表； Go要求包之间不能存在循环依赖，这样一个包的依赖关系便形成了一张有向无环图。由于无环，包可以被单独编译，也可以并行编译； 已编译的Go包对应的目标文件(file_name.o或package_name.a)中不仅记录了该包本身的导出符号信息，还记录了其所依赖包的导出符号信息。这样，Go编译器在编译某包P时，针对P依赖的每个包导入(比如：导入包Q)，只需读取一个目标文件即可（比如：Q包编译成的目标文件，该目标文件中已经包含了Q包的依赖包的导出信息），而无需再读取其他文件中的信息了。 不过近期有读者问到：Go是否支持增量构建(incremental build)？这是一个好问题，书中并未提到这方面内容。但语言编译器编译速度再快，如果没有增量构建，构建大型代码工程的时间也不会短。那么Go是否支持增量构建呢？在这篇文章中，我就来告诉你答案。 1. 什么是增量构建？ 提到构建(build)，我们通常所指的是静态编译型语言，比如：C、Go、Java等。Python等动态语言不需要构建，直接用解释器run即可。每种静态编译型编程语言通常都有自己的编译单元，比如Go的编译单元为一个package，c/c++的编译单元是一个c/c++源文件，java则以class为编译单元等。静态语言的构建就是将编译单元的源码编译为对应的中间目标文件(.o/.a/.class)，然后将这些目标文件通过链接器链接在一起形成最终可执行文件的过程。不过Java除外，java在编译过程没有链接环节，jvm加载class文件时会有一个链接过程。 那么问题来了：每次项目构建，项目中的所有源文件都要被重新编译一遍而形成新的中间目标文件吗？如果我只改动了一个源文件中的几行代码，项目中的其他源文件也要跟着重新编译一遍么？我们显然不希望这样浪费算力、浪费开发者时间的事情发生！ 为了避免这样的事情发生，“增量构建”被提了出来。简单来说就是每次构建仅重新编译变动了的编译单元以及对这些变动的编译单元有依赖的编译单元的源码！ 上图展示了一个项目的编译单元的依赖关系。当开发人员修改了编译单元C的源码后，如果该项目支持增量编译，那么再次构建这个项目时，仅变动的编译单元C的源码以及直接依赖C的B、间接依赖C的A会被重新编译，而D、E两个编译单元不会被重新编译，其中间目标文件会被链接器重用。 对增量编译的支持，有两种策略：一种是编程语言的编译器自身就支持，比如Rust。另外一种则是语言自身编译器不支持，需要通过第三方项目构建管理工具协助实现，最典型的就是C/C++与Make/CMake的组合。 那么Go语言的编译器go compiler(gc)是否本身就支持增量编译呢？是否需要通过外部项目构建管理工具协助呢？我们继续往下看。 2. 通过示例看Go是否支持增量构建 Go语言提供了统一的go工具链，在这个工具链中用于构建的命令只有一个，那就是go build。下面我们就通过一系列实例来验证一下Go是否原生支持增量构建。 该示例的项目结构如下： demo1/ ├── go.mod ├── main.go ├── pkg1/ │   └── pkg1.go └── pkg2/ └── pkg2.go a) 首次构建 在这个项目中，顶层的module为demo1，main包依赖pkg1包与pkg2包。我们先通过go build命令对该项目做首次构建，我们通过命令行参数-x -v输出构建的详细日志，以便于我们分析： $go build -x -v ### 笔者注：创建临时目录用于此次构建 WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build1907281507 cd /Users/tonybai/test/go git status --porcelain cd [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-native-support-incremental-build-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/03/21/go-native-support-incremental-build">本文永久链接</a> &#8211; https://tonybai.com/2022/03/21/go-native-support-incremental-build</p>
<p>Go语言<strong>以编译速度快闻名于码农界</strong>。这缘于Go在设计之初就选择抛弃其祖辈C语言的头文件包含机制，选择了以包(package)作为基本编译单元。Go语言的这种以包为基本构建单元的构建模型使得依赖分析变得十分简单，避免了C语言那种通过头文件分析依赖的巨大开销。在我的<a href="https://mp.weixin.qq.com/s/h-lsWUbKRPnzFLINub5uYw">《Go语言精进之路》</a>一书中，我也给出了Go编译速度快的三点具体原因，包括：</p>
<ul>
<li>Go要求每个源文件在开头处显式地列出所有依赖的包导入，这样Go编译器不必读取和处理整个文件就可以确定其依赖的包列表；</li>
<li>Go要求包之间不能存在循环依赖，这样一个包的依赖关系便形成了一张有向无环图。由于无环，包可以被单独编译，也可以并行编译；</li>
<li>已编译的Go包对应的目标文件(file_name.o或package_name.a)中不仅记录了该包本身的导出符号信息，还记录了其所依赖包的导出符号信息。这样，Go编译器在编译某包P时，针对P依赖的每个包导入(比如：导入包Q)，只需读取一个目标文件即可（比如：Q包编译成的目标文件，该目标文件中已经包含了Q包的依赖包的导出信息），而无需再读取其他文件中的信息了。 </li>
</ul>
<p>不过近期有读者问到：<strong>Go是否支持增量构建(incremental build)</strong>？这是一个好问题，书中并未提到这方面内容。但语言编译器编译速度再快，如果没有增量构建，构建大型代码工程的时间也不会短。那么Go是否支持增量构建呢？在这篇文章中，我就来告诉你答案。</p>
<h3>1. 什么是增量构建？</h3>
<p>提到构建(build)，我们通常所指的是静态编译型语言，比如：C、Go、Java等。Python等动态语言不需要构建，直接用解释器run即可。每种静态编译型编程语言通常都有自己的编译单元，比如Go的编译单元为一个package，c/c++的编译单元是一个c/c++源文件，java则以class为编译单元等。<strong>静态语言的构建就是将编译单元的源码编译为对应的中间目标文件(.o/.a/.class)，然后将这些目标文件通过链接器链接在一起形成最终可执行文件的过程</strong>。不过Java除外，java在编译过程没有链接环节，jvm加载class文件时会有一个链接过程。</p>
<p>那么问题来了：每次项目构建，项目中的所有源文件都要被重新编译一遍而形成新的中间目标文件吗？如果我只改动了一个源文件中的几行代码，项目中的其他源文件也要跟着重新编译一遍么？我们显然不希望这样浪费算力、浪费开发者时间的事情发生！</p>
<p>为了避免这样的事情发生，“增量构建”被提了出来。简单来说就是<strong>每次构建仅重新编译变动了的编译单元以及对这些变动的编译单元有依赖的编译单元的源码</strong>！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-native-support-incremental-build-2.png" alt="" /></p>
<p>上图展示了一个项目的编译单元的依赖关系。当开发人员修改了编译单元C的源码后，如果该项目支持增量编译，那么再次构建这个项目时，仅变动的编译单元C的源码以及直接依赖C的B、间接依赖C的A会被重新编译，而D、E两个编译单元不会被重新编译，其中间目标文件会被链接器重用。</p>
<p>对增量编译的支持，有两种策略：一种是编程语言的编译器自身就支持，比如Rust。另外一种则是语言自身编译器不支持，需要通过第三方项目构建管理工具协助实现，最典型的就是C/C++与Make/CMake的组合。</p>
<p>那么Go语言的编译器go compiler(gc)是否本身就支持增量编译呢？是否需要通过外部项目构建管理工具协助呢？我们继续往下看。</p>
<h3>2. 通过示例看Go是否支持增量构建</h3>
<p>Go语言提供了统一的go工具链，在这个工具链中用于构建的命令只有一个，那就是go build。下面我们就通过一系列实例来验证一下Go是否原生支持增量构建。</p>
<p>该示例的项目结构如下：</p>
<pre><code>demo1/
├── go.mod
├── main.go
├── pkg1/
│   └── pkg1.go
└── pkg2/
    └── pkg2.go
</code></pre>
<h4>a) 首次构建</h4>
<p>在这个项目中，顶层的module为demo1，main包依赖pkg1包与pkg2包。我们先通过go build命令对该项目做首次构建，我们通过命令行参数-x -v输出构建的详细日志，以便于我们分析：</p>
<pre><code>$go build -x -v 

### 笔者注：创建临时目录用于此次构建

WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build1907281507
cd /Users/tonybai/test/go
git status --porcelain
cd /Users/tonybai/test/go
git show -s --no-show-signature --format=%H:%ct
demo1/pkg2
demo1/pkg1
mkdir -p $WORK/b003/
mkdir -p $WORK/b002/
cat &gt;$WORK/b003/importcfg &lt;&lt; 'EOF' # internal
# import config
EOF

### 笔者注：编译demo1/pkg1和demo1/pkg2包

cd /Users/tonybai/test/go/incremental-build/demo1
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b003/_pkg_.a -trimpath "$WORK/b003=&gt;" -p demo1/pkg2 -lang=go1.18 -complete -buildid 4ixic55Fpug9OyS7vsew/4ixic55Fpug9OyS7vsew -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b003/importcfg -pack ./pkg2/pkg2.go
cat &gt;$WORK/b002/importcfg &lt;&lt; 'EOF' # internal
# import config
EOF
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b002/_pkg_.a -trimpath "$WORK/b002=&gt;" -p demo1/pkg1 -lang=go1.18 -complete -buildid jgyT36iBuu6-dYIzK5SD/jgyT36iBuu6-dYIzK5SD -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b002/importcfg -pack ./pkg1/pkg1.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b003/_pkg_.a # internal
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b002/_pkg_.a # internal

### 笔者注：将编译demo1/pkg1和demo1/pkg2包得到的目标文件缓存到gocache中

cp $WORK/b003/_pkg_.a /Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d # internal
cp $WORK/b002/_pkg_.a /Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d # internal

runtime
mkdir -p $WORK/b004/
cat &gt;$WORK/b004/go_asm.h &lt;&lt; 'EOF' # internal
EOF
cd /Users/tonybai/.bin/go1.18rc1/src/runtime
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -gensymabis -o $WORK/b004/symabis ./asm.s ./asm_amd64.s ./duff_amd64.s ./memclr_amd64.s ./memmove_amd64.s ./preempt_amd64.s ./rt0_darwin_amd64.s ./sys_darwin_amd64.s
cat &gt;$WORK/b004/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a
packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a
packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a
packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a
packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a
packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a
packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a
EOF

### 笔者注：由于笔者在执行build前使用go clean -cache将所有cache清空，因此这里go build会重新编译Go运行时库并缓存到gocache中

cd /Users/tonybai/test/go/incremental-build/demo1
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b004/_pkg_.a -trimpath "$WORK/b004=&gt;" -p runtime -std -+ -buildid cjuCOFTfsWmpOEnkAPsP/cjuCOFTfsWmpOEnkAPsP -goversion go1.18rc1 -symabis $WORK/b004/symabis -c=4 -nolocalimports -importcfg $WORK/b004/importcfg -pack -asmhdr $WORK/b004/go_asm.h /Users/tonybai/.bin/go1.18rc1/src/runtime/alg.go /Users/tonybai/.bin/go1.18rc1/src/runtime/asan0.go /Users/tonybai/.bin/go1.18rc1/src/runtime/atomic_pointer.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cgo.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cgocall.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cgocallback.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cgocheck.go /Users/tonybai/.bin/go1.18rc1/src/runtime/chan.go /Users/tonybai/.bin/go1.18rc1/src/runtime/checkptr.go /Users/tonybai/.bin/go1.18rc1/src/runtime/compiler.go /Users/tonybai/.bin/go1.18rc1/src/runtime/complex.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cpuflags.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cpuflags_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cpuprof.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cputicks.go /Users/tonybai/.bin/go1.18rc1/src/runtime/debug.go /Users/tonybai/.bin/go1.18rc1/src/runtime/debugcall.go /Users/tonybai/.bin/go1.18rc1/src/runtime/debuglog.go /Users/tonybai/.bin/go1.18rc1/src/runtime/debuglog_off.go /Users/tonybai/.bin/go1.18rc1/src/runtime/defs_darwin_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/env_posix.go /Users/tonybai/.bin/go1.18rc1/src/runtime/error.go /Users/tonybai/.bin/go1.18rc1/src/runtime/extern.go /Users/tonybai/.bin/go1.18rc1/src/runtime/fastlog2.go /Users/tonybai/.bin/go1.18rc1/src/runtime/fastlog2table.go /Users/tonybai/.bin/go1.18rc1/src/runtime/float.go /Users/tonybai/.bin/go1.18rc1/src/runtime/hash64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/heapdump.go /Users/tonybai/.bin/go1.18rc1/src/runtime/histogram.go /Users/tonybai/.bin/go1.18rc1/src/runtime/iface.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lfstack.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lfstack_64bit.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lock_sema.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lockrank.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lockrank_off.go /Users/tonybai/.bin/go1.18rc1/src/runtime/malloc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/map.go /Users/tonybai/.bin/go1.18rc1/src/runtime/map_fast32.go /Users/tonybai/.bin/go1.18rc1/src/runtime/map_fast64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/map_faststr.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mbarrier.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mbitmap.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mcache.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mcentral.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mcheckmark.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mem_darwin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/metrics.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mfinal.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mfixalloc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcmark.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcpacer.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcscavenge.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcstack.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcsweep.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcwork.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mheap.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mpagealloc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mpagealloc_64bit.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mpagecache.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mpallocbits.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mprof.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mranges.go /Users/tonybai/.bin/go1.18rc1/src/runtime/msan0.go /Users/tonybai/.bin/go1.18rc1/src/runtime/msize.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mspanset.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mstats.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mwbbuf.go /Users/tonybai/.bin/go1.18rc1/src/runtime/nbpipe_pipe.go /Users/tonybai/.bin/go1.18rc1/src/runtime/netpoll.go /Users/tonybai/.bin/go1.18rc1/src/runtime/netpoll_kqueue.go /Users/tonybai/.bin/go1.18rc1/src/runtime/os_darwin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/os_nonopenbsd.go /Users/tonybai/.bin/go1.18rc1/src/runtime/panic.go /Users/tonybai/.bin/go1.18rc1/src/runtime/plugin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/preempt.go /Users/tonybai/.bin/go1.18rc1/src/runtime/preempt_nonwindows.go /Users/tonybai/.bin/go1.18rc1/src/runtime/print.go /Users/tonybai/.bin/go1.18rc1/src/runtime/proc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/profbuf.go /Users/tonybai/.bin/go1.18rc1/src/runtime/proflabel.go /Users/tonybai/.bin/go1.18rc1/src/runtime/race0.go /Users/tonybai/.bin/go1.18rc1/src/runtime/rdebug.go /Users/tonybai/.bin/go1.18rc1/src/runtime/relax_stub.go /Users/tonybai/.bin/go1.18rc1/src/runtime/runtime.go /Users/tonybai/.bin/go1.18rc1/src/runtime/runtime1.go /Users/tonybai/.bin/go1.18rc1/src/runtime/runtime2.go /Users/tonybai/.bin/go1.18rc1/src/runtime/rwmutex.go /Users/tonybai/.bin/go1.18rc1/src/runtime/select.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sema.go /Users/tonybai/.bin/go1.18rc1/src/runtime/signal_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/signal_darwin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/signal_darwin_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/signal_unix.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sigqueue.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sizeclasses.go /Users/tonybai/.bin/go1.18rc1/src/runtime/slice.go /Users/tonybai/.bin/go1.18rc1/src/runtime/softfloat64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/stack.go /Users/tonybai/.bin/go1.18rc1/src/runtime/string.go /Users/tonybai/.bin/go1.18rc1/src/runtime/stubs.go /Users/tonybai/.bin/go1.18rc1/src/runtime/stubs_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/stubs_nonlinux.go /Users/tonybai/.bin/go1.18rc1/src/runtime/symtab.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sys_darwin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sys_libc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sys_nonppc64x.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sys_x86.go /Users/tonybai/.bin/go1.18rc1/src/runtime/time.go /Users/tonybai/.bin/go1.18rc1/src/runtime/time_nofake.go /Users/tonybai/.bin/go1.18rc1/src/runtime/timestub.go /Users/tonybai/.bin/go1.18rc1/src/runtime/tls_stub.go /Users/tonybai/.bin/go1.18rc1/src/runtime/trace.go /Users/tonybai/.bin/go1.18rc1/src/runtime/traceback.go /Users/tonybai/.bin/go1.18rc1/src/runtime/type.go /Users/tonybai/.bin/go1.18rc1/src/runtime/typekind.go /Users/tonybai/.bin/go1.18rc1/src/runtime/utf8.go /Users/tonybai/.bin/go1.18rc1/src/runtime/vdso_in_none.go /Users/tonybai/.bin/go1.18rc1/src/runtime/write_err.go
cd /Users/tonybai/.bin/go1.18rc1/src/runtime
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/asm.o ./asm.s
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/asm_amd64.o ./asm_amd64.s
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/duff_amd64.o ./duff_amd64.s
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/memclr_amd64.o ./memclr_amd64.s
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/memmove_amd64.o ./memmove_amd64.s
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/preempt_amd64.o ./preempt_amd64.s
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/rt0_darwin_amd64.o ./rt0_darwin_amd64.s
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath "$WORK/b004=&gt;" -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/sys_darwin_amd64.o ./sys_darwin_amd64.s
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/pack r $WORK/b004/_pkg_.a $WORK/b004/asm.o $WORK/b004/asm_amd64.o $WORK/b004/duff_amd64.o $WORK/b004/memclr_amd64.o $WORK/b004/memmove_amd64.o $WORK/b004/preempt_amd64.o $WORK/b004/rt0_darwin_amd64.o $WORK/b004/sys_darwin_amd64.o # internal
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b004/_pkg_.a # internal
cp $WORK/b004/_pkg_.a /Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d # internal
demo1
mkdir -p $WORK/b001/
cat &gt;$WORK/b001/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile demo1/pkg1=$WORK/b002/_pkg_.a
packagefile demo1/pkg2=$WORK/b003/_pkg_.a
packagefile runtime=$WORK/b004/_pkg_.a
EOF

### 笔者注：编译main包并缓存

cd /Users/tonybai/test/go/incremental-build/demo1
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=&gt;" -p main -lang=go1.18 -complete -buildid ZhPqHmBh6WQ6HFsDI1Yh/ZhPqHmBh6WQ6HFsDI1Yh -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/e8/e86257379cbdd59856f799594b63f3bb33ae89011955fee50e6fe90d3809ce5a-d # internal
cat &gt;$WORK/b001/importcfg.link &lt;&lt; 'EOF' # internal
packagefile demo1=$WORK/b001/_pkg_.a
packagefile demo1/pkg1=$WORK/b002/_pkg_.a
packagefile demo1/pkg2=$WORK/b003/_pkg_.a
packagefile runtime=$WORK/b004/_pkg_.a
packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a
packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a
packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a
packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a
packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a
packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a
packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tdemo1\nmod\tdemo1\t(devel)\t\nbuild\t-compiler=gc\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=darwin\nbuild\tGOAMD64=v1\nbuild\tvcs=git\nbuild\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\nbuild\tvcs.time=2022-03-12T13:52:57Z\nbuild\tvcs.modified=true\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .

### 笔者注：执行链接过程

/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=mzN3WRwHiNhsESy6r89L/ZhPqHmBh6WQ6HFsDI1Yh/Nvx0U2gM2zWzj7FTESXk/mzN3WRwHiNhsESy6r89L -extld=clang $WORK/b001/_pkg_.a
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal

### 笔者注：将构建出来的可执行文件放到正确位置并改名
mv $WORK/b001/exe/a.out demo1
rm -r $WORK/b001/
</code></pre>
<h4>b) 删除可执行文件后，再次构建</h4>
<p>接下来我们删除之前构建出来的可执行文件demo1，然后再执行一次go build：</p>
<pre><code>$go build -x -v
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3889005616
cd /Users/tonybai/test/go
git status --porcelain
cd /Users/tonybai/test/go
git show -s --no-show-signature --format=%H:%ct
mkdir -p $WORK/b001/
cat &gt;$WORK/b001/importcfg.link &lt;&lt; 'EOF' # internal

### 笔者注：这次构建直接使用了上一次缓存的各个包的缓存结果 

packagefile demo1=/Users/tonybai/Library/Caches/go-build/e8/e86257379cbdd59856f799594b63f3bb33ae89011955fee50e6fe90d3809ce5a-d
packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d
packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d
packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d

packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a
packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a
packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a
packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a
packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a
packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a
packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tdemo1\nmod\tdemo1\t(devel)\t\nbuild\t-compiler=gc\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=darwin\nbuild\tGOAMD64=v1\nbuild\tvcs=git\nbuild\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\nbuild\tvcs.time=2022-03-12T13:52:57Z\nbuild\tvcs.modified=true\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=mzN3WRwHiNhsESy6r89L/ZhPqHmBh6WQ6HFsDI1Yh/Nvx0U2gM2zWzj7FTESXk/mzN3WRwHiNhsESy6r89L -extld=clang /Users/tonybai/Library/Caches/go-build/e8/e86257379cbdd59856f799594b63f3bb33ae89011955fee50e6fe90d3809ce5a-d
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out demo1
rm -r $WORK/b001/
</code></pre>
<p>通过go build命令输出的日志我们看到：go build并没有重新编译各个包中的源文件，而是直接使用上一次构建缓存在cache中的demo1、demo1/pkg1和demo1/pkg2进行链接并输出最终可执行文件。初步判断，Go编译器是可以识别出项目中的源文件是否发生了改变并决定是否对其重新编译的。</p>
<h4>c) 新增pkg3</h4>
<p>我们为demo1新增pkg3，并在main.go中调用pkg3包中的函数，相当于建立了一个对pkg3的依赖，然后我们再来build一下该项目：</p>
<pre><code>$go build -x -v
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3890553968
cd /Users/tonybai/test/go
git status --porcelain
cd /Users/tonybai/test/go
git show -s --no-show-signature --format=%H:%ct
demo1/pkg3
mkdir -p $WORK/b004/
cat &gt;$WORK/b004/importcfg &lt;&lt; 'EOF' # internal
# import config
EOF

### 笔者注：构建pkg3

cd /Users/tonybai/test/go/incremental-build/demo1
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b004/_pkg_.a -trimpath "$WORK/b004=&gt;" -p demo1/pkg3 -lang=go1.18 -complete -buildid yVeHBkrjxeJ1Ib-jc5Fu/yVeHBkrjxeJ1Ib-jc5Fu -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b004/importcfg -pack ./pkg3/pkg3.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b004/_pkg_.a # internal
cp $WORK/b004/_pkg_.a /Users/tonybai/Library/Caches/go-build/2c/2c02674d62c50d4f2b8439c9314ef51b3e211d45d4114fa495fdd0e20c43440d-d # internal
demo1
mkdir -p $WORK/b001/
cat &gt;$WORK/b001/importcfg &lt;&lt; 'EOF' # internal
# import config

### 笔者注：直接重用demo1/pkg1和demo1/pkg2在cache中的目标文件

packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d
packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d
packagefile demo1/pkg3=$WORK/b004/_pkg_.a
packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d
EOF

### 笔者注：重新编译main.go

/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=&gt;" -p main -lang=go1.18 -complete -buildid Jii_iiylmm9d82X_Mzem/Jii_iiylmm9d82X_Mzem -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/52/52b5b7e233ac17201702c26f1da97c5a23e42e68f74040d576905323a016f66e-d # internal
cat &gt;$WORK/b001/importcfg.link &lt;&lt; 'EOF' # internal
packagefile demo1=$WORK/b001/_pkg_.a
packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d
packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d
packagefile demo1/pkg3=$WORK/b004/_pkg_.a
packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d
packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a
packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a
packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a
packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a
packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a
packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a
packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tdemo1\nmod\tdemo1\t(devel)\t\nbuild\t-compiler=gc\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=darwin\nbuild\tGOAMD64=v1\nbuild\tvcs=git\nbuild\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\nbuild\tvcs.time=2022-03-12T13:52:57Z\nbuild\tvcs.modified=true\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=GM4wTB4eDZmuIuIaQgup/Jii_iiylmm9d82X_Mzem/5aVh7LKgEkk3c4g5_WBq/GM4wTB4eDZmuIuIaQgup -extld=clang $WORK/b001/_pkg_.a
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out demo1
rm -r $WORK/b001/
</code></pre>
<p>我们看到Go只是编译了新增的pkg3以及依赖pkg3的main.go，pkg1和pkg2包并未被重新编译，而是直接使用了缓存在gocache中的中间目标文件。</p>
<h4>d) 重新编译单个变更的源文件还是重新编译整个包？</h4>
<p>如果一个go package包含多个源文件，当某一个源文件发生内容变化时，go编译器是只会编译该源文件还是整个包呢？我们来验证一下。</p>
<p>我们为pkg3添加另外一个源文件pkg3_1.go，然后做一次构建。之后再修改pkg3_1.go，再做构建：</p>
<pre><code>$go build -x -v
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build213842995
cd /Users/tonybai/test/go
git status --porcelain
cd /Users/tonybai/test/go
git show -s --no-show-signature --format=%H:%ct
demo1/pkg3
mkdir -p $WORK/b004/
cat &gt;$WORK/b004/importcfg &lt;&lt; 'EOF' # internal
# import config
EOF
cd /Users/tonybai/test/go/incremental-build/demo1

### 笔者注：编译pkg3包

/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b004/_pkg_.a -trimpath "$WORK/b004=&gt;" -p demo1/pkg3 -lang=go1.18 -complete -buildid pX9UOIUBAZfMKmMgHv3q/pX9UOIUBAZfMKmMgHv3q -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b004/importcfg -pack ./pkg3/pkg3.go ./pkg3/pkg3_1.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b004/_pkg_.a # internal
cp $WORK/b004/_pkg_.a /Users/tonybai/Library/Caches/go-build/0c/0c3ce444d214c6c2999ba01b01eb4888c7864947d88bfcf63a41db4ac44002c2-d # internal
demo1
mkdir -p $WORK/b001/
cat &gt;$WORK/b001/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d
packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d
packagefile demo1/pkg3=$WORK/b004/_pkg_.a
packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d
EOF

### 笔者注：编译依赖pkg3包的main.go

/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=&gt;" -p main -lang=go1.18 -complete -buildid cQBq3r5n1_wurKrb8Xmq/cQBq3r5n1_wurKrb8Xmq -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/e2/e2818b14455f4dd54caf5f731c7b3b6b8254a37a8912e73c33b327771069bde7-d # internal
cat &gt;$WORK/b001/importcfg.link &lt;&lt; 'EOF' # internal
packagefile demo1=$WORK/b001/_pkg_.a
packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d
packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d
packagefile demo1/pkg3=$WORK/b004/_pkg_.a
packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d
packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a
packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a
packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a
packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a
packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a
packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a
packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tdemo1\nmod\tdemo1\t(devel)\t\nbuild\t-compiler=gc\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=darwin\nbuild\tGOAMD64=v1\nbuild\tvcs=git\nbuild\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\nbuild\tvcs.time=2022-03-12T13:52:57Z\nbuild\tvcs.modified=true\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=qvFsK1K1jm8CeANRL3a2/cQBq3r5n1_wurKrb8Xmq/BB3nEx0b9edm7IM0XGAQ/qvFsK1K1jm8CeANRL3a2 -extld=clang $WORK/b001/_pkg_.a
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out demo1
rm -r $WORK/b001/
</code></pre>
<p>我们看到：虽然只修改了pkg3包下面的源文件pkg3_1.go，但go build还是会将整个包的所有源文件都重新编译一次。依赖pkg3包的main.go也会被随之重新编译。就此可以证实，<strong>Go的增量编译是以Go包为基本单位的，而不是以单个源文件为单位的</strong>。这与go工具缓存在gocache中的中间目标文件(pkg.a)<strong>以包为单位的</strong>是一致的。</p>
<h4>e) 当间接依赖的包发生了变动</h4>
<p>前面的示例展示的都是直接依赖包发生变动后，增量构建涵盖的编译单元范畴。如果某个包的间接依赖包发生变化，该包是否会参与增量构建呢？答案是肯定的。我们继续用示例来证明一下。</p>
<p>我们为该demo1项目增加pkg4包，并使得pkg3依赖pkg4。这样就会出现main.go直接依赖pkg3包，间接依赖pkg4包的情况。我们在添加完pkg4包后，进行一次构建。之后修改pkg4包的部分内容，然后再执行构建，其输出日志如下：</p>
<pre><code>$go build -x -v
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build2817187631
cd /Users/tonybai/test/go
git status --porcelain
cd /Users/tonybai/test/go
git show -s --no-show-signature --format=%H:%ct
demo1/pkg4
mkdir -p $WORK/b005/
cat &gt;$WORK/b005/importcfg &lt;&lt; 'EOF' # internal
# import config
EOF

### 笔者注：编译demo1/pkg4包

cd /Users/tonybai/test/go/incremental-build/demo1
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b005/_pkg_.a -trimpath "$WORK/b005=&gt;" -p demo1/pkg4 -lang=go1.18 -complete -buildid AIv0TfCgKL2o00SexGru/AIv0TfCgKL2o00SexGru -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b005/importcfg -pack ./pkg4/pkg4.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b005/_pkg_.a # internal
cp $WORK/b005/_pkg_.a /Users/tonybai/Library/Caches/go-build/7e/7e2b8f229f8ca2f1ee315438f61cad5421bb5af9ed155e88a460faca806f4f90-d # internal
demo1/pkg3
mkdir -p $WORK/b004/
cat &gt;$WORK/b004/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile demo1/pkg4=$WORK/b005/_pkg_.a
EOF

### 笔者注：编译直接依赖demo1/pkg4包的demo1/pkg3包

/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b004/_pkg_.a -trimpath "$WORK/b004=&gt;" -p demo1/pkg3 -lang=go1.18 -complete -buildid ObVmRzLu3J1liPWzEiXx/ObVmRzLu3J1liPWzEiXx -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b004/importcfg -pack ./pkg3/pkg3.go ./pkg3/pkg3_1.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b004/_pkg_.a # internal
cp $WORK/b004/_pkg_.a /Users/tonybai/Library/Caches/go-build/4f/4f69cad7558ecf297799e29353bc802415785847c195555c22151f52abe1d9d9-d # internal
demo1
mkdir -p $WORK/b001/
cat &gt;$WORK/b001/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d
packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d
packagefile demo1/pkg3=$WORK/b004/_pkg_.a
packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d
EOF

### 笔者注：编译间接依赖demo1/pkg4包的main.go

/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=&gt;" -p main -lang=go1.18 -complete -buildid i543xzAqlwVlWgQBYhsS/i543xzAqlwVlWgQBYhsS -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/67/67a5ff80f6dbbbe01fcf9efb4d6ff380cb27fd1723b06b209a17987f1c74f425-d # internal
cat &gt;$WORK/b001/importcfg.link &lt;&lt; 'EOF' # internal
packagefile demo1=$WORK/b001/_pkg_.a
packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d
packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d
packagefile demo1/pkg3=$WORK/b004/_pkg_.a
packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d
packagefile demo1/pkg4=$WORK/b005/_pkg_.a
packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a
packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a
packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a
packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a
packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a
packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a
packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tdemo1\nmod\tdemo1\t(devel)\t\nbuild\t-compiler=gc\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=darwin\nbuild\tGOAMD64=v1\nbuild\tvcs=git\nbuild\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\nbuild\tvcs.time=2022-03-12T13:52:57Z\nbuild\tvcs.modified=true\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=5U4vzFHU8ahkEvKH4-CV/i543xzAqlwVlWgQBYhsS/GOFyatqByKmjWK1zLLiq/5U4vzFHU8ahkEvKH4-CV -extld=clang $WORK/b001/_pkg_.a
/Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out demo1
rm -r $WORK/b001/
</code></pre>
<p>我们看到：pkg4包修改后，无论是直接依赖pkg4的包，还是间接依赖pkg4的包都会在下次增量构建时被重新编译。</p>
<h3>3. 小结</h3>
<p>由上面的示例我们看到：Go编译器是原生支持增量构建的，无需第三方构建管理工具的辅助。Go的增量构建是建立在<a href="https://tonybai.com/2018/02/17/some-changes-in-go-1-10">Go 1.10引入的build cache机制</a>的基础上的。Go的增量构建以Go包为单位，当Go包中的任一源文件发生变化时，Go都会对其进行重新构建，并且会连带构建所有直接或间接依赖该包的Go包。</p>
<p>本文示例源码在<a href="https://github.com/bigwhite/experiments/tree/master/incremental-build/demo1">这里</a>可以下载。</p>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.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}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</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; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/03/21/go-native-support-incremental-build/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>C,C++开源项目中的100个Bugs</title>
		<link>https://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/</link>
		<comments>https://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/#comments</comments>
		<pubDate>Wed, 10 Apr 2013 01:59:42 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Chromium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[Code-Analyzer]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[PVS-Studio]]></category>
		<category><![CDATA[ReactOS]]></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">http://tonybai.com/?p=1247</guid>
		<description><![CDATA[俄罗斯OOO Program Verification Systems公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目，诸如Apache Http Server、Chromium、Clang、CMake、MySQL等的源码进行了分析，找出了100个典型的Bugs。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析，倒不如在编码时就提高自知自觉，避免这份列表上的错误发生在你的代码中，因此这里将部分摘录一些Bugs（Bug编号这里不连续，为的是对应原文的编号）并做简要说明。原文将这份Bug列表分为了几类，这里也将沿用这个思路。 一、数组和字符串处理错误 数组和字符串处理错误是C/C++程序中最多的一类缺陷类型。这也可以看作是我们为拥有高效地底层内存操作能力而付出的代价。 [#1]&#160;Wolfenstein 3D项目 -&#34;只有部分对象被clear了&#34; void CG_RegisterItemVisuals( int itemNum ) { &#160;&#160;&#160; &#8230; &#160;&#160;&#160; itemInfo_t *itemInfo; &#160;&#160;&#160; &#8230; &#160;&#160;&#160; memset( itemInfo, 0, sizeof( &#38;itemInfo ) ); &#160;&#160;&#160; &#8230; } 这里的Bug出现在memset那一行。代码的真实意图是clear iteminfo这块内存，但调用memset时，第三个参数传入的却是sizeof(&#38;iteminfo)，要知道 sizeof(&#38;itemInfo) != sizeof(itemInfo_t)，前者只是一个指针的大小罢了。正确的写法是： memset(itemInfo, 0, sizeof(itemInfo_t)); 或memset(itemInfo, 0, sizeof(*itemInfo)); [#2]&#160;Wolfenstein 3D项目 -&#34;只有部分Matrix被clear了&#34; ID_INLINE mat3_t::mat3_t( float src[ 3 [...]]]></description>
			<content:encoded><![CDATA[<p style="font-size: 13px;">俄罗斯<a href="http://www.viva64.com/">OOO Program Verification Systems</a>公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目，诸如<a href="http://httpd.apache.org">Apache Http Server</a>、<a href="http://www.chromium.org">Chromium</a>、<a href="http://clang.llvm.org">Clang</a>、<a href="http://www.cmake.org">CMake</a>、<a href="http://www.mysql.com">MySQL</a>等的源码进行了分析，找出了<a href="http://www.viva64.com/en/a/0079/">100个典型的Bugs</a>。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析，倒不如在编码时就提高自知自觉，避免这份列表上的错误发生在你的代码中，因此这里将部分摘录一些Bugs（Bug编号这里不连续，为的是对应原文的编号）并做简要说明。原文将这份Bug列表分为了几类，这里也将沿用这个思路。</p>
<p><b>一、数组和字符串处理错误</b></p>
<p>数组和字符串处理错误是C/C++程序中最多的一类缺陷类型。这也可以看作是我们为拥有高效地底层内存操作能力而付出的代价。</p>
<p><b>[</b><b><tt>#1</tt></b><b>]</b><a href="http://en.wikipedia.org/wiki/Wolfenstein_3D">&nbsp;Wolfenstein 3D</a>项目 -&quot;只有部分对象被clear了&quot;</p>
<p><span style="font-family: 'courier new', courier, monospace;">void CG_RegisterItemVisuals( int itemNum ) {<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; itemInfo_t *itemInfo;<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; memset( itemInfo, 0, sizeof( &amp;itemInfo ) );<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>这里的Bug出现在memset那一行。代码的真实意图是clear iteminfo这块内存，但调用memset时，第三个参数传入的却是sizeof(&amp;iteminfo)，要知道 sizeof(&amp;itemInfo) != sizeof(itemInfo_t)，前者只是一个指针的大小罢了。正确的写法是：</p>
<p><span style="font-family: 'courier new', courier, monospace;">memset(itemInfo, 0, sizeof(itemInfo_t)); 或memset(itemInfo, 0, sizeof(*itemInfo));</span></p>
<p><b>[#2]&nbsp;</b>Wolfenstein 3D项目 -&quot;只有部分Matrix被clear了&quot;</p>
<p><span style="font-family: 'courier new', courier, monospace;">ID_INLINE mat3_t::mat3_t( float src[ 3 ][ 3 ] ) {<br />
	&nbsp;&nbsp;&nbsp; memcpy( mat, src, sizeof( src ) );<br />
	}</span></p>
<p>这里的Bug出现在memcpy一行。程序的原意是将clear src[3][3]这个<a href="http://tonybai.com/2013/03/28/pointer-and-multi-dimension-array-in-c/">二维数组</a>。但这里有个坑：那就是作为函数形式参数的数组名已经退化为指针了，对其sizeof只能得到一个指针的长度，因此这里的 memcpy只是copy了一个指针的长度，没有copy全。这里的代码是C++代码，原文中给出了正确的改正方法 &#8211; 传reference：</p>
<p><span style="font-family: 'courier new', courier, monospace;">ID_INLINE mat3_t::mat3_t( float (&amp;src)[3][3] )<br />
	{<br />
	&nbsp;&nbsp;&nbsp; memcpy( mat, src, sizeof( src ) );<br />
	}</span></p>
<p><b>[#4]&nbsp;</b><a href="http://www.reactos.org">ReactOS</a>项目 &#8211; &quot;错误地计算一个字符串的长度&quot;</p>
<p><span style="font-family: 'courier new', courier, monospace;">static const PCHAR Nv11Board = &quot;NV11 (GeForce2) Board&quot;;<br />
	static const PCHAR Nv11Chip = &quot;Chip Rev B2&quot;;<br />
	static const PCHAR Nv11Vendor = &quot;NVidia Corporation&quot;;</span></p>
<p><span style="font-family:courier new,courier,monospace;">BOOLEAN<br />
	IsVesaBiosOk(&#8230;)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; if (!(strncmp(Vendor, Nv11Vendor, sizeof(Nv11Vendor))) &amp;&amp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !(strncmp(Product, Nv11Board, sizeof(Nv11Board))) &amp;&amp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !(strncmp(Revision, Nv11Chip, sizeof(Nv11Chip))) &amp;&amp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (OemRevision == 0&#215;311))<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>Bug处在IsVesaBiosOK中那一串strncmp调用中，代码将一个指针的size传入strncmp作为第三个参数，导致 strncmp实际只是比较了字符串的前4 or 8个字节，而不是字符串的全部内容。</p>
<p><b>[#6]&nbsp;</b>CPU Identifying Tool项目 &#8211; 数组越界</p>
<p><span style="font-family: 'courier new', courier, monospace;">#define FINDBUFFLEN 64&nbsp; // Max buffer find/replace size<br />
	&#8230;<br />
	int WINAPI Sticky (&#8230;)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; static char findWhat[FINDBUFFLEN] = {&#39;\0&#39;};<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; findWhat[FINDBUFFLEN] = &#39;\0&#39;;<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>bug出在&quot;findWhat[FINDBUFFLEN] = &#8216;\0&#8242;;”这一行。数组的最大长度为FINDBUFFLEN，但下标的最大值应该是FINDBUFFLEN-1，而不是FINDBUFFLEN。因此这 行代码显然应该改为findWhat[FINDBUFFLEN-1] = &#39;\0&#39;;</p>
<p><b>[#7]</b>&nbsp;Wolfenstein 3D项目 &#8211; 数组越界</p>
<p><span style="font-family: 'courier new', courier, monospace;">typedef struct bot_state_s<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; char teamleader[32]; //netname of the team leader<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}&nbsp; bot_state_t;</span></p>
<p><span style="font-family:courier new,courier,monospace;">void BotTeamAI( bot_state_t *bs ) {<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; bs-&gt;teamleader[sizeof( bs->teamleader )] = &#39;\0&#39;;<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>&quot;sizeof( bs-&gt;teamleader )]&quot;这行的结果值已经超出了数组的最大边界，正确的代码是：</p>
<p><span style="font-family: 'courier new', courier, monospace;">bs-&gt;teamleader[<br />
	&nbsp; sizeof(bs-&gt;teamleader) / sizeof(bs-&gt;teamleader[0]) &#8211; 1<br />
	&nbsp; ] = &#39;\0&#39;;</span></p>
<p><b>[#8]&nbsp;</b><a href="http://www.miranda-im.org">Miranda IM</a>项目 &#8211; 只Copy了部分字符串</p>
<p><span style="font-family: 'courier new', courier, monospace;">struct _textrangew<br />
	{<br />
	&nbsp;&nbsp;&nbsp; CHARRANGE chrg;<br />
	&nbsp;&nbsp;&nbsp; LPWSTR lpstrText;<br />
	} TEXTRANGEW;</span></p>
<p><span style="font-family:courier new,courier,monospace;">const wchar_t* Utils::extractURLFromRichEdit(&#8230;)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; ::CopyMemory(tr.lpstrText, L&quot;mailto:&quot;, 7);<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>这里的bug在于L&quot;mailto:&quot;是宽字符串，宽字符串中的每个字符占2或4个字节（依Compiler使用的<a href="http://tonybai.com/2007/11/03/also-talk-about-char-encoding/">字符集</a>编码而定），因此这里只 copy 7个字节显然是不够的，应该是7 * sizeof(wchar_t)。</p>
<p><b>[#9]</b>&nbsp;CMake项目 &#8211; 循环內的数组越界</p>
<p><span style="font-family: 'courier new', courier, monospace;">static const struct {<br />
	&nbsp;&nbsp;&nbsp; DWORD&nbsp;&nbsp; winerr;<br />
	&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; doserr;<br />
	} doserrors[] =<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	};</span></p>
<p><span style="font-family:courier new,courier,monospace;">static void<br />
	la_dosmaperr(unsigned long e)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; for (i = 0; i &lt; sizeof(doserrors); i++)<br />
	&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (doserrors[i].winerr == e)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; errno = doserrors[i].doserr;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>作者原本意图la_dosmaperr中for循环的次数等于数组的元素个数，但sizeof(doserrors)返回的却是数组占用的字节个数，这远远大于数组元素个数，因此造成数组越界。正确的写法：</p>
<p><span style="font-family: 'courier new', courier, monospace;">for (i = 0; i &lt; sizeof(doserrors) / sizeof(*doserrors); i++)</span></p>
<p><strong>[#10]</strong>&nbsp;CPU Identifying Tool项目 &#8211; 打印到自身的字符串</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">char * OSDetection ()<br />
	{<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; sprintf(szOperatingSystem,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;%sversion %d.%d %s (Build %d)&quot;,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; szOperatingSystem,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.dwMajorVersion,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.dwMinorVersion,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.szCSDVersion,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.dwBuildNumber &amp; 0xFFFF);<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; sprintf (szOperatingSystem, &quot;%s%s(Build %d)&quot;,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; szOperatingSystem, osvi.szCSDVersion,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.dwBuildNumber &amp; 0xFFFF);<br />
	&nbsp; &nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">通过sprintf，szOperatingSystem字符串将自己打印到自己里面，这是十分危险的，将导致无法预知的错误结果，可能会导致栈溢出等严重问题。</p>
<p style="font-size: 13px;"><strong>[#12]</strong>&nbsp;<a href="http://notepad-plus-plus.org">Notepad++</a>项目 &#8211; 数组局部clear</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">#define CONT_MAP_MAX 50<br />
	int _iContMap[CONT_MAP_MAX];<br />
	&#8230;<br />
	DockingManager::DockingManager()<br />
	{<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; memset(_iContMap, -1, CONT_MAP_MAX);<br />
	&nbsp; &nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">代码的原本试图将数组_iContMap清零，但memset的第三个参数CONT_MAP_MAX并不能代表数组的真正大小，而只是数组的元素个数而已，显然其忘记乘以sizeof(int)了。</p>
<p style="font-size: 13px;"><b>二、未定义行为</b></p>
<p style="font-size: 13px;">在C/C++的语言规范中，我们常常能看到&ldquo;xx is undefined&rdquo;。规范中并没有明确表明这类错误是什么样子的，只是说取决于Compiler的实现，也许Compiler会给出正确的结果，但这么使用却是不可移植的。</p>
<p style="font-size: 13px;"><strong>[#1]</strong>&nbsp;Chromium项目 &#8211; 智能指针的误用</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">void AccessibleContainsAccessible(&#8230;)<br />
	{<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; auto_ptr&lt;VARIANT&gt; child_array(new VARIANT[child_count]);<br />
	&nbsp; &nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这里的问题在于使用new[]分配的内存，在智能指针释放时却用了delete，这将会导致未定义行为。看看autoptr的destructor就知道了：</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">~auto_ptr() {<br />
	&nbsp; &nbsp; delete _Myptr;<br />
	}</span></p>
<p style="font-size: 13px;">我们可以找一些更合适的类来fix这个问题，比如boost::scopedarray。</p>
<p style="font-size: 13px;"><strong>[#2]</strong>&nbsp;IPP Sample项目 &#8211; 经典未定义行为</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">template&lt;typename T, Ipp32s size&gt; void HadamardFwdFast(&#8230;)<br />
	{<br />
	&nbsp; Ipp32s *pTemp;<br />
	&nbsp; &#8230;<br />
	&nbsp; for(j=0;j&lt;4;j++) {<br />
	&nbsp; &nbsp; a[0] = pTemp[0*4] + pTemp[1*4];<br />
	&nbsp; &nbsp; a[1] = pTemp[0*4] &#8211; pTemp[1*4];<br />
	&nbsp; &nbsp; a[2] = pTemp[2*4] + pTemp[3*4];<br />
	&nbsp; &nbsp; a[3] = pTemp[2*4] &#8211; pTemp[3*4];<br />
	&nbsp; &nbsp; pTemp = pTemp++;<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">很多人一眼就看到了&quot;pTemp = pTemp++&quot;这行，对于这个代码编译器会产生两种结果截然不同的翻译：</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">pTemp = pTemp + 1;<br />
	pTemp = pTemp;</span></p>
<p style="font-size: 13px;">或</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">TMP = pTemp;<br />
	pTemp = pTemp + 1;<br />
	pTemp = TMP;</span></p>
<p style="font-size: 13px;">到底是哪种呢？依赖于编译器的实现，甚至是优化级别的设定。</p>
<p style="font-size: 13px;"><b>三、与运算优先级相关的错误</b></p>
<p style="font-size: 13px;"><strong>[#1]</strong>&nbsp;MySQL工程 &#8211; !和&amp;的运算优先级</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">int ha_innobase::create(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (srv_file_per_table<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &amp;&amp; !mysqld_embedded<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &amp;&amp; (!create_info-&gt;options &amp; HA_LEX_CREATE_TMP_TABLE)) {<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这段代码原意是想测试create_info-&gt;options变量中几个bit位的值是否set了，即!(create_info-&gt;options &amp; HA_LEX_CREATE_TMP_TABLE)，但由于!的运算优先级高于&amp;，实际逻辑变成了(!create_info-&gt;options) &amp;&nbsp;HA_LEX_CREATE_TMP_TABLE了。如果想要这段代码如期工作，就不要吝啬小括号了。</p>
<p style="font-size: 13px;"><strong>[#2]</strong>&nbsp;<a href="http://www.emule-project.net">Emule</a>工程 &#8211; *和++的运算优先级</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">STDMETHODIMP<br />
	CCustomAutoComplete::Next(&#8230;, ULONG *pceltFetched)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (pceltFetched != NULL)<br />
	&nbsp; &nbsp; *pceltFetched++;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">显然作者原意是想对pceltFetched所指向的long型变量进行++操作，但由于*和++的运算优先级没有搞对，导致实际上执行了*(pceltFetched++)的操作，而不是(*pceltFetched)++操作。</p>
<p style="font-size: 13px;"><strong>[#3]</strong> Chromium项目 &#8211; &amp;和!=的运算优先级</p>
<p><span style="font-family:courier new,courier,monospace;">#define FILE_ATTRIBUTE_DIRECTORY 0&#215;00000010</span></p>
<p><span style="font-family:courier new,courier,monospace;">bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {<br />
	&nbsp; &#8230;<br />
	&nbsp; info-&gt;is_directory =<br />
	&nbsp; &nbsp; file_info.dwFileAttributes &amp; FILE_ATTRIBUTE_DIRECTORY != 0;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这个程序员的意图是通过测试file_info.dwFileAttributes的几个bit位的值来判定是否是目录，逻辑上应该是(file_info.dwFileAttributes &amp; FILE_ATTRIBUTE_DIRECTORY) != 0，但由于!=优先级高于&amp;，原代码中无括号，结果逻辑变成了file_info.dwFileAttributes &amp; (FILE_ATTRIBUTE_DIRECTORY != 0)，导致is_directory将永远求值为true。</p>
<p style="font-size: 13px;"><strong>[#4]</strong> BCmenu项目 &#8211; if和else弄混</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void BCMenu::InsertSpaces(void)<br />
	{<br />
	&nbsp; if(IsLunaMenuStyle())<br />
	&nbsp; &nbsp; if(!xp_space_accelerators) return;<br />
	&nbsp; else<br />
	&nbsp; &nbsp; if(!original_space_accelerators) return;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这又是C语言的一个&ldquo;大坑&rdquo;，无奈这个BCMenu项目的程序员掉坑里了。虽然从代码缩进上来看，else似乎是与最外层的if配对使用，但实际这段代码的效果是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if(IsLunaMenuStyle())<br />
	{<br />
	&nbsp; &nbsp;if(!xp_space_accelerators) {<br />
	&nbsp; &nbsp; &nbsp;return;<br />
	&nbsp; &nbsp;} else {<br />
	&nbsp; &nbsp; &nbsp;if(!original_space_accelerators) return;<br />
	&nbsp; &nbsp;}<br />
	}</span></p>
<p style="font-size: 13px;">这显然不是程序员原意，看来括号必要时还是不能省略的。修改后的代码如下：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if(IsLunaMenuStyle()) {<br />
	&nbsp; if(!xp_space_accelerators) return;<br />
	} else {<br />
	&nbsp; if(!original_space_accelerators) return;<br />
	}</span></p>
<p style="font-size: 13px;"><b style="font-size: 13px;">四、格式化输出错误</b></p>
<p style="font-size: 13px;"><strong>[#1]</strong> ReactOS项目 &#8211; 错误地输出WCHAR字符</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">static void REGPROC_unescape_string(WCHAR* str)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; default:<br />
	&nbsp; &nbsp; fprintf(stderr,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;Warning! Unrecognized escape sequence: \\%c&#39;\n&quot;,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; str[str_idx]);<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">%c是用来格式化输出非宽字符的，这里用来输出WCHAR显然会得到错误的结果，fix solution是将%c换位%C。</p>
<p style="font-size: 13px;"><strong>[#2]</strong> Intel AMT SDK项目 &#8211; 缺少%s</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void addAttribute(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; int index = _snprintf(temp, 1023,&nbsp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;%02x%02x:%02x%02x:%02x%02x:%02x%02x:&quot;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;%02x%02x:02x%02x:%02x%02x:%02x%02x&quot;,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value[0],value[1],value[2],value[3],value[4],<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value[5],value[6],value[7],value[8],<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value[9],value[10],value[11],value[12],<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value[13],value[14],value[15]);<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;">不解释了，自己慢慢数和对照吧。</p>
<p style="font-size: 13px;"><strong>[#3]</strong>&nbsp;Intel AMT SDK项目 &#8211; 未使用的参数</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">bool GetUserValues(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; printf(&quot;Error: illegal value. Aborting.\n&quot;, tmp);<br />
	&nbsp; return false;<br />
	}</span></p>
<p style="font-size: 13px;">显然tmp是多余的。</p>
<p style="font-size: 13px;"><b style="font-size: 13px;">五、书写错误</b></p>
<p style="font-size: 13px;"><strong>[#1]</strong> Miranda IM项目 &#8211; 在if中赋值</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void CIcqProto::handleUserOffline(BYTE *buf, WORD wLen)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; else if (wTLVType = 0&#215;29 &amp;&amp; wTLVLen == sizeof(DWORD))<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">&ldquo;wTLVType = 0&#215;29&rdquo;显然是笔误，应该是&ldquo;wTLVType ==&nbsp;0&#215;29&rdquo;才对。</p>
<p style="font-size: 13px;"><strong>[#3]</strong> Clang项目 &#8211; 对象名书写错误</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">static Value *SimplifyICmpInst(&#8230;) {<br />
	&nbsp; &#8230;<br />
	&nbsp; case Instruction::Shl: {<br />
	&nbsp; &nbsp; bool NUW =<br />
	&nbsp; &nbsp; &nbsp; LBO-&gt;hasNoUnsignedWrap() &amp;&amp; LBO-&gt;hasNoUnsignedWrap();<br />
	&nbsp; &nbsp; bool NSW =<br />
	&nbsp; &nbsp; &nbsp; LBO-&gt;hasNoSignedWrap() &amp;&amp; RBO-&gt;hasNoSignedWrap();<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">从最后一行先后使用了LBO和RBO来看，前面只用了LBO的那行很可能是有问题的，正确的应该是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">bool NUW =<br />
	&nbsp; &nbsp; &nbsp; LBO-&gt;hasNoUnsignedWrap() &amp;&amp; RBO-&gt;hasNoUnsignedWrap();</span></p>
<p style="font-size: 13px;"><strong>[#6]</strong> G3D Content Pak项目 &#8211; 一对括号放错了地方</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">bool Matrix4::operator==(const Matrix4&amp; other) const {<br />
	&nbsp; if (memcmp(this, &amp;other, sizeof(Matrix4) == 0)) {<br />
	&nbsp; &nbsp; return true;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">由于括号放错了地方，导致memcmp最后的参数变成了sizeof(Matrix4) == 0，这行代码的正确写法应该是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if (memcmp(this, &amp;other, sizeof(Matrix4)) == 0) {</span></p>
<p style="font-size: 13px;"><strong>[#8]</strong> Apache Http Server项目 &#8211; 多余的sizeof</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">PSECURITY_ATTRIBUTES GetNullACL(void)<br />
	{<br />
	&nbsp; PSECURITY_ATTRIBUTES sa;<br />
	&nbsp; sa &nbsp;= (PSECURITY_ATTRIBUTES)<br />
	&nbsp; &nbsp; LocalAlloc(LPTR, sizeof(SECURITY_ATTRIBUTES));<br />
	&nbsp; sa-&gt;nLength = sizeof(sizeof(SECURITY_ATTRIBUTES));<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">最后一行显然是笔误，sizeof(sizeof(SECURITY_ATTRIBUTES))应该写为sizeof(SECURITY_ATTRIBUTES)才对。</p>
<p style="font-size: 13px;"><strong>[#10]</strong> Notepad++项目 &#8211; 在本来应该用&amp;的地方使用了&amp;&amp;</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">TCHAR GetASCII(WPARAM wParam, LPARAM lParam)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; result=ToAscii(wParam,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(lParam &gt;&gt; 16) &amp;&amp; 0xff, keys,&amp;dwReturnedValue,0);<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">(lParam &gt;&gt; 16) &amp;&amp; 0xff没有什么意义，求值结果总是true。这里的代码应该是(lParam &gt;&gt; 16) &amp;&nbsp;0xff。</p>
<p style="font-size: 13px;"><strong>[#12]</strong>&nbsp;Fennec Media Project项目 &#8211; 额外的分号</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">int settings_default(void)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; for(i=0; i&lt;16; i++);<br />
	&nbsp; &nbsp; for(j=0; j&lt;32; j++)<br />
	&nbsp; &nbsp; {<br />
	&nbsp; &nbsp; &nbsp; settings.conversion.equalizer_bands.boost[i][j] = 0.0;<br />
	&nbsp; &nbsp; &nbsp; settings.conversion.equalizer_bands.preamp[i] &nbsp; = 0.0;<br />
	&nbsp; &nbsp; }<br />
	}</span></p>
<p style="font-size: 13px;">这又是一个实际逻辑与代码缩进不符的例子。作者的原意是这样的：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">for(i=0; i&lt;16; i++)&nbsp;<br />
	{<br />
	&nbsp; &nbsp; for(j=0; j&lt;32; j++)<br />
	&nbsp; &nbsp; {<br />
	&nbsp; &nbsp; &nbsp; settings.conversion.equalizer_bands.boost[i][j] = 0.0;<br />
	&nbsp; &nbsp; &nbsp; settings.conversion.equalizer_bands.preamp[i] &nbsp; = 0.0;<br />
	&nbsp; &nbsp; }<br />
	}</span></p>
<p style="font-size: 13px;">但实际执行代码逻辑却是：</p>
<p><span style="font-family:courier new,courier,monospace;">for(i=0; i&lt;16; i++)&nbsp;<br />
	{<br />
	&nbsp; &nbsp; ;<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">for(j=0; j&lt;32; j++)<br />
	{ &nbsp;&nbsp;<br />
	&nbsp; settings.conversion.equalizer_bands.boost[i][j] = 0.0;<br />
	&nbsp; settings.conversion.equalizer_bands.preamp[i] &nbsp; = 0.0;<br />
	}</span></p>
<p style="font-size: 13px;">这一切都是那个;导致的。</p>
<p style="font-size: 13px;"><strong>六、对基本函数和类的误用</strong></p>
<p style="font-size: 13px;"><strong>[#2]</strong>&nbsp;TortoiseSVN项目 &#8211; remove函数的误用</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">STDMETHODIMP CShellExt::Initialize(&#8230;.)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; ignoredprops = UTF8ToWide(st.c_str());<br />
	&nbsp; // remove all escape chars (&#39;\\&#39;)<br />
	&nbsp; std::remove(ignoredprops.begin(), ignoredprops.end(), &#39;\\&#39;);<br />
	&nbsp; break;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">作者意图删除所有&#39;\\&#39;，但他用错了函数，remove函数只是交换元素的位置，将要删除的元素交换到尾部trash，并且返回指向trash首地址的iterator。正确的做法应该是&quot;v.erase(remove(v.begin(), v.end(), 2), v.end())&quot;。</p>
<p style="font-size: 13px;"><strong>[#5]</strong> Pixie项目 &#8211; 在循环中使用alloca函数</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">inline &nbsp;void &nbsp;triangulatePolygon(&#8230;) {<br />
	&nbsp; &#8230;<br />
	&nbsp; for (i=1;i&lt;nloops;i++) {<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; do {<br />
	&nbsp; &nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; &nbsp; do {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; CTriVertex &nbsp;*snVertex =<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(CTriVertex *)alloca(2*sizeof(CTriVertex));<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; &nbsp; } while(dVertex != loops[0]);<br />
	&nbsp; &nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; } while(sVertex != loops[i]);<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">alloca函数在栈上分配内存，因此在循环中使用alloca可能会很快导致栈溢出。</p>
<p style="font-size: 13px;"><strong style="font-size: 13px;">七、无意义的代码</strong></p>
<p style="font-size: 13px;"><strong>[#1]</strong> IPP Samples项目 &#8211; 不完整的条件</p>
<p><span style="font-family:courier new,courier,monospace;">void lNormalizeVector_32f_P3IM(Ipp32f *vec[3],<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Ipp32s* mask, Ipp32s len)<br />
	{<br />
	&nbsp; Ipp32s &nbsp;i;<br />
	&nbsp; Ipp32f &nbsp;norm;</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; for(i=0; i&lt;len; i++) {<br />
	&nbsp; &nbsp; if(mask&lt;0) continue;<br />
	&nbsp; &nbsp; norm = 1.0f/sqrt(vec[0][i]*vec[0][i]+<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;vec[1][i]*vec[1][i]+vec[2][i]*vec[2][i]);<br />
	&nbsp; &nbsp; vec[0][i] *= norm; vec[1][i] *= norm; vec[2][i] *= norm;<br />
	&nbsp; }<br />
	}</span></p>
<p style="font-size: 13px;">mask是Ipp32s类型指针，这样if (mask&lt; 0)这句代码显然没啥意义，正确的代码应该是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if (mask[i] &lt; 0) continue;</span></p>
<p style="font-size: 13px;"><strong>[#2]</strong> QT项目 &#8211; 重复的检查</p>
<p><span style="font-family:courier new,courier,monospace;">Q3TextCustomItem* Q3TextDocument::parseTable(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; while (end &lt; length<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;/td&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;td&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;/th&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;th&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;td&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;/tr&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;tr&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;/table&quot;))) {</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这里对&quot;&lt;td&quot;做了两次check。</p>
<p style="font-size: 13px;"><strong style="font-size: 13px;">八、总是True或False的条件</strong></p>
<p style="font-size: 13px;"><strong>[#1]</strong>&nbsp;Shareaza项目 &#8211; char类型的值范围</p>
<p><span style="font-family:courier new,courier,monospace;">void CRemote::Output(LPCTSTR pszName)<br />
	{</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &#8230;<br />
	&nbsp; CHAR* pBytes = new CHAR[ nBytes ];<br />
	&nbsp; hFile.Read( pBytes, nBytes );<br />
	&nbsp; &#8230;<br />
	&nbsp; if ( nBytes &gt; 3 &amp;&amp; pBytes[0] == 0xEF &amp;&amp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pBytes[1] == 0xBB &amp;&amp; pBytes[2] == 0xBF )<br />
	&nbsp; {<br />
	&nbsp; &nbsp; pBytes += 3;<br />
	&nbsp; &nbsp; nBytes -= 3;<br />
	&nbsp; &nbsp; bBOM = true;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>表达式&quot;pBytes[0] == 0xEF&quot;总是False。char类型的值范围是-128~127 &lt; 0xEF，因此这个表达式总是False，导致整个if condition总是为False，与预期逻辑不符。</p>
<p><strong>[#3]</strong> VirtualDub项目 &#8211; 无符号类型总是&gt;=0</p>
<p><span style="font-family:courier new,courier,monospace;">typedef unsigned short wint_t;<br />
	&#8230;<br />
	void lexungetc(wint_t c) {<br />
	&nbsp; if (c &lt; 0)<br />
	&nbsp; &nbsp; return;<br />
	&nbsp; &nbsp;g_backstack.push_back(c);<br />
	}</span></p>
<p>c是unsigned short类型，永远不会小于0,也就是说if (c &lt; 0)永远为False。</p>
<p><strong>[#8]</strong> MySQL项目 &#8211; 条件错误</p>
<p><span style="font-family:courier new,courier,monospace;">enum enum_mysql_timestamp_type<br />
	str_to_datetime(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; else if (str[0] != &#8216;a&#8217; || str[0] != &#39;A&#39;)<br />
	&nbsp; &nbsp; continue; /* Not AM/PM */<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>if (str[0] != &#8216;a&#8217; || str[0] != &#39;A&#39;)这个条件永远为真。也许这块本意是想用&amp;&amp;。</p>
<p><strong style="font-size: 13px;">九、代码漏洞</strong></p>
<p>导致漏洞的代码错误实际上也都是笔误、不正确的条件以及不正确的数组操作等。但这里还是想将一些特定错误划归为一类，因为入侵者可以利用这些错误来攻击你的代码，获取其利益。</p>
<p><strong>[#1]</strong> Ultimate TCP/IP项目 &#8211; 空字符串的错误检查</p>
<p><span style="font-family:courier new,courier,monospace;">char *CUT_CramMd5::GetClientResponse(LPCSTR ServerChallenge)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (m_szPassword != NULL)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; if (m_szPassword != &#39;\0&#39;)<br />
	&nbsp; &nbsp; {<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>第二个if condition check意图检查m_szPassword是否为空字符串，但却错误的将指针与&#39;\0&#39;进行比较，正确的代码应该是这样的：</p>
<p><span style="font-family:courier new,courier,monospace;">if (*m_szPassword != &#39;\0&#39;)</span></p>
<p style="font-size: 13px;"><strong>[#2]</strong> Chromium项目 &#8211; NULL指针的处理</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">bool ChromeFrameNPAPI::Invoke(&#8230;)<br />
	{<br />
	&nbsp; ChromeFrameNPAPI* plugin_instance =<br />
	&nbsp; &nbsp; ChromeFrameInstanceFromNPObject(header);<br />
	&nbsp; if (!plugin_instance &amp;&amp;<br />
	&nbsp; &nbsp; &nbsp; (plugin_instance-&gt;automation_client_.get()))<br />
	&nbsp; &nbsp; return false;<br />
	&nbsp; &#8230;<br />
	} &nbsp;&nbsp;</span></p>
<p style="font-size: 13px;">一旦plugin_instance为NULL，!plugin_instance为True，代码对&amp;&amp;后面的子条件求值，引用plugin_instance将导致程序崩溃。正确的做法应该是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if (plugin_instance &amp;&amp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; (plugin_instance-&gt;automation_client_.get()))<br />
	&nbsp; return false;</span></p>
<p style="font-size: 13px;"><strong>[#5]</strong> Apache httpd Server项目 &#8211; 不完整的缓冲区clear</p>
<p><span style="font-family:courier new,courier,monospace;">#define MEMSET_BZERO(p,l) &nbsp; &nbsp; &nbsp; memset((p), 0, (l))</span></p>
<p><span style="font-family:courier new,courier,monospace;">void apr__SHA256_Final(&#8230;, SHA256_CTX* context) {<br />
	&nbsp; &#8230;<br />
	&nbsp; MEMSET_BZERO(context, sizeof(context));<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这个错误前面提到过，sizeof(context)只是指针的大小，将之改为sizeof(*context)就OK了。</p>
<p style="font-size: 13px;"><strong>[#7]</strong> PNG Library项目 &#8211; 意外的指针clear</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">png_size_t<br />
	png_check_keyword(png_structp png_ptr, png_charp key,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; png_charpp new_key)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (key_len &gt; 79)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; png_warning(png_ptr, &quot;keyword length must be 1 &#8211; 79 characters&quot;);<br />
	&nbsp; &nbsp; new_key[79] = &#39;\0&#39;;<br />
	&nbsp; &nbsp; key_len = 79;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">new_key的类型为png_charpp，顾名思义，这是一个char**类型，但代码中new_key[79] = &#8216;\0&#8242;这句显然是要给某个char赋值，但new_key[n]得到的应该是一个地址，给一个地址赋值为&#8217;\0&#8242;显然是有误的。正确的写法应该是(*new_key)[79] = &#39;\0&#39;。</p>
<p style="font-size: 13px;"><strong>[#10]</strong> Miranda IM项目 &#8211; 保护没生效</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void Append( PCXSTR pszSrc, int nLength )<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; UINT nOldLength = GetLength();<br />
	&nbsp; if (nOldLength &lt; 0)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; // protects from underflow<br />
	&nbsp; &nbsp; nOldLength = 0;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">nOldLength椒UINT类型，其值永远不会小于0,因此if (nOldLength &lt; 0)这行成了摆设。</p>
<p style="font-size: 13px;"><strong>[#12]</strong> Ultimate TCP/IP项目 &#8211; 不正确的循环结束条件</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void CUT_StrMethods::RemoveSpaces(LPSTR szString) {<br />
	&nbsp; &#8230;<br />
	&nbsp; size_t loop, len = strlen(szString);<br />
	&nbsp; // Remove the trailing spaces<br />
	&nbsp; for(loop = (len-1); loop &gt;= 0; loop&#8211;) {<br />
	&nbsp; &nbsp; if(szString[loop] != &#39; &#39;)<br />
	&nbsp; &nbsp; &nbsp; break;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">循环中的结束条件loop &gt;= 0将永远为True，因为loop变量的类型是size_t是unsigned类型，永远不会小于0。</p>
<p style="font-size: 13px;"><strong style="font-size: 13px;">十、拷贝粘贴</strong></p>
<p style="font-size: 13px;">和笔误不同，程序员们决不因该低估拷贝粘贴问题，这类问题发生了太多。程序员们花费了大量时间在这些问题的debug上。</p>
<p style="font-size: 13px;"><strong>[#1]</strong> Fennec Media Project项目 &#8211; 处理数组元素时出错</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void* tag_write_setframe(char *tmem,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;const char *tid, const string dstr)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if(lset)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; fhead[11] = &#39;\0&#39;;<br />
	&nbsp; &nbsp; fhead[12] = &#39;\0&#39;;<br />
	&nbsp; &nbsp; fhead[13] = &#39;\0&#39;;<br />
	&nbsp; &nbsp; fhead[13] = &#39;\0&#39;;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;">咋看一下，fhead[13]做了两次赋值，似乎没啥问题。但仔细想一下，最后那行程序员的原意极可能是想写fhead[14] = &#39;\0&#39;。问题就在这里了。</p>
<p style="font-size: 13px;">[#2] MySQL项目 &#8211; 处理数组元素时出错</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">static int rr_cmp(uchar *a,uchar *b)<br />
	{<br />
	&nbsp; if (a[0] != b[0])<br />
	&nbsp; &nbsp; return (int) a[0] &#8211; (int) b[0];<br />
	&nbsp; if (a[1] != b[1])<br />
	&nbsp; &nbsp; return (int) a[1] &#8211; (int) b[1];<br />
	&nbsp; if (a[2] != b[2])<br />
	&nbsp; &nbsp; return (int) a[2] &#8211; (int) b[2];<br />
	&nbsp; if (a[3] != b[3])<br />
	&nbsp; &nbsp; return (int) a[3] &#8211; (int) b[3];<br />
	&nbsp; if (a[4] != b[4])<br />
	&nbsp; &nbsp; return (int) a[4] &#8211; (int) b[4];<br />
	&nbsp; if (a[5] != b[5])<br />
	&nbsp; &nbsp; return (int) a[1] &#8211; (int) b[5];<br />
	&nbsp; if (a[6] != b[6])<br />
	&nbsp; &nbsp; return (int) a[6] &#8211; (int) b[6];<br />
	&nbsp; return (int) a[7] &#8211; (int) b[7];<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;">编写这类代码时，我猜绝大多数人会选择Copy-Paste，然后再逐行修改，问题就发生在修改过程中，上面的代码中当处理a[5] != b[5]时就忘记修改一个下标了：return (int) a[1] &#8211; (int) b[5];显然这里的正确代码应该是return (int) a[5] &#8211; (int) b[5]。</p>
<p style="font-size: 13px;"><strong>[#3]</strong>&nbsp;TortoiseSVN项目 文件名不正确</p>
<p><span style="font-family:courier new,courier,monospace;">BOOL GetImageHlpVersion(DWORD &amp;dwMS, DWORD &amp;dwLS)<br />
	{<br />
	&nbsp; return(GetInMemoryFileVersion((&quot;DBGHELP.DLL&quot;),<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dwMS, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dwLS)) ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">BOOL GetDbgHelpVersion(DWORD &amp;dwMS, DWORD &amp;dwLS)<br />
	{<br />
	&nbsp; return(GetInMemoryFileVersion((&quot;DBGHELP.DLL&quot;),<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dwMS, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dwLS)) ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<br />
	}</span></p>
<p style="font-size: 13px;">GetImageHlpVersion和GetDbgHelpVersion都使用了&quot;DBGHELP.DLL&quot;文件，显然GetImageHlpVersion写错文件名了。应该用&quot;IMAGEHLP.DLL&quot;就对了。</p>
<p style="font-size: 13px;"><strong>[#4]</strong> Clang项目 &#8211; 等同的函数体</p>
<p><span style="font-family:courier new,courier,monospace;">MapTy PerPtrTopDown;<br />
	MapTy PerPtrBottomUp;</span></p>
<p><span style="font-family:courier new,courier,monospace;">void clearBottomUpPointers() {<br />
	&nbsp; PerPtrTopDown.clear();<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">void clearTopDownPointers() {<br />
	&nbsp; PerPtrTopDown.clear();<br />
	}</span></p>
<p style="font-size: 13px;">我们看到虽然两个函数名不同，但是函数体的内容是相同的，显然又是copy-paste惹的祸。做如下修改即可：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void clearBottomUpPointers() {<br />
	&nbsp; PerPtrBottomUp.clear();<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;"><strong style="font-size: 13px;">十一、Null指针的校验迟了</strong></p>
<p style="font-size: 13px;">这里的&ldquo;迟了&rdquo;的含义是先使用指针，然后再校验指针是否为NULL。</p>
<p style="font-size: 13px;"><strong>[#1]</strong>&nbsp;Quake-III-Arena项目 &#8211; 校验迟了</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void Item_Paint(itemDef_t *item) {<br />
	&nbsp; vec4_t red;<br />
	&nbsp; menuDef_t *parent = (menuDef_t*)item-&gt;parent;<br />
	&nbsp; red[0] = red[3] = 1;<br />
	&nbsp; red[1] = red[2] = 0;<br />
	&nbsp; if (item == NULL) {<br />
	&nbsp; &nbsp; return;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;">在校验item是否为NULL前已经使用过item了，一旦item真的为NULL，那程序必然崩溃。</p>
<p style="font-size: 13px;"><strong>十二、其他杂项</strong></p>
<p style="font-size: 13px;"><strong>[#1]</strong> Image Processing 项目 &#8211; 八进制数</p>
<p><span style="font-family:courier new,courier,monospace;">inline<br />
	void elxLuminocity(const PixelRGBus&amp; iPixel,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;LuminanceCell&lt; PixelRGBus &gt;&amp; oCell)<br />
	{<br />
	&nbsp; oCell._luminance = uint16(0.2220f*iPixel._red +<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0.7067f*iPixel._blue + 0.0713f*iPixel._green);<br />
	&nbsp; oCell._pixel = iPixel;<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">inline<br />
	void elxLuminocity(const PixelRGBi&amp; iPixel,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;LuminanceCell&lt; PixelRGBi &gt;&amp; oCell)<br />
	{<br />
	&nbsp; oCell._luminance = 2220*iPixel._red +<br />
	&nbsp; &nbsp; 7067*iPixel._blue + 0713*iPixel._green;<br />
	&nbsp; oCell._pixel = iPixel;<br />
	}</span></p>
<p>第二个函数，程序员原意是使用713这个十进制整数，但0713 != 713，在C中，0713是八进制的表示法，Compiler会认为这是个八进制数。</p>
<p><strong>[#2]</strong> IPP Sample工程 &#8211; 一个变量用于两个loop中</p>
<p><span style="font-family:courier new,courier,monospace;">JERRCODE CJPEGDecoder::DecodeScanBaselineNI(void)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; for(c = 0; c &lt; m_scan_ncomps; c++)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; block = m_block_buffer + (DCTSIZE2*m_nblock*(j+(i*m_numxMCU)));</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; // skip any relevant components<br />
	&nbsp; &nbsp; for(c = 0; c &lt; m_ccomp[m_curr_comp_no].m_comp_no; c++)<br />
	&nbsp; &nbsp; {<br />
	&nbsp; &nbsp; &nbsp; block += (DCTSIZE2*m_ccomp[c][/c][/c].m_nblocks);<br />
	&nbsp; &nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>变量c用在了两个loop中，这会导致只有部分数据被处理，或外部循环中止。</p>
<p><strong>[#3]</strong> Notepad++项目 &#8211; 怪异的条件表达式</p>
<p><span style="font-family:courier new,courier,monospace;">int Notepad_plus::getHtmlXmlEncoding(&#8230;.) const<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (langT != L_XML &amp;&amp; langT != L_HTML &amp;&amp; langT == L_PHP)<br />
	&nbsp; &nbsp; return -1;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>代码中的那行if条件等价于 if (langT == L_PHP)，显然似乎不是作者原意，猜测正确的代码应该是这样的：</p>
<p><span style="font-family:courier new,courier,monospace;">int Notepad_plus::getHtmlXmlEncoding(&#8230;.) const<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (langT != L_XML &amp;&amp; langT != L_HTML &amp;&amp; langT != L_PHP)<br />
	&nbsp; &nbsp; return -1;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
	</channel>
</rss>
