<?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; Glibc</title>
	<atom:link href="http://tonybai.com/tag/glibc/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 20 Apr 2026 23:16:50 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Go 考古：Go 官方如何决定支持你的 CPU 和 OS？</title>
		<link>https://tonybai.com/2026/01/01/go-archaeology-porting-policy/</link>
		<comments>https://tonybai.com/2026/01/01/go-archaeology-porting-policy/#comments</comments>
		<pubDate>Thu, 01 Jan 2026 05:16:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AlpineLinux]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[BlockReleases]]></category>
		<category><![CDATA[BrokenPorts]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[FirstClassPorts]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GOARCH]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoogleGoTeam]]></category>
		<category><![CDATA[GOOS]]></category>
		<category><![CDATA[GoPortingPolicy]]></category>
		<category><![CDATA[musl]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[port]]></category>
		<category><![CDATA[proposal]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[SecondaryPorts]]></category>
		<category><![CDATA[x/sys]]></category>
		<category><![CDATA[一等公民]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[提案]]></category>
		<category><![CDATA[机器码]]></category>
		<category><![CDATA[构建机器]]></category>
		<category><![CDATA[次要组合]]></category>
		<category><![CDATA[治理智慧]]></category>
		<category><![CDATA[硬件消亡]]></category>
		<category><![CDATA[稳定性承诺]]></category>
		<category><![CDATA[系统调用]]></category>
		<category><![CDATA[线程调度]]></category>
		<category><![CDATA[维护成本]]></category>
		<category><![CDATA[跨平台编译]]></category>
		<category><![CDATA[运行时]]></category>

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

		<guid isPermaLink="false">https://tonybai.com/?p=5040</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/16/brand-new-os-impossible 大家好，我是Tony Bai。 对于许多心怀浪漫主义的开发者来说，“从零开始编写一个属于自己的操作系统”，或许是技术生涯中最终极、最性感的梦想。这几乎是现代编程世界的“创世纪”，是掌控计算机每一个比特的至高权力。 然而，最近一位名为 Wildan M 的工程师，在他的一篇博文中，用一次亲身参与 Redox OS 项目的经历，给我们所有人泼了一盆冷水。他的结论简单而又颠覆： 现在，从零开始编写一个全新的、能被广泛采用的操作系统，已几乎是一项不可能完成的任务。 而其真正的难点，并非我们想象中那个神秘而复杂的内核，而在于内核之外，那座看不见的、庞大到令人绝望的“冰山”。 冰山一角：内核，那个“最简单”的部分 故事的主角是 Redox OS，一个雄心勃勃的项目。它旨在用内存安全的 Rust 语言，构建一个现代的、基于微内核架构的、可以替代 Linux 和 BSD 的完整操作系统。 当我们谈论“写一个 OS”时，我们通常指的是编写内核。那么 Redox OS 的内核有多复杂呢？文章给出了惊人的数据： * 代码量： 约 3 万行 (30k LoC)。 * 启动速度： 大多数情况下，不到 1 秒。 在短短十年间，Redox 团队已经完成了动态链接、Unix 套接字等核心功能。这无疑是令人敬佩的工程壮举。但 Wildan 指出，这仅仅是浮出水面的冰山一角。一个能启动的内核，距离一个“能用”的操作系统，还有着遥远的距离。 冰山之下：生态移植的“五层地狱” 当作者兴致勃勃地想为 Redox OS 贡献力量，尝试将一些现代程序（如 Go, Node.js, Rust [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/brand-new-os-impossible-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/16/brand-new-os-impossible">本文永久链接</a> &#8211; https://tonybai.com/2025/08/16/brand-new-os-impossible</p>
<p>大家好，我是Tony Bai。</p>
<p>对于许多心怀浪漫主义的开发者来说，“从零开始编写一个属于自己的操作系统”，或许是技术生涯中最终极、最性感的梦想。这几乎是现代编程世界的“创世纪”，是掌控计算机每一个比特的至高权力。</p>
<p>然而，最近一位名为 Wildan M 的工程师，在<a href="https://blog.wellosoft.net/writing-a-brand-new-os-is-almost-impossible-by-now">他的一篇博文</a>中，用一次亲身参与 Redox OS 项目的经历，给我们所有人泼了一盆冷水。他的结论简单而又颠覆：</p>
<p><strong>现在，从零开始编写一个全新的、能被广泛采用的操作系统，已几乎是一项不可能完成的任务。</strong></p>
<p>而其真正的难点，并非我们想象中那个神秘而复杂的内核，而在于内核之外，那座看不见的、庞大到令人绝望的“冰山”。</p>
<h2>冰山一角：内核，那个“最简单”的部分</h2>
<p>故事的主角是 Redox OS，一个雄心勃勃的项目。它旨在用内存安全的 Rust 语言，构建一个现代的、基于微内核架构的、可以替代 Linux 和 BSD 的完整操作系统。</p>
<p>当我们谈论“写一个 OS”时，我们通常指的是编写内核。那么 Redox OS 的内核有多复杂呢？文章给出了惊人的数据：<br />
*   <strong>代码量：</strong> 约 3 万行 (30k LoC)。<br />
*   <strong>启动速度：</strong> 大多数情况下，不到 1 秒。</p>
<p>在短短十年间，Redox 团队已经完成了动态链接、Unix 套接字等核心功能。这无疑是令人敬佩的工程壮举。但 Wildan 指出，这仅仅是浮出水面的冰山一角。一个能启动的内核，距离一个“能用”的操作系统，还有着遥远的距离。</p>
<h2>冰山之下：生态移植的“五层地狱”</h2>
<p>当作者兴致勃勃地想为 Redox OS 贡献力量，尝试将一些现代程序（如 Go, Node.js, Rust 编译器）移植上去时，他才真正撞上了那座隐藏在水面之下的巨大冰山。</p>
<p>一个现代操作系统之所以“能用”，是因为它能运行我们日常使用的所有软件。而将这些软件“搬”到一个全新的操作系统上，需要闯过一重又一重难关。</p>
<p><strong>第一层：系统调用 (Syscall) 的鸿沟</strong></p>
<p>这是最底层的障碍。每个操作系统都有自己的一套与硬件和内核交互的“语言”，即系统调用。Redox OS 的 syscall 与我们熟知的 Linux 完全不同。这意味着，任何需要与内核打交道的程序（几乎是所有程序），都必须重写这部分逻辑，告诉它如何在新世界里“说话”。</p>
<p><strong>第二层：libc 的重担</strong></p>
<p>为了不让每个程序都去痛苦地学习 syscall 这门“方言”，操作系统通常会提供一个标准的“翻译官”——C 标准库 (libc)。它将复杂的 syscall 封装成开发者熟悉的函数（如 printf, open, read）。因此，一个新 OS 的核心任务之一，就是自己实现一个兼容的 libc。Redox 为此用 Rust 实现了一个名为 relibc 的项目，其工程量之浩大可想而知。</p>
<p><strong>第三层：POSIX 的“几乎兼容”陷阱</strong></p>
<p>即便新 OS 像 Redox 一样，努力兼容 POSIX 这个通用标准，噩梦也远未结束。因为无数现有的软件，早已深度依赖于 Linux 特有的、非 POSIX 的功能，比如解析 /proc 文件系统、操作 cgroups 等。结果就是，即使有了 relibc，你依然需要为这些软件挨个打上无数的“补丁”。文章提到，仅 Redox OS 的官方“软件食谱 (Cookbook)”中，就包含了<strong>约 70 个</strong>这样的补丁。</p>
<p><strong>第四层：编译器的“先有鸡还是先有蛋”</strong></p>
<p>你想在新 OS 上原生编译软件吗？那你首先需要一个能在这个 OS 上运行的编译器，比如 GCC、Rustc 或 Go 编译器。但问题是，移植编译器本身，就是所有软件移植任务中最复杂、最艰巨的一种。它需要处理极其底层的二进制格式、链接方式和系统调用。这形成了一个经典的“鸡生蛋还是蛋生鸡”的困局。</p>
<p><strong>第五层：语言生态的“次元壁”</strong></p>
<p>如果说移植 C 语言程序还只是“困难模式”，那么移植那些拥有自己庞大生态的现代语言程序（如 Rust, Go, Node.js），则是“地狱模式”。这些语言的包管理器（如 Cargo, Go Modules）会从中央仓库下载海量依赖，你很难像修改 C 代码一样，通过一个简单的 .patch 文件来修复所有问题。唯一的办法，往往是去 fork 无数个核心依赖库，然后逐一修改，这几乎是一项不可能完成的任务。</p>
<h2>小结：生态，才是那座无法逾越的山</h2>
<p>当 Wildan 经历过这一切后，他得出了文章开头的那个结论。</p>
<p>一个操作系统的成功，或许 <strong>20% 在于内核的精巧，而 80% 在于其上能否运行用户想要的所有软件。</strong> 后者，那个由编译器、标准库、第三方包、应用软件共同构成的庞大生态，才是真正的、几乎无法被复制的“护城河”。</p>
<p>这就像建造一座城市。你可以设计出最宏伟、最先进的市政厅（内核），但如果没有配套的道路、水电、学校、医院、商店（软件生态），这座城市就永远只是一座无法住人的“鬼城”。</p>
<p>这篇文章并非是要劝退所有对底层技术抱有热情的开发者。正如作者所说，如果你想<strong>学习</strong>，从零开始或加入 Redox 这样的项目，会是一段极其宝贵的经历。但如果你想构建一个<strong>被广泛采用</strong>的新 OS，你面对的将不仅仅是技术挑战，更是一个需要说服全球成千上万开发者为你“投票”的社会学难题。</p>
<p>这或许就是对那些仍在坚持构建新 OS 的探索者们，我们应该报以最高敬意的原因。因为他们挑战的，不仅仅是代码，更是一整个时代建立起来的软件文明。</p>
<p>资料链接：https://blog.wellosoft.net/writing-a-brand-new-os-is-almost-impossible-by-now</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/16/brand-new-os-impossible/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go编译的几个细节，连专家也要停下来想想</title>
		<link>https://tonybai.com/2024/11/11/some-details-about-go-compilation/</link>
		<comments>https://tonybai.com/2024/11/11/some-details-about-go-compilation/#comments</comments>
		<pubDate>Sun, 10 Nov 2024 22:13:45 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[CFLAGS]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CGO_ENABLED]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[DWARF]]></category>
		<category><![CDATA[expvar]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[getaddrinfo]]></category>
		<category><![CDATA[getnameinfo]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[glibc-static]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-sqlite3]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golist]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[inittask]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[ldflags]]></category>
		<category><![CDATA[LD_LIBRARY_PATH]]></category>
		<category><![CDATA[libresolv.so]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[NameResolution]]></category>
		<category><![CDATA[nm]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[sqlite-devel]]></category>
		<category><![CDATA[sqlite3]]></category>
		<category><![CDATA[TinyGo]]></category>
		<category><![CDATA[yum]]></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=4383</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/11/11/some-details-about-go-compilation 在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。 注：本文示例使用的环境为Go 1.23.0、Linux Kernel 3.10.0和CentOS 7.9。 1. Go编译默认采用静态链接还是动态链接？ 我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？ 很多人脱口而出：动态链接，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？ 我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点： $go env&#124;grep CGO_ENABLED CGO_ENABLED='1' 验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可： // go-compilation/main.go package main import "fmt" func main() { fmt.Println("hello, world") } 构建该程序： $go build -o helloworld-default main.go 之后，我们查看一下生成的可执行文件helloworld-default的文件属性： $file helloworld-default helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ldd [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/some-details-about-go-compilation-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/11/11/some-details-about-go-compilation">本文永久链接</a> &#8211; https://tonybai.com/2024/11/11/some-details-about-go-compilation</p>
<p>在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。</p>
<blockquote>
<p>注：本文示例使用的环境为<a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23/">Go 1.23.0</a>、Linux Kernel 3.10.0和CentOS 7.9。</p>
</blockquote>
<h2>1. Go编译默认采用静态链接还是动态链接？</h2>
<p>我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？</p>
<p>很多人脱口而出：<a href="https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/">动态链接</a>，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？</p>
<p>我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点：</p>
<pre><code>$go env|grep CGO_ENABLED
CGO_ENABLED='1'
</code></pre>
<p>验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可：</p>
<pre><code>// go-compilation/main.go

package main

import "fmt"

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>构建该程序：</p>
<pre><code>$go build -o helloworld-default main.go
</code></pre>
<p>之后，我们查看一下生成的可执行文件helloworld-default的文件属性：</p>
<pre><code>$file helloworld-default
helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-default
   不是动态可执行文件
</code></pre>
<p>我们看到，虽然CGO_ENABLED=1，但默认情况下，Go构建出的helloworld程序是静态链接的(statically linked)。</p>
<p>那么默认情况下，Go编译器是否都会采用静态链接的方式来构建Go程序呢？我们给上面的main.go添加一行代码：</p>
<pre><code>// go-compilation/main-with-os-user.go

package main

import (
    "fmt"
    _ "os/user"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>和之前的hello, world不同的是，这段代码多了一行<strong>包的空导入</strong>，导入的是os/user这个包。</p>
<p>编译这段代码，我们得到helloworld-with-os-user可执行文件。</p>
<pre><code>$go build -o helloworld-with-os-user main-with-os-user.go
</code></pre>
<p>使用file和ldd检视文件helloworld-with-os-user：</p>
<pre><code>$file helloworld-with-os-user
helloworld-with-os-user: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-os-user
    linux-vdso.so.1 =&gt;  (0x00007ffcb8fd4000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007fb5d6fce000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007fb5d6c00000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb5d71ea000)
</code></pre>
<p>我们看到：<strong>一行新代码居然让helloworld从静态链接变为了动态链接</strong>，同时这也是如何编译出一个hello world版的动态链接Go程序的答案。</p>
<p>通过nm命令我们还可以查看Go程序依赖了哪些C库的符号：</p>
<pre><code>$nm -a helloworld-with-os-user |grep " U "
                 U abort
                 U __errno_location
                 U fprintf
                 U fputc
                 U free
                 U fwrite
                 U malloc
                 U mmap
                 U munmap
                 U nanosleep
                 U pthread_attr_destroy
                 U pthread_attr_getstack
                 U pthread_attr_getstacksize
                 U pthread_attr_init
                 U pthread_cond_broadcast
                 U pthread_cond_wait
                 U pthread_create
                 U pthread_detach
                 U pthread_getattr_np
                 U pthread_key_create
                 U pthread_mutex_lock
                 U pthread_mutex_unlock
                 U pthread_self
                 U pthread_setspecific
                 U pthread_sigmask
                 U setenv
                 U sigaction
                 U sigaddset
                 U sigemptyset
                 U sigfillset
                 U sigismember
                 U stderr
                 U strerror
                 U unsetenv
                 U vfprintf
</code></pre>
<p>由此，我们可以得到一个结论，在默认情况下(CGO_ENABLED=1)，Go会尽力使用静态链接的方式，但在某些情况下，会采用动态链接。那么究竟在哪些情况下会默认生成动态链接的程序呢？我们继续往下看。</p>
<h2>2. 在何种情况下默认会生成动态链接的Go程序？</h2>
<p>在以下几种情况下，Go编译器会默认(CGO_ENABLED=1)生成动态链接的可执行文件，我们逐一来看一下。</p>
<h3>2.1 一些使用C实现的标准库包</h3>
<p>根据上述示例，我们可以看到，在某些情况下，即使只依赖标准库，Go 仍会在CGO_ENABLED=1的情况下采用动态链接。这是因为代码依赖的标准库包使用了C版本的实现。虽然这种情况并不常见，但<a href="https://pkg.go.dev/os/user">os/user包</a>和<a href="https://pkg.go.dev/net">net包</a>是两个典型的例子。</p>
<p>os/user包的示例在前面我们已经见识过了。user包允许开发者通过名称或ID查找用户账户。对于大多数Unix系统(包括linux)，该包内部有两种版本的实现，用于解析用户和组ID到名称，并列出附加组ID。一种是用纯Go编写，解析/etc/passwd和/etc/group文件。另一种是基于cgo的，依赖于标准C库（libc）中的例程，如getpwuid_r、getgrnam_r和getgrouplist。当cgo可用(CGO_ENABLED=1)，并且特定平台的libc实现了所需的例程时，将使用基于cgo的（libc支持的）代码，即采用动态链接方式。</p>
<p>同样，net包在名称解析(Name Resolution，即域名或主机名对应IP查找)上针对大多数Unix系统也有两个版本的实现：一个是纯Go版本，另一个是基于C的版本。C版本会在cgo可用且特定平台实现了相关C函数(比如getaddrinfo和getnameinfo等)时使用。</p>
<p>下面是一个简单的使用net包并采用动态链接的示例：</p>
<pre><code>// go-compilation/main-with-net.go

package main

import (
    "fmt"
    _ "net"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>编译后，我们查看一下文件属性：</p>
<pre><code>$go build -o helloworld-with-net main-with-net.go 

$file helloworld-with-net
helloworld-with-net: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-net
    linux-vdso.so.1 =&gt;  (0x00007ffd75dfd000)
    libresolv.so.2 =&gt; /lib64/libresolv.so.2 (0x00007fdda2cf9000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007fdda2add000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007fdda270f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fdda2f13000)
</code></pre>
<p>我们看到C版本实现依赖了libresolv.so这个用于名称解析的C库。</p>
<p>由此可得，当Go在默认cgo开启时，一旦依赖了标准库中拥有C版本实现的包，比如os/user、net等，Go编译器会采用动态链接的方式编译Go可执行程序。</p>
<h3>2.2 显式使用cgo调用外部C程序</h3>
<p>如果使用cgo与外部C代码交互，那么生成的可执行文件必然会包含动态链接。下面我们来看一个调用cgo的简单示例。</p>
<p>首先，建立一个简单的C lib：</p>
<pre><code>// go-compilation/my-c-lib

$tree my-c-lib
my-c-lib
├── Makefile
├── mylib.c
└── mylib.h

// go-compilation/my-c-lib/Makefile

.PHONY:  all static

all:
        gcc -c -fPIC -o mylib.o mylib.c
        gcc -shared -o libmylib.so mylib.o
static:
        gcc -c -fPIC -o mylib.o mylib.c
        ar rcs libmylib.a mylib.o

// go-compilation/my-c-lib/mylib.h

#ifndef MYLIB_H
#define MYLIB_H

void hello();
int add(int a, int b);

#endif // MYLIB_H

// go-compilation/my-c-lib/mylib.c

#include &lt;stdio.h&gt;

void hello() {
    printf("Hello from C!\n");
}

int add(int a, int b) {
    return a + b;
}
</code></pre>
<p>执行make all构建出动态链接库libmylib.so！接下来，我们编写一个Go程序通过cgo调用libmylib.so中：</p>
<pre><code>// go-compilation/main-with-call-myclib.go 

package main

/*
#cgo CFLAGS: -I ./my-c-lib
#cgo LDFLAGS: -L ./my-c-lib -lmylib
#include "mylib.h"
*/
import "C"
import "fmt"

func main() {
    // 调用 C 函数
    C.hello()

    // 调用 C 中的加法函数
    result := C.add(3, 4)
    fmt.Printf("Result of addition: %d\n", result)
}
</code></pre>
<p>编译该源码：</p>
<pre><code>$go build -o helloworld-with-call-myclib main-with-call-myclib.go
</code></pre>
<p>通过ldd可以看到，可执行文件helloworld-with-call-myclib是动态链接的，并依赖libmylib.so：</p>
<pre><code>$ldd helloworld-with-call-myclib
    linux-vdso.so.1 =&gt;  (0x00007ffcc39d8000)
    libmylib.so =&gt; not found
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007f7166df5000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007f7166a27000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f7167011000)
</code></pre>
<p>设置LD_LIBRARY_PATH(为了让程序找到libmylib.so)并运行可执行文件helloworld-with-call-myclib：</p>
<pre><code>$ LD_LIBRARY_PATH=./my-c-lib:$LD_LIBRARY_PATH ./helloworld-with-call-myclib
Hello from C!
Result of addition: 7
</code></pre>
<h3>2.3 使用了依赖cgo的第三方包</h3>
<p>在日常开发中，我们经常依赖一些第三方包，有些时候这些第三方包依赖cgo，比如<a href="https://github.com/mattn/go-sqlite3">mattn/go-sqlite3</a>。下面就是一个依赖go-sqlite3包的示例：</p>
<pre><code>// go-compilation/go-sqlite3/main.go
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    // 打开数据库（如果不存在，则创建）
    db, err := sql.Open("sqlite3", "./test.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 创建表
    sqlStmt := `CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);`
    _, err = db.Exec(sqlStmt)
    if err != nil {
        log.Fatalf("%q: %s\n", err, sqlStmt)
    }

    // 插入数据
    _, err = db.Exec(`INSERT INTO user (name) VALUES (?)`, "Alice")
    if err != nil {
        log.Fatal(err)
    }

    // 查询数据
    rows, err := db.Query(`SELECT id, name FROM user;`)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        err = rows.Scan(&amp;id, &amp;name)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%d: %s\n", id, name)
    }

    // 检查查询中的错误
    if err = rows.Err(); err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>编译和运行该源码：</p>
<pre><code>$go build demo
$ldd demo
    linux-vdso.so.1 =&gt;  (0x00007ffe23d8e000)
    libdl.so.2 =&gt; /lib64/libdl.so.2 (0x00007faf0ddef000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007faf0dbd3000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007faf0d805000)
    /lib64/ld-linux-x86-64.so.2 (0x00007faf0dff3000)
$./demo
1: Alice
</code></pre>
<p>到这里，有些读者可能会问一个问题：如果需要在上述依赖场景中生成静态链接的Go程序，该怎么做呢？接下来，我们就来看看这个问题的解决细节。</p>
<h2>3. 如何在上述情况下实现静态链接？</h2>
<p>到这里是不是有些烧脑了啊！我们针对上一节的三种情况，分别对应来看一下静态编译的方案。</p>
<h3>3.1 仅依赖标准包</h3>
<p>在前面我们说过，之所以在使用os/user、net包时会在默认情况下采用动态链接，是因为Go使用了这两个包对应功能的C版实现，如果要做静态编译，让Go编译器选择它们的纯Go版实现即可。那我们仅需要关闭CGO即可，以依赖标准库os/user为例：</p>
<pre><code>$CGO_ENABLED=0 go build -o helloworld-with-os-user-static main-with-os-user.go
$file helloworld-with-os-user-static
helloworld-with-os-user-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-with-os-user-static
    不是动态可执行文件
</code></pre>
<h3>3.2 使用cgo调用外部c程序（静态链接）</h3>
<p>对于依赖cgo调用外部c的程序，我们要使用静态链接就必须要求外部c库提供静态库，因此，我们需要my-c-lib提供一份libmylib.a，这通过下面命令可以实现(或执行make static)：</p>
<pre><code>$gcc -c -fPIC -o mylib.o mylib.c
$ar rcs libmylib.a mylib.o
</code></pre>
<p>有了libmylib.a后，我们还要让Go程序静态链接该.a文件，于是我们需要修改一下Go源码中cgo链接的flag，加上静态链接的选项：</p>
<pre><code>// go-compilation/main-with-call-myclib-static.go
... ...
#cgo LDFLAGS: -static -L my-c-lib -lmylib
... ...
</code></pre>
<p>编译链接并查看一下文件属性：</p>
<pre><code>$go build -o helloworld-with-call-myclib-static main-with-call-myclib-static.go

$file helloworld-with-call-myclib-static
helloworld-with-call-myclib-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=b3da3ed817d0d04230460069b048cab5f5bfc3b9, not stripped
</code></pre>
<p>我们得到了预期的结果！</p>
<h3>3.3 依赖使用cgo的外部go包（静态链接）</h3>
<p>最麻烦的是这类情况，要想实现静态链接，我们需要找出外部go依赖的所有c库的.a文件(静态共享库)。以我们的go-sqlite3示例为例，go-sqlite3是sqlite库的go binding，它依赖sqlite库，同时所有第三方c库都依赖libc，我们还要准备一份libc的.a文件，下面我们就先安装这些：</p>
<pre><code>$yum install -y gcc glibc-static sqlite-devel
... ...

已安装:
  sqlite-devel.x86_64 0:3.7.17-8.el7_7.1                                                                                          

更新完毕:
  glibc-static.x86_64 0:2.17-326.el7_9.3
</code></pre>
<p>接下来，我们就来以静态链接的方式在go-compilation/go-sqlite3-static下编译一下：</p>
<pre><code>$go build -tags 'sqlite_omit_load_extension' -ldflags '-linkmode external -extldflags "-static"' demo

$file ./demo
./demo: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=c779f5c3eaa945d916de059b56d94c23974ce61c, not stripped
</code></pre>
<p>这里命令行中的-tags &#8216;sqlite_omit_load_extension&#8217;用于禁用SQLite3的动态加载功能，确保更好的静态链接兼容性。而-ldflags &#8216;-linkmode external -extldflags “-static”&#8216;的含义是使用外部链接器(比如gcc linker)，并强制静态链接所有库。</p>
<p>我们再看完略烧脑的几个细节后，再来看一个略轻松的话题。</p>
<h2>4. Go编译出的可执行文件过大，能优化吗？</h2>
<p>Go编译出的二进制文件一般较大，一个简单的“Hello World”程序通常在2MB左右：</p>
<pre><code>$ls -lh helloworld-default
-rwxr-xr-x 1 root root 2.1M 11月  3 10:39 helloworld-default
</code></pre>
<p>这一方面是因为Go将整个runtime都编译到可执行文件中了，另一方面也是因为Go静态编译所致。那么在默认情况下，Go二进制文件的大小还有优化空间么？方法不多，有两种可以尝试：</p>
<ul>
<li>去除符号表和调试信息</li>
</ul>
<p>在编译时使用-ldflags=”-s -w”标志可以去除符号表和调试符号，其中-s用于去掉符号表和调试信息，-w用于去掉DWARF调试信息，这样能显著减小文件体积。以helloworld为例，可执行文件的size减少了近四成：</p>
<pre><code>$go build -ldflags="-s -w" -o helloworld-default-nosym main.go
$ls -l
-rwxr-xr-x 1 root root 2124504 11月  3 10:39 helloworld-default
-rwxr-xr-x 1 root root 1384600 11月  3 13:34 helloworld-default-nosym
</code></pre>
<ul>
<li>使用tinygo</li>
</ul>
<p><a href="https://github.com/tinygo-org/tinygo/">TinyGo</a>是一个Go语言的编译器，它专为资源受限的环境而设计，例如微控制器、WebAssembly和其他嵌入式设备。TinyGo的目标是提供一个轻量级的、能在小型设备上运行的Go运行时，同时尽可能支持Go语言的特性。tinygo的一大优点就是生成的二进制文件通常比标准Go编译器生成的文件小得多：</p>
<pre><code>$tinygo build -o helloworld-tinygo main.go
$ls -l
总用量 2728
-rwxr-xr-x  1 root root 2128909 11月  5 05:43 helloworld-default*
-rwxr-xr-x  1 root root  647600 11月  5 05:45 helloworld-tinygo*
</code></pre>
<p>我们看到：tinygo生成的可执行文件的size仅是原来的30%。</p>
<blockquote>
<p>注：虽然TinyGo在特定场景（如IoT和嵌入式开发）中非常有用，但在常规服务器环境中，由于生态系统兼容性、性能、调试支持等方面的限制，可能并不是最佳选择。对于需要高并发、复杂功能和良好调试支持的应用，标准Go仍然是更合适的选择。</p>
<p>注：这里使用的tinygo为0.34.0版本。</p>
</blockquote>
<h2>5. 未使用的符号是否会被编译到Go二进制文件中？</h2>
<p>到这里，相信读者心中也都会萦绕一些问题：到底哪些符号被编译到最终的Go二进制文件中了呢？未使用的符号是否会被编译到Go二进制文件中吗？在这一小节中，我们就来探索一下。</p>
<p>出于对Go的了解，我们已经知道无论是GOPATH时代，还是Go module时代，Go的编译单元始终是包(package)，一个包（无论包中包含多少个Go源文件）都会作为一个编译单元被编译为一个目标文件(.a)，然后Go链接器会将多个目标文件链接在一起生成可执行文件，因此如果一个包被依赖，那么它就会进入到Go二进制文件中，它内部的符号也会进入到Go二进制文件中。</p>
<p>那么问题来了！是否被依赖包中的所有符号都会被放到最终的可执行文件中呢？我们以最简单的helloworld-default为例，它依赖fmt包，并调用了fmt包的Println函数，我们看看Println这个符号是否会出现在最终的可执行文件中：</p>
<pre><code>$nm -a helloworld-default | grep "Println"
000000000048eba0 T fmt.(*pp).doPrintln
</code></pre>
<p>居然没有！我们初步怀疑是inline优化在作祟。接下来，关闭优化再来试试：</p>
<pre><code>$go build -o helloworld-default-noinline -gcflags='-l -N' main.go

$nm -a helloworld-default-noinline | grep "Println"
000000000048ec00 T fmt.(*pp).doPrintln
0000000000489ee0 T fmt.Println
</code></pre>
<p>看来的确如此！不过当使用”fmt.”去过滤helloworld-default-noinline的所有符号时，我们发现fmt包的一些常见的符号并未包含在其中，比如Printf、Fprintf、Scanf等。</p>
<p>这是因为Go编译器的一个重要特性：死码消除(dead code elimination)，即编译器会将未使用的代码和数据从最终的二进制文件中剔除。</p>
<p>我们再来继续探讨一个衍生问题：如果Go源码使用空导入方式导入了一个包，那么这个包是否会被编译到Go二进制文件中呢？其实道理是一样的，如果用到了里面的符号，就会存在，否则不会。</p>
<p>以空导入os/user为例，即便在CGO_ENABLED=0的情况下，因为没有使用os/user中的任何符号，在最终的二进制文件中也不会包含user包：</p>
<pre><code>$CGO_ENABLED=0 go build -o helloworld-with-os-user-noinline -gcflags='-l -N' main-with-os-user.go
[root@iZ2ze18rmx2avqb5xgb4omZ helloworld]# nm -a helloworld-with-os-user-noinline |grep user
0000000000551ac0 B runtime.userArenaState
</code></pre>
<p>但是如果是带有init函数的包，且init函数中调用了同包其他符号的情况呢？我们以expvar包为例看一下：</p>
<pre><code>// go-compilation/main-with-expvar.go

package main

import (
    _ "expvar"
    "fmt"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>编译并查看一下其中的符号：</p>
<pre><code>$go build -o helloworld-with-expvar-noinline -gcflags='-l -N' main-with-expvar.go
$nm -a helloworld-with-expvar-noinline|grep expvar
0000000000556480 T expvar.appendJSONQuote
00000000005562e0 T expvar.cmdline
00000000005561c0 T expvar.expvarHandler
00000000005568e0 T expvar.(*Func).String
0000000000555ee0 T expvar.Func.String
00000000005563a0 T expvar.init.0
00000000006e0560 D expvar..inittask
0000000000704550 d expvar..interfaceSwitch.0
... ...
</code></pre>
<p>除此之外，如果一个包即便没有init函数，但有需要初始化的全局变量，比如crypto包的hashes：</p>
<pre><code>// $GOROOT/src/crypto/crypto.go
var hashes = make([]func() hash.Hash, maxHash)
</code></pre>
<p>crypto包的相关如何也会进入最终的可执行文件中，大家自己动手不妨试试。下面是我得到的一些输出：</p>
<pre><code>$go build -o helloworld-with-crypto-noinline -gcflags='-l -N' main-with-crypto.go
$nm -a helloworld-with-crypto-noinline|grep crypto
00000000005517b0 B crypto.hashes
000000000048ee60 T crypto.init
0000000000547280 D crypto..inittask
</code></pre>
<p>有人会问：os/user包也有一些全局变量啊，为什么这些符号没有被包含在可执行文件中呢？比如：</p>
<pre><code>// $GOROOT/src/os/user/user.go
var (
    userImplemented      = true
    groupImplemented     = true
    groupListImplemented = true
)
</code></pre>
<p>这就要涉及Go包初始化的逻辑了。我们看到crypto包包含在可执行文件中的符号中有crypto.init和crypto..inittask这两个符号，显然这不是crypto包代码中的符号，而是Go编译器为crypto包自动生成的init函数和inittask结构。</p>
<p>Go编译器会为每个包生成一个init函数，即使包中没有显式定义init函数，同时<a href="https://go.dev/src/cmd/compile/internal/pkginit/init.go">每个包都会有一个inittask结构</a>，用于运行时的包初始化系统。当然这么说也不足够精确，如果一个包没有init函数、需要初始化的全局变量或其他需要运行时初始化的内容，则编译器不会为其生成init函数和inittask。比如上面的os/user包。</p>
<p>os/user包确实有上述全局变量的定义，但是这些变量是在编译期就可以确定值的常量布尔值，而且未被包外引用或在包内用于影响控制流。Go编译器足够智能，能够判断出这些初始化是”无副作用的”，不需要在运行时进行初始化。只有真正需要运行时初始化的包才会生成init和inittask。这也解释了为什么空导入os/user包时没有相关的init和inittask符号，而crypto、expvar包有的init.0和inittask符号。</p>
<h2>6. 如何快速判断Go项目是否依赖cgo？</h2>
<p>在使用开源Go项目时，我们经常会遇到项目文档中没有明确说明是否依赖Cgo的情况。这种情况下，如果我们需要在特定环境（比如CGO_ENABLED=0）下使用该项目，就需要事先判断项目是否依赖Cgo，有些时候还要快速地给出判断。</p>
<p>那究竟是否可以做到这种快速判断呢？我们先来看看一些常见的作法。</p>
<p>第一类作法是源码层面的静态分析。最直接的方式是检查源码中是否存在import “C”语句，这种引入方式是CGO使用的显著标志。</p>
<pre><code>// 在项目根目录中执行
$grep -rn 'import "C"' .
</code></pre>
<p>这个命令会递归搜索当前目录下所有文件，显示包含import “C”的行号和文件路径，帮助快速定位CGO的使用位置。</p>
<p>此外，CGO项目通常包含特殊的编译指令，这些指令以注释形式出现在源码中，比如前面见识过的#cgo CFLAGS、#cgo LDFLAGS等，通过对这些编译指令的检测，同样可以来判断项目是否依赖CGO。</p>
<p>不过第一类作法并不能查找出Go项目的依赖包是否依赖cgo。而找出直接依赖或间接依赖是否依赖cgo，我们需要工具帮忙，比如使用Go工具链提供的命令分析项目依赖：</p>
<pre><code>$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
</code></pre>
<p>其中ImportPath是依赖包的导入路径，而CgoFiles则是依赖中包含import “C”的Go源文件。我们以go-sqlite3那个依赖cgo的示例来验证一下：</p>
<pre><code>// cd go-compilation/go-sqlite3

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
runtime/cgo: [cgo.go]
github.com/mattn/go-sqlite3: [backup.go callback.go error.go sqlite3.go sqlite3_context.go sqlite3_load_extension.go sqlite3_opt_serialize.go sqlite3_opt_userauth_omit.go sqlite3_other.go sqlite3_type.go]
</code></pre>
<p>用空导入os/user的示例再来看一下：</p>
<pre><code>$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}'  main-with-os-user.go | grep -v '\[\]'
runtime/cgo: [cgo.go]
os/user: [cgo_lookup_cgo.go getgrouplist_unix.go]
</code></pre>
<p>我们知道os/user有纯go和C版本两个实现，因此上述判断只能说“对了一半”，当我关闭CGO_ENABLED时，Go编译器不会使用基于cgo的C版实现。</p>
<p>那是否在禁用cgo的前提下对源码进行一次编译便能验证项目是否对cgo有依赖呢？这样做显然谈不上是一种“快速”的方法，那是否有效呢？我们来对上面的go-sqlite3项目做一个测试，我们在关闭CGO_ENABLED时，编译一下该示例：</p>
<pre><code>// cd go-compilation/go-sqlite3
$ CGO_ENABLED=0 go build demo
</code></pre>
<p>我们看到，Go编译器并未报错！似乎该项目不需要cgo!  但真的是这样吗？我们运行一下编译后的demo可执行文件：</p>
<pre><code>$ ./demo
2024/11/03 22:10:36 "Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub": CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);
</code></pre>
<p>我们看到成功编译出来的程序居然出现运行时错误，提示需要cgo！</p>
<p>到这里，没有一种方法可以快速、精确的给出项目是否依赖cgo的判断。也许判断Go项目是否依赖CGO并没有捷径，需要从源码分析、依赖检查和构建测试等多个维度进行。</p>
<h2>7. 小结</h2>
<p>在本文中，我们深入探讨了Go语言编译过程中的几个重要细节，尤其是在静态链接和动态链接的选择上。通过具体示例，我们了解到：</p>
<ul>
<li>
<p>默认链接方式：尽管CGO_ENABLED默认值为1，Go编译器在大多数情况下会采用静态链接，只有在依赖特定的C库或标准库包时，才会切换到动态链接。</p>
</li>
<li>
<p>动态链接的条件：我们讨论了几种情况下Go会默认生成动态链接的可执行文件，包括依赖使用C实现的标准库包、显式使用cgo调用外部C程序，以及使用依赖cgo的第三方包。</p>
</li>
<li>
<p>实现静态链接：对于需要动态链接的场景，我们也提供了将其转为静态链接的解决方案，包括关闭CGO、使用静态库，以及处理依赖cgo的外部包的静态链接问题。</p>
</li>
<li>
<p>二进制文件优化：我们还介绍了如何通过去除符号表和使用TinyGo等方法来优化生成的Go二进制文件的大小，以满足不同场景下的需求。</p>
</li>
<li>
<p>符号编译与死码消除：最后，我们探讨了未使用的符号是否会被编译到最终的二进制文件中，并解释了Go编译器的死码消除机制。</p>
</li>
</ul>
<p>通过这些细节探讨，我希望能够帮助大家更好地理解Go编译的复杂性，并在实际开发中做出更明智的选择，亦能在面对Go编译相关问题时，提供有效的解决方案。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go-compilation">这里</a>下载。</p>
<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>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/11/11/some-details-about-go-compilation/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.21新特性前瞻</title>
		<link>https://tonybai.com/2023/04/26/go-1-21-foresight/</link>
		<comments>https://tonybai.com/2023/04/26/go-1-21-foresight/#comments</comments>
		<pubDate>Tue, 25 Apr 2023 22:39:57 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[clear]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.env]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[maps]]></category>
		<category><![CDATA[OnceFunc]]></category>
		<category><![CDATA[OnceValue]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[slices]]></category>
		<category><![CDATA[static]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[wasi]]></category>
		<category><![CDATA[wasip1]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[wazero]]></category>
		<category><![CDATA[WebAssembly]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[腾讯]]></category>
		<category><![CDATA[鹅厂]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3865</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/04/26/go-1-21-foresight Go 1.21版本正在如火如荼地开发当中，按照Go核心团队的一年两次的发布节奏来算，Go 1.21版本预计将在2023年8月发布(Go 1.20版本是在2023年2月份发布的)。 本文将和大家一起看看Go 1.21都会带来哪些新特性。不过由于目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.21的最终版本中，所以切记一切变更要以最终Go 1.21版本发布时为准。 在细数变化之前，我们先来看看Go语言的当前状态。 1. Go语言当前状态 在《2022年Go语言盘点》一文中，我们提到年初Go语言的2022年终排名为12位，同时TIOBE官方编辑也提到：“在新兴编程语言中，Go是唯一一个可能在未来冲入前十的后端编程语言”。Go语言的发展似乎应验了这一预测，在今年的3月份，Go就再次进入编程语言排行榜前十： 一个月后的四月初，TIOBE排行榜上，Go稳住了第10名的位次： 在国内，在鹅厂前不久发布的《2022年腾讯研发大数据报告》中， 在国内，继Go在2021年从C++手中夺过红旗首次登顶鹅厂最热门编程语言之后，在鹅厂前不久发布的《2022年腾讯研发大数据报告》中，Go蝉联鹅厂最热门编程语言，继续夯实在国内头部互联网公司内的优势地位： Go于2009年开源，在经历多年的宣传和鼓吹后，Go目前进入了平稳发展的阶段。疫情结束后，原先线上举办或取消的国内外的Go技术大会现在陆续又都开始恢复了，相信这会让更多开发人员接触到Go。像Go这样的能在世界各地持续多年举办技术大会的语言真是不多了。 接下来，我们就来聚焦到Go 1.21版本，挖掘一下这个版本都有哪些新特性。 2. 语言变化 目前Go 1.21版本里程碑中涉及语言变化的有大约2项，我们来看看。 2.1 增加clear预定义函数 Go 1.21增加了一个clear预定义函数用来做切片和map的clear操作，其原型如下： // $GOROOT/src/builtin.go // The clear built-in function clears maps and slices. // For maps, clear deletes all entries, resulting in an empty map. // For slices, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1-21-foresight-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/04/26/go-1-21-foresight">本文永久链接</a> &#8211; https://tonybai.com/2023/04/26/go-1-21-foresight</p>
<p><a href="https://github.com/golang/go/milestone/279">Go 1.21版本</a>正在如火如荼地开发当中，按照Go核心团队的一年两次的发布节奏来算，Go 1.21版本预计将在2023年8月发布(<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20/">Go 1.20版本</a>是在2023年2月份发布的)。</p>
<p>本文将和大家一起看看Go 1.21都会带来哪些新特性。不过由于目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.21的最终版本中，所以切记一切变更要以最终Go 1.21版本发布时为准。</p>
<p>在细数变化之前，我们先来看看Go语言的当前状态。</p>
<h2>1. Go语言当前状态</h2>
<p>在<a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">《2022年Go语言盘点》</a>一文中，我们提到年初Go语言的2022年终排名为12位，同时TIOBE官方编辑也提到：“在新兴编程语言中，Go是唯一一个可能在未来冲入前十的后端编程语言”。Go语言的发展似乎应验了这一预测，在今年的3月份，Go就再次进入编程语言排行榜前十：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1-21-foresight-2.png" alt="" /></p>
<p>一个月后的四月初，TIOBE排行榜上，Go稳住了第10名的位次：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1-21-foresight-3.png" alt="" /></p>
<p>在国内，在鹅厂前不久发布的《2022年腾讯研发大数据报告》中，</p>
<p>在国内，继Go在2021年从C++手中夺过红旗首次登顶鹅厂最热门编程语言之后，在鹅厂前不久发布的《2022年腾讯研发大数据报告》中，<a href="https://mp.weixin.qq.com/s/qk_byuLMYZWa8RwE3tq0rQ">Go蝉联鹅厂最热门编程语言</a>，继续夯实在国内头部互联网公司内的优势地位：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1-21-foresight-4.png" alt="" /></p>
<p>Go于2009年开源，在经历多年的宣传和鼓吹后，Go目前进入了平稳发展的阶段。疫情结束后，原先线上举办或取消的国内外的Go技术大会现在陆续又都开始恢复了，相信这会让更多开发人员接触到Go。像Go这样的能在世界各地持续多年举办技术大会的语言真是不多了。</p>
<p>接下来，我们就来聚焦到Go 1.21版本，挖掘一下这个版本都有哪些新特性。</p>
<h2>2. 语言变化</h2>
<p>目前Go 1.21版本里程碑中涉及语言变化的有大约2项，我们来看看。</p>
<h3>2.1 增加clear预定义函数</h3>
<p>Go 1.21<a href="https://github.com/golang/go/issues/56351">增加了一个clear预定义函数用来做切片和map的clear操作</a>，其原型如下：</p>
<pre><code>// $GOROOT/src/builtin.go

// The clear built-in function clears maps and slices.
// For maps, clear deletes all entries, resulting in an empty map.
// For slices, clear sets all elements up to the length of the slice
// to the zero value of the respective element type. If the argument
// type is a type parameter, the type parameter's type set must
// contain only map or slice types, and clear performs the operation
// implied by the type argument.
func clear[T ~[]Type | ~map[Type]Type1](t T)
</code></pre>
<p>clear是针对map和slice的操作函数，它的语义是什么呢？我们通过一个例子来看一下：</p>
<pre><code>package main

import "fmt"

func main() {
    var sl = []int{1, 2, 3, 4, 5, 6}
    fmt.Printf("before clear, sl=%v, len(sl)=%d, cap(sl)=%d\n", sl, len(sl), cap(sl))
    clear(sl)
    fmt.Printf("after clear, sl=%v, len(sl)=%d, cap(sl)=%d\n", sl, len(sl), cap(sl))

    var m = map[string]int{
        "tony": 13,
        "tom":  14,
        "amy":  15,
    }
    fmt.Printf("before clear, m=%v, len(m)=%d\n", m, len(m))
    clear(m)
    fmt.Printf("after clear, m=%v, len(m)=%d\n", m, len(m))
}
</code></pre>
<p>运行该程序：</p>
<pre><code>before clear, sl=[1 2 3 4 5 6], len(sl)=6, cap(sl)=6
after clear, sl=[0 0 0 0 0 0], len(sl)=6, cap(sl)=6
before clear, m=map[amy:15 tom:14 tony:13], len(m)=3
after clear, m=map[], len(m)=0
</code></pre>
<p>我们看到：</p>
<ul>
<li>针对slice，clear保持slice的长度和容量，但将所有slice内已存在的元素(len个)都置为元素类型的零值；</li>
<li>针对map，clear则是清空所有map的键值对，clear后，我们将得到一个empty map。</li>
</ul>
<h3>2.2 改变panic(nil)语义</h3>
<p>使用defer+recover捕获panic是Go语言唯一处理panic的方法，其典型模式如下：</p>
<pre><code>package main

import "fmt"

func foo() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("panicked: %v\n", err)
            return
        }
        fmt.Println("it's ok")
    }()

    panic("some error")
}

func main() {
    foo()
}
</code></pre>
<p>运行上面程序会输出：</p>
<pre><code>panicked: some error
</code></pre>
<p>例子中我们向panic传入了表示panic原因的字符串，panic的参数是一个interface{}类型，可以传入任意值，当然<strong>也可以传入nil</strong>。</p>
<p>比如上面例子，当我们给foo函数的panic调用传入nil时，我们将得到下面结果：</p>
<pre><code>it's ok
</code></pre>
<p>这可能会给开发者带去疑惑：明明是触发了panic，但函数却按照正常逻辑处理！2018年，前Go核心团队成员bradfitz就提出了一个issue：<a href="https://github.com/golang/go/issues/25448">spec: guarantee non-nil return value from recover</a>，提出当开发者调用panic(nil)时，recover应该返回某种runtime error，而不是nil。这个issue在今年被纳入了Go 1.21版本，现在<a href="https://go-review.googlesource.com/c/go/+/461956">该issue的实现</a>已经被merge到了主干。</p>
<p>新的实现在src/runtime/panic.go中定义了一个名为PanicNilError的新Error：</p>
<pre><code>// $GOROOT/src/runtime/panic.go

// A PanicNilError happens when code calls panic(nil).
//
// Before Go 1.21, programs that called panic(nil) observed recover returning nil.
// Starting in Go 1.21, programs that call panic(nil) observe recover returning a *PanicNilError.
// Programs can change back to the old behavior by setting GODEBUG=panicnil=1.
type PanicNilError struct {
    // This field makes PanicNilError structurally different from
    // any other struct in this package, and the _ makes it different
    // from any struct in other packages too.
    // This avoids any accidental conversions being possible
    // between this struct and some other struct sharing the same fields,
    // like happened in go.dev/issue/56603.
    _ [0]*PanicNilError
}

func (*PanicNilError) Error() string { return "panic called with nil argument" }
func (*PanicNilError) RuntimeError() {}
</code></pre>
<p>Go编译器会将panic(nil)替换为panic(new(runtime.PanicNilError))，这样我们用Go 1.21版本运行上面的程序，我们就会得到下面结果了：</p>
<pre><code>panicked: panic called with nil argument
</code></pre>
<p>如果你的遗留代码中调用了panic(nil)(注：显然这不是一种很idiomatic的作法)，升级到Go 1.21版本后你就要小心了。如果你想保留原先的panic(nil)行为，可以用GODEBUG=panicnil=1。</p>
<p>有童鞋可能会质疑这违反了<a href="https://go.dev/doc/go1compat">Go1兼容性承诺</a>，但实际上Go1兼容性规范保留了对语言规范中不一致或错误的修订权力，即便这种修订会导致遗留代码出现与原先不一致的行为。</p>
<h2>3. 编译器与工具链</h2>
<p>每个Go版本中，编译器和工具链的改动都不少，我们挑重点看一下：</p>
<h3>3.1 一些OS的最小支持版本的更新</h3>
<p>Go 1.21开始，go installer<a href="https://github.com/golang/go/issues/58105">支持最小macOS版本更新为10.15</a>，而<a href="https://go-review.googlesource.com/c/build/+/478616">最小Windows版本为Windows 10</a>。</p>
<h3>3.2 低版本的go编译器将拒绝编译高版本的go module</h3>
<p>从Go 1.21版本开始，<a href="https://github.com/golang/go/issues/59033">低版本的go编译器将拒绝编译高版本的go module</a>(go.mod中go version标识最低版本) ，这也是Russ Cox策划的<a href="https://go.googlesource.com/proposal/+/master/design/57001-gotoolchain.md">Go扩展的向前兼容性</a>提案的一部分。此外，<a href="https://github.com/golang/go/issues/57001">Go扩展向前兼容性</a>提案感觉比较复杂，可能不会全部在Go 1.21版本落地。</p>
<h3>3.3 支持WASI</h3>
<p>Go从<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">1.11版本</a>就开始支持将Go源码编译为wasm二进制文件，并在支持wasm的浏览器环境中运行。</p>
<p>不过WebAssembly绝不仅仅被设计为仅限于在Web浏览器中运行，核心的WebAssembly语言是独立于其周围环境的，WebAssembly完全可以通过API与外部世界互动。在Web上，它自然使用浏览器提供的现有Web API。然而，在浏览器之外，之前还没有一套标准的API可以让WebAssembly程序使用。这使得创建真正可移植的非Web WebAssembly程序变得困难。<a href="https://wasi.dev">WebAssembly System Interface(WASI)</a>是一个填补这一空白的倡议，它有一套干净的API，可以由多个引擎在多个平台上实现，并且不依赖于浏览器的功能（尽管它们仍然可以在浏览器中运行）。</p>
<p><a href="https://github.com/golang/go/issues/58141">Go 1.21将增加对WASI的支持</a>，初期先支持<a href="https://github.com/WebAssembly/WASI/blob/b44552d84267af4d5899ed32364966740ef1846e/legacy/preview1/docs.md">WASI Preview1版本</a>，之后会支持WASI Preview2版本，直至最终WASI API版本发布！目前我们可以使用GOOS=wasip1 GOARCH=wasm将Go源码编译为支持WASI的wasm程序，下面是一个例子：</p>
<pre><code>// main.go
package main            

func main() {
    println("hello")
}
</code></pre>
<p>下载最新go dev版本后(go install http://golang.org/dl/gotip@latest)，可以执行下面命令将main.go编译为wasm程序：</p>
<pre><code>$ GOARCH=wasm GOOS=wasip1 gotip build -o main.wasm main.go
</code></pre>
<p>开源的wasm运行时有很多，<a href="https://wazero.io/">wazero</a>是目前比较火的且使用纯Go实现的wasm运行时程序，安装wazero后，可以用来执行上面编译出来的main.wasm：</p>
<pre><code>$curl https://wazero.io/install.sh
$wazero run main.wasm
hello
</code></pre>
<h3>3.4 Go 1.21可能推出纯静态工具链，不再依赖glibc</h3>
<p>使用纯Go实现的net resolver，原先DNS的问题也将被解决，这样Go团队很可能<a href="https://github.com/golang/go/issues/57007">在构建工具链的时候使用CGO_ENABLED=0构建出静态工具链</a>，没有动态链接库的依赖。</p>
<h3>3.5 go test -c支持为多个包同时构建测试二进制程序</h3>
<p>Go 1.21版本之前，go test -c仅支持将单个包的测试代码编译为测试二进制程序，Go 1.21版本则<a href="https://github.com/golang/go/issues/15513">允许我们同时为多个包构建测试二进制程序</a>。</p>
<p>下面是官方给出的例子：</p>
<pre><code>$ go test -c -o /tmp ./pkg1 ./pkg2 ./pkg2
$ ls /tmp
pkg1.test pkg2.test pkg3.test
</code></pre>
<h3>3.6 <a href="https://github.com/golang/go/issues/57179">增加\$GOROOT/go.env</a></h3>
<p>今天使用go env -w命令修改的默认环境变量会写入：filepath.Join(os.UserConfigDir(), “go/env”)。在Mac上，这个路径是\$HOME/Library/Application Support/go/env；在Linux上，这个路径是\$HOME/.config/go/env。</p>
<p>Go 1.21将增加一个全局层次上的go.env，放在\$GOROOT下面，目前默认的go.env为：</p>
<pre><code>// $GOROOT/go.env

# This file contains the initial defaults for go command configuration.
# Values set by 'go env -w' and written to the user's go/env file override these.
# The environment overrides everything else.

# Use the Go module mirror and checksum database by default.
# See https://proxy.golang.org for details.
GOPROXY=https://proxy.golang.org,direct
GOSUMDB=sum.golang.org
</code></pre>
<p>我们仍然可以通过go env -w命令修改user级的env文件来覆盖上述配置，当然最高优先级的是OS用户环境变量，如果在OS用户环境变量文件(比如.bash_profile、.bashrc)中设置了Go的环境变量值，比如GOPROXY等，那么以OS用户环境变量为优先。</p>
<h2>4. 标准库</h2>
<p>我们接下来再来看看变更最多的一部分：标准库，我们将对主要变更项作简要介绍。</p>
<h3>4.1 slices和maps进入标准库</h3>
<p><a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18版本</a>泛型落地发布前的最后一刻，Rob Pike叫停了slices、maps等泛型包的入库，后来这两个包先放置在golang.org/x/exp下作为实验包。随着Go泛型日益成熟以及Go团队对泛型使用经验的增多，Go团队终于决定将<a href="https://github.com/golang/go/issues/57433">golang.org/x/exp/slices</a>和<a href="https://github.com/golang/go/issues/57436">golang.org/x/exp/maps</a>在Go 1.21版本中将挪入标准库。</p>
<h3>4.2 log/slog加入标准库</h3>
<p>log/slog是Go官方版结构化日志包，大致与uber的zap包相当。在我之前的一篇文章<a href="https://tonybai.com/2022/10/30/first-exploration-of-slog">《slog：Go官方版结构化日志包》</a>有对slog的详尽说明，大家可以移步到那篇文章看看。不过slog的proposal依旧很多，后续slog可能会有持续改进和变更，与那篇文章中的内容可能会有一些差异。</p>
<h3>4.3 <a href="https://github.com/golang/go/issues/56102">sync包增加OnceFunc、OnceValue和OnceValues</a></h3>
<p>在sync.Once的基础上，这个issue增加了三个与Once相关的”语法糖”API，用在一些对Once有需求的最常见的场景中。</p>
<h3>4.4 <a href="https://github.com/golang/go/issues/52600">增加testing.Testing函数</a></h3>
<p>Go 1.21为testing包增加了func Testing() bool函数，该函数可以用来报告当前程序是否是go test创建的测试程序。使用Testing函数，我们可以确保一些无需在单测阶段执行的函数不被执行。比如下面例子来自这个issue：</p>
<pre><code>// file/that/should/not/be/used/from/testing.go

func prodEnvironmentData() *Environment {
    if testing.Testing() {
        log.Fatal("Using production data in unit tests")
    }
    ....
}
</code></pre>
<h3>4.5 一些变更点</h3>
<ul>
<li><a href="https://github.com/golang/go/issues/56661">context: 增加为deadline或timeout context设置cancel原因的API</a> &#8211; https://github.com/golang/go/issues/56661</li>
<li><a href="https://github.com/golang/go/issues/55079">unicode升级到15.0版本</a> &#8211; https://github.com/golang/go/issues/55079</li>
</ul>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://github.com/golang/go/milestone/279">Go 1.21 milestone</a> &#8211; https://github.com/golang/go/milestone/279</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，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>著名云主机服务厂商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>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</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; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/04/26/go-1-21-foresight/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>后端程序员一定要看的语言大比拼：Java vs. Go vs. Rust</title>
		<link>https://tonybai.com/2020/05/01/comparison-between-java-go-and-rust/</link>
		<comments>https://tonybai.com/2020/05/01/comparison-between-java-go-and-rust/#comments</comments>
		<pubDate>Thu, 30 Apr 2020 17:01:20 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alpine]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JVM]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Maven]]></category>
		<category><![CDATA[musl]]></category>
		<category><![CDATA[OpenJDK]]></category>
		<category><![CDATA[REST]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[STW]]></category>
		<category><![CDATA[wrk]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[性能基准]]></category>
		<category><![CDATA[递归]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2901</guid>
		<description><![CDATA[这是Java，Go和Rust之间的比较。这不是基准测试，更多是对可执行文件大小、内存使用率、CPU使用率、运行时要求等的比较，当然还有一个小的基准测试，可以看到每秒处理的请求数量，我将尝试对这些数字进行有意义的解读。 为了尝试尽可能公平比较，我在此比较中使用每种语言编写了一个Web服务。Web服务非常简单，它提供了三个REST服务端点(endpoint)。 Web服务提供的服务端点 这三个Web服务的代码仓库托管在github上。 编译后的二进制文件尺寸 有关如何构建二进制文件的一些信息。对于Java，我使用maven-shade-plugin和mvn package命令将所有内容构建到一个大的jar中。对于Go，我使用go build。最后，我使用了cargo build &#8211;release构建Rust服务的二进制文件。 每个程序的大小（以兆字节为单位） 编译后的文件大小还取决于所选的库/依赖项，因此，如果依赖项的身躯臃肿，则编译后的程序也将难以幸免。在我的特定情况下，针对我选择的特定库，以上是程序编译后的大小。 在后续的一个单独小节中，我会把这三个程序都构建并打包为docker镜像，并列出它们的大小，以显示每种语言所需的运行时开销。下面有更多详细信息。 内存使用情况 空闲状态 每个应用程序在内存空闲时的内存使用情况 什么？Go和Rust版本显示空闲时内存占用量的条形图在哪里？好了，它们在那里，只有JVM启动的程序在空闲状态时消耗160 MB以上的内存，它什么也没做。Go应用程序仅使用0.86 MB，Rust应用也仅使用了0.36 MB。这是一个巨大的差异！在这里，Java使用的内存比Go和Rust应用使用的内存高出两个数量级，只是空占着内存却什么都不做。那是巨大的资源浪费。 服务REST请求 让我们使用wrk发起访问API的请求，并观察内存和CPU使用情况，以及在我的计算机上三个版本程序的每个端点每秒处理的请求数。 wrk -t2 -c400 -d30s http://127.0.0.1:8080/hello wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Jane wrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35 上面的wrk命令使用两个线程并在连接池中保持400个打开的连接，并重复调用GET端点，持续30秒。这里我仅使用两个线程，因为wrk和被测程序都在同一台计算机上运行，所以我不希望它们在可用资源（尤其是CPU）上相互竞争（太多）。 每个Web服务都经过单独测试，并且在每次运行之间都重新启动了Web服务。 以下是该程序的每个版本的三个运行中的最佳结果。 /hello 该端点返回Hello，World！信息。它分配字符串“ Hello，World！” 并将其序列化并以JSON格式返回。 /hello端点的CPU使用率 /hello端点的内存使用情况 /hello端点处理的每秒请求数 /greeting/{name} 该端点接受一个段路径参数{name}，然后格式化字符串“Hello,{name}!”，序列化并以JSON格式的问候消息返回。 /greeting端点的CPU使用率 /greeting端点的内存使用情况 /greeting端点处理的每秒请求数 /fibonacci/{number} 该端点接受一个段路径参数{number}，并返回序列化为JSON格式的斐波纳契数和输入数。 对于这个特定的端点，我选择以递归形式实现它。我毫不怀疑，迭代实现会产生更好的性能结果，并且出于生产目的，应该选择一种迭代形式，但是在生产代码中，有些情况下必须使用递归（并非专门用于计算第n个斐波那契数 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-1.png" alt="" /></p>
<p>这是<a href="https://tonybai.com/tag/java">Java</a>，<a href="https://tonybai.com/tag/go">Go</a>和Rust之间的比较。这不是<a href="https://tonybai.com/2015/08/25/go-debugging-profiling-optimization/">基准测试</a>，更多是对可执行文件大小、内存使用率、CPU使用率、运行时要求等的比较，当然还有一个小的基准测试，可以看到每秒处理的请求数量，我将尝试对这些数字进行有意义的解读。</p>
<p>为了尝试尽可能公平比较，我在此比较中使用每种语言编写了一个Web服务。Web服务非常简单，它提供了三个REST服务端点(endpoint)。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-2.png" alt="" /><br />
<center>Web服务提供的服务端点</center></p>
<p>这三个Web服务的代码仓库托管在<a href="https://github.com/dexterdarwich/ws-compare">github上</a>。</p>
<h2>编译后的二进制文件尺寸</h2>
<p>有关如何构建二进制文件的一些信息。对于Java，我使用<a href="http://maven.apache.org/plugins/maven-shade-plugin/">maven-shade-plugin</a>和<code>mvn package</code>命令将所有内容构建到一个大的jar中。对于Go，我使用go build。最后，我使用了cargo build &#8211;release构建Rust服务的二进制文件。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-3.png" alt="" /><br />
<center> 每个程序的大小（以兆字节为单位）</center></p>
<p>编译后的文件大小还取决于所选的库/依赖项，因此，如果依赖项的身躯臃肿，则编译后的程序也将难以幸免。在我的特定情况下，针对我选择的特定库，以上是程序编译后的大小。</p>
<p>在后续的一个单独小节中，我会把这三个程序都构建并打包为<a href="https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/">docker镜像</a>，并列出它们的大小，以显示每种语言所需的<a href="https://tonybai.com/2020/03/21/illustrated-tales-of-go-runtime-scheduler/">运行时</a>开销。下面有更多详细信息。</p>
<h2>内存使用情况</h2>
<h3>空闲状态</h3>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-4.png" alt="" /><br />
<center>每个应用程序在内存空闲时的内存使用情况</center></p>
<p>什么？Go和Rust版本显示空闲时内存占用量的条形图在哪里？好了，它们在那里，只有JVM启动的程序在空闲状态时消耗160 MB以上的内存，它什么也没做。Go应用程序仅使用0.86 MB，Rust应用也仅使用了0.36 MB。这是一个巨大的差异！在这里，Java使用的内存比Go和Rust应用使用的内存高出两个数量级，只是空占着内存却什么都不做。那是巨大的资源浪费。</p>
<h3>服务REST请求</h3>
<p>让我们使用<a href="https://github.com/wg/wrk">wrk</a>发起访问API的请求，并观察内存和CPU使用情况，以及在我的计算机上三个版本程序的每个端点每秒处理的请求数。</p>
<pre><code>wrk -t2 -c400 -d30s http://127.0.0.1:8080/hello
wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Jane
wrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35
</code></pre>
<p>上面的wrk命令使用两个线程并在连接池中保持400个打开的连接，并重复调用GET端点，持续30秒。这里我仅使用两个线程，因为wrk和被测程序都在同一台计算机上运行，所以我不希望它们在可用资源（尤其是CPU）上相互竞争（太多）。</p>
<p>每个Web服务都经过单独测试，并且在每次运行之间都重新启动了Web服务。</p>
<p>以下是该程序的每个版本的三个运行中的最佳结果。</p>
<ul>
<li>/hello</li>
</ul>
<p>该端点返回Hello，World！信息。它分配字符串“ Hello，World！” 并将其序列化并以JSON格式返回。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-5.png" alt="" /><br />
<center>/hello端点的CPU使用率</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-6.png" alt="" /><br />
<center>/hello端点的内存使用情况</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-7.png" alt="" /><br />
<center>/hello端点处理的每秒请求数</center></p>
<ul>
<li>/greeting/{name}</li>
</ul>
<p>该端点接受一个段路径参数{name}，然后格式化字符串“Hello,{name}!”，序列化并以JSON格式的问候消息返回。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-8.png" alt="" /><br />
<center>/greeting端点的CPU使用率</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-9.png" alt="" /><br />
<center>/greeting端点的内存使用情况</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-10.png" alt="" /><br />
<center>/greeting端点处理的每秒请求数</center></p>
<ul>
<li>/fibonacci/{number}</li>
</ul>
<p>该端点接受一个段路径参数{number}，并返回序列化为JSON格式的斐波纳契数和输入数。</p>
<p>对于这个特定的端点，我选择以递归形式实现它。我毫不怀疑，迭代实现会产生更好的性能结果，并且出于生产目的，应该选择一种迭代形式，但是在生产代码中，有些情况下必须使用递归（并非专门用于计算第n个斐波那契数 ）。为此，我希望该实现涉及大量CPU栈分配。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-11.png" alt="" /><br />
<center>/fibonacci端点的CPU使用率</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-12.png" alt="" /><br />
<center>/fibonacci端点的内存使用情况</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-13.png" alt="" /><br />
<center>/fibonacci端点处理的每秒请求数</center></p>
<p>在Fibonacci端点测试期间，Java是唯一一个有150个请求超时的实现，如下面wrk的输出所示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-14.png" alt="" /><br />
<center>超时时间</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-15.png" alt="" /><br />
<center>/fibonacci端点的延迟</center></p>
<h2>运行时大小</h2>
<p>为了模拟现实世界中的云原生应用程序，并避免“它仅可以在我的机器上运行！”，我分别为这三个应用程序创建了一个docker镜像。</p>
<p>Docker文件的源代码包含在代码库相应程序文件夹下。</p>
<p>作为我使用过的Java应用程序的基础镜像，openjdk:8-jre-alpine是已知大小最小的镜像之一，但是，这附带了一些警告，这些警告可能适用于您的应用程序，也可能不适用于您的应用程序，主要是alpine镜像在处理环境变量名称方面不是posix兼容的，因此您不能在Dockerfile中使用ENV中的（点）字符（不过这没什么大不了的），另一个是alpine Linux镜像是使用musl libc而不是<a href="https://tonybai.com/tag/glibc">glibc</a>编译的，这意味着如果您的应用程序依赖于需要glibc，它可能无法正常工作。不过，在这里，alpine镜像工作是正常的。</p>
<p>至于应用程序的Go版本和Rust版本，我已经对其进行了静态编译，这意味着它们不希望在运行时镜像中存在libc（glibc，musl…等），这也意味着它们不需要运行OS的基本镜像。因此，我使用了scratch docker镜像，这是一个no-op镜像，以零开销托管已编译的可执行文件。</p>
<p>我使用的Docker镜像的命名约定为{lang}/webservice。该应用程序的Java，Go和Rust版本的镜像大小分别为113、8.68和4.24 MB。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-16.png" alt="" /><br />
<center>最终Docker镜像大小</center></p>
<h2>结论</h2>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-17.png" alt="" /><br />
<center>三种语言的比较</center></p>
<p>在得出任何结论之前，我想指出这三种语言之间的关系。Java和Go都是支持垃圾回收的语言，但是Java会提前编译为在JVM上运行的字节码。启动Java应用程序时，JIT编译器会被调用以通过将字节码编译为本地代码来优化字节码，以提高应用程序的性能。</p>
<p>Go和Rust都提前编译为本地代码，并且在运行时不会进行进一步的优化。</p>
<p>Java和Go都是支持垃圾收集的语言，具有<strong>STW(停止世界)</strong>的副作用。这意味着，每当垃圾收集器运行时，它将停止应用程序，进行垃圾收集，并在完成后从停止的地方恢复应用程序。大多数垃圾收集器需要停止运行，但是有些实现似乎不需要这样做。</p>
<p>当Java语言在90年代创建时，其最大的卖点之一是<strong>一次编写，可在任何地方运行</strong>。当时这非常好，因为市场上没有很多虚拟化解决方案。如今，大多数CPU支持虚拟化，这种虚拟化抵消了使用某种语言进行开发的诱惑(该语言承诺可以运行在任何平台上)。Docker和其他解决方案以更为低廉的代价提供虚拟化。</p>
<p>在整个测试中，应用程序的Java版本比Go或Rust对应版本消耗了更多的内存，在前两个测试中，Java使用的内存大约增加了8000％。这意味着对于实际应用程序，Java应用程序的运行成本会更高。</p>
<p>对于前两个测试，Go应用程序使用的CPU比Java少20％，同时处理比java版多出38％的请求。另一方面，Rust版本使用的CPU比Go减少了57％，而处理的请求却增加了13％。</p>
<p>第三次测试在设计上是占用大量CPU的资源，因此我想从中挤出CPU的每一分。Go和Rust都比Java多使用了1％的CPU。而且我认为，如果wrk不是在同一台计算机上运行，那么这三个版本都会使CPU达到100%的上限值。在内存方面，Java使用的内存比Go和Rust多2000％。Java可以处理的请求比Go多出20％，而Rust可以处理的请求比Java多出15％。</p>
<p>在撰写本文时，Java编程语言已经存在了将近30年，这使得在市场上寻找Java开发人员变得相对容易。另一方面，Go和Rust都是相对较新的语言，因此与Java相比，自然而然的开发人员的数量更少些。不过，Go和Rust都拥有很大的吸引力，许多开发人员正在将它们用于新项目，并且有许多使用Go和Rust的生产中正在运行的项目，因为简单地说，就资源而言，它们比Java更有效。</p>
<p>在编写本文的程序时，我同时学习了Go和Rust。就我而言，Go的学习曲线很短，因为它是一种相对容易掌握的语言，并且与其他语言相比语法很小。我只用了几天就用Go编写了程序。关于Go需要注意的一件事是编译速度，我不得不承认，与Java/C/C++/Rust等其他语言相比，它的速度非常快。该程序的Rust版本花了我大约一个星期的时间来完成，我不得不说，大部分时间都花在弄清borrow checker向我要什么上。Rust具有严格的所有权规则，但是一旦掌握了Rust的所有权和借用概念，编译器错误消息就会突然变得更加有意义。违反借阅检查规则时，Rust编译器对您大吼的原因是因为编译器希望在编译时证明已分配内存的寿命和所有权。这样做可以保证程序的安全性（例如：没有悬挂的指针，除非使用了不安全(unsafe)的代码逃离检查），并且在编译时确定了释放位置，从而消除了垃圾收集器的需求和运行时成本。当然，这是以学习Rust的所有权系统为代价的。</p>
<p>在竞争方面，我认为Go是Java（通常是JVM语言）的直接竞争对手，但不是Rust的竞争对手。另一方面，Rust是Java，Go，C和C ++的重要竞争对手。</p>
<p>由于他们的效率，我看到了自己将会在Go和Rust中编写更多的程序，但是很可能在Rust中编写更多的程序。两者都非常适合Web服务，CLI，系统程序（..etc）开发。但是，Rust比Go具有根本优势。它不是垃圾收集的语言，与C和C++相比，它可以安全地编写代码。例如，Go并不是特别适合用于编写OS内核，而这里又是Rust的亮点，并与C/C ++竞争，因为它们是使用OS编写的长期存在和事实上的语言。Rust与C/C++竞争的另一种方式在嵌入式世界中，我将继续进行讨论。</p>
<p>感谢您的阅读！</p>
<p>本文翻译自<a href="https://medium.com/@dexterdarwich/comparison-between-java-go-and-rust-fdb21bd5fb7c">《Comparison between Java, Go, and Rust》</a>。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</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>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/05/01/comparison-between-java-go-and-rust/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>追求极简：Docker镜像构建演化史</title>
		<link>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/</link>
		<comments>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/#comments</comments>
		<pubDate>Wed, 20 Dec 2017 23:31:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alpine]]></category>
		<category><![CDATA[baseimage]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[busybox]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[CSDN]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[Dockerfile]]></category>
		<category><![CDATA[dotCloud]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[LXC]]></category>
		<category><![CDATA[multi-stage-build]]></category>
		<category><![CDATA[musl-libc]]></category>
		<category><![CDATA[namespaces]]></category>
		<category><![CDATA[scratch]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[unionfs]]></category>
		<category><![CDATA[多阶段构建]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[程序员杂志]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2495</guid>
		<description><![CDATA[本文首发于CSDN《程序员》杂志2017.12期，这里是原文地址。 本文为《程序员》杂志授权转载，谢绝其他转载。全文如下： 自从2013年dotCloud公司(现已改名为Docker Inc)发布Docker容器技术以来，到目前为止已经有四年多的时间了。这期间Docker技术飞速发展，并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没：镜像让容器真正插上了翅膀，实现了容器自身的重用和标准化传播，使得开发、交付、运维流水线上的各个角色真正围绕同一交付物，“test what you write, ship what you test”成为现实。 对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。 一、镜像：继承中的创新 谈镜像构建之前，我们先来简要说下镜像。 Docker技术本质上并不是新技术，而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在Sun公司的Solaris操作系统上，Solaris是当时最先进的服务器操作系统。2005年Sun发布了Solaris Container技术，从此开启了内核容器之门。 2008年，以Google公司开发人员为主导实现的Linux Container(即LXC)功能在被merge到Linux内核中。LXC是一种内核级虚拟化技术，主要基于Namespaces和Cgroups技术，实现共享一个操作系统内核前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。Docker也是基于Namespaces和Cgroups技术之上实现的，Docker的创新之处在于其基于Union File System技术定义了一套容器打包规范，真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去，而这种文件就被称为镜像（即image），原理见下图（引自Docker官网）： 图1：Docker镜像原理 镜像是容器的“序列化”标准，这一创新为容器的存储、重用和传输奠定了基础。并且“坐上了巨轮”的容器镜像可以传播到世界每一个角落，这无疑助力了容器技术的飞速发展。 与Solaris Container、LXC等早期内核容器技术不同，Docker为开发者提供了开发者体验良好的工具集，这其中就包括了用于镜像构建的Dockerfile以及一种用于编写Dockerfile领域特定语言。采用Dockerfile方式构建成为镜像构建的标准方法，其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用docker commit命令提交的镜像所不能比拟的。 二、“镜像是个筐”：初学者的认知 “镜像是个筐，什么都往里面装” &#8211; 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将httpserver.go这个源文件编译为httpd程序并通过镜像发布，考虑到被编译的源码并非本文重点，这里使用了一个极简的demo代码： //httpserver.go package main import ( "fmt" "net/http" ) func main() { fmt.Println("http daemon start") fmt.Println(" -&#62; listen on port:8080") http.ListenAndServe(":8080", nil) } 接下来，我们来编写一个用于构建目标image的Dockerfile： From ubuntu:14.04 RUN [...]]]></description>
			<content:encoded><![CDATA[<p>本文首发于<a href="https://www.csdn.net/">CSDN</a><a href="http://programmer.csdn.net/">《程序员》</a>杂志<a href="http://blog.csdn.net/qq_40027052/article/details/78720370">2017.12期</a>，这里是<a href="https://mp.weixin.qq.com/s/6--iyRTiAtpSpsLd0Tgf8w">原文地址</a>。</p>
<p>本文为《程序员》杂志授权转载，谢绝其他转载。全文如下：</p>
<p>自从2013年<a href="https://en.wikipedia.org/wiki/DotCloud">dotCloud公司</a>(现已改名为<a href="https://en.wikipedia.org/wiki/Docker,_Inc.">Docker Inc</a>)发布<a href="http://tonybai.com/tag/docker">Docker容器技术</a>以来，到目前为止已经有四年多的时间了。这期间<a href="https://en.wikipedia.org/wiki/Docker_(software)">Docker技术</a>飞速发展，并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没：镜像让容器真正插上了翅膀，实现了容器自身的重用和标准化传播，使得开发、交付、运维流水线上的各个角色真正围绕同一交付物，“test what you write, ship what you test”成为现实。</p>
<p>对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。</p>
<h3>一、镜像：继承中的创新</h3>
<p>谈镜像构建之前，我们先来简要说下<strong>镜像</strong>。</p>
<p>Docker技术本质上并不是新技术，而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在<a href="https://en.wikipedia.org/wiki/Sun_Microsystems">Sun公司</a>的<a href="https://en.wikipedia.org/wiki/Solaris_(operating_system)">Solaris操作系统</a>上，<a href="http://tonybai.com/tag/solaris">Solaris</a>是当时最先进的服务器操作系统。2005年Sun发布了<a href="https://en.wikipedia.org/wiki/Solaris_Containers">Solaris Container</a>技术，从此开启了内核容器之门。</p>
<p>2008年，以Google公司开发人员为主导实现的Linux Container(即<a href="https://en.wikipedia.org/wiki/LXC">LXC</a>)功能在被merge到<a href="https://www.kernel.org/">Linux内核</a>中。LXC是一种内核级虚拟化技术，主要基于<a href="https://en.wikipedia.org/wiki/Cgroups#NAMESPACE-ISOLATION">Namespaces</a>和<a href="https://en.wikipedia.org/wiki/Cgroups">Cgroups</a>技术，实现共享一个操作系统内核前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。Docker也是基于Namespaces和Cgroups技术之上实现的，Docker的<strong>创新之处</strong>在于其基于<a href="https://en.wikipedia.org/wiki/UnionFS">Union File System</a>技术定义了一套容器打包规范，真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去，而这种文件就被称为<strong>镜像</strong>（即image），原理见下图（引自Docker官网）：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-layers-and-container.png" alt="img{512x368}" /><br />
图1：Docker镜像原理</p>
<p>镜像是容器的“序列化”标准，这一创新为容器的存储、重用和传输奠定了基础。并且“坐上了巨轮”的容器镜像可以传播到世界每一个角落，这无疑助力了容器技术的飞速发展。</p>
<p>与<a href="https://en.wikipedia.org/wiki/Solaris_Containers">Solaris Container</a>、LXC等早期内核容器技术不同，Docker为开发者提供了开发者体验良好的工具集，这其中就包括了用于镜像构建的Dockerfile以及一种用于编写Dockerfile领域特定语言。采用Dockerfile方式构建成为镜像构建的标准方法，其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用docker commit命令提交的镜像所不能比拟的。</p>
<h3>二、“镜像是个筐”：初学者的认知</h3>
<p><strong>“镜像是个筐，什么都往里面装”</strong> &#8211; 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将httpserver.go这个源文件编译为httpd程序并通过镜像发布，考虑到被编译的源码并非本文重点，这里使用了一个极简的demo代码：</p>
<pre><code>//httpserver.go

package main

import (
        "fmt"
        "net/http"
)

func main() {
        fmt.Println("http daemon start")
        fmt.Println("  -&gt; listen on port:8080")
        http.ListenAndServe(":8080", nil)
}

</code></pre>
<p>接下来，我们来编写一个用于构建目标image的Dockerfile：</p>
<pre><code>From ubuntu:14.04

RUN apt-get update \
      &amp;&amp; apt-get install -y software-properties-common \
      &amp;&amp; add-apt-repository ppa:gophers/archive \
      &amp;&amp; apt-get update \
      &amp;&amp; apt-get install -y golang-1.9-go \
                            git \
      &amp;&amp; rm -rf /var/lib/apt/lists/*

ENV GOPATH /root/go
ENV GOROOT /usr/lib/go-1.9
ENV PATH="/usr/lib/go-1.9/bin:${PATH}"

COPY ./httpserver.go /root/httpserver.go
RUN go build -o /root/httpd /root/httpserver.go \
      &amp;&amp; chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>构建这个Image：</p>
<pre><code># docker build -t repodemo/httpd:latest .
//...构建输出这里省略...

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              183dbef8eba6        2 minutes ago       550MB
ubuntu                           14.04               dea1945146b9        2 months ago        188MB
</code></pre>
<p>整个镜像的构建过程因环境而定。如果您的网络速度一般，这个构建过程可能会花费你10多分钟甚至更多。最终如我们所愿，基于repodemo/httpd:latest这个镜像的容器可以正常运行：</p>
<pre><code># docker run repodemo/httpd
http daemon start
  -&gt; listen on port:8080

</code></pre>
<p>一个Dockerfile最终生产出一个镜像。Dockerfile由若干Command组成，每个Command执行结果都会单独形成一个layer。我们来探索一下构建出来的镜像：</p>
<pre><code># docker history 183dbef8eba6
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
183dbef8eba6        21 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["/root/httpd"]   0B
27aa721c6f6b        21 minutes ago      /bin/sh -c #(nop) WORKDIR /root                 0B
a9d968c704f7        21 minutes ago      /bin/sh -c go build -o /root/httpd /root/h...   6.14MB
... ...
aef7700a9036        30 minutes ago      /bin/sh -c apt-get update       &amp;&amp; apt-get...   356MB
.... ...
&lt;missing&gt;           2 months ago        /bin/sh -c #(nop) ADD file:8f997234193c2f5...   188MB

</code></pre>
<p>我们去除掉那些Size为0或很小的layer，我们看到三个size占比较大的layer，见下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-2.png" alt="img{512x368}" /><br />
图2：Docker镜像分层探索</p>
<p>虽然Docker引擎利用r缓存机制可以让同主机下非首次的镜像构建执行得很快，但是在Docker技术热情催化下的这种构建思路让docker镜像在存储和传输方面的优势荡然无存，要知道一个ubuntu-server 16.04的虚拟机ISO文件的大小也就不过600多MB而已。</p>
<h3>三、”理性的回归”：builder模式的崛起</h3>
<p>Docker使用者在新技术接触初期的热情“冷却”之后迎来了“理性的回归”。根据上面分层镜像的图示，我们发现最终镜像中包含构建环境是多余的，我们只需要在最终镜像中包含足够支撑httpd运行的运行环境即可，而base image自身就可以满足。于是我们应该去除不必要的中间层：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-3-1.png" alt="img{512x368}" /><br />
图3：去除不必要的分层</p>
<p>现在问题来了！如果不在同一镜像中完成应用构建，那么在哪里、由谁来构建应用呢？至少有两种方法：</p>
<ol>
<li>在本地构建并COPY到镜像中；</li>
<li>借助构建者镜像(builder image)构建。</li>
</ol>
<p>不过方法1本地构建有很多局限性，比如：本地环境无法复用、无法很好融入持续集成/持续交付流水线等。借助builder image进行构建已经成为Docker社区的一个最佳实践，Docker官方为此也推出了各种主流编程语言的官方base image，比如：<a href="http://tonybai.com/tag/go">go</a>、<a href="http://tonybai.com/tag/java">java</a>、node、<a href="http://tonybai.com/tag/python">python</a>以及<a href="http://tonybai.com/tag/ruby">ruby</a>等。借助builder image进行镜像构建的流程原理如下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-3-2.png" alt="img{512x368}" /><br />
图4：借助builder image进行镜像构建的流程图</p>
<p>通过原理图，我们可以看到整个目标镜像的构建被分为了两个阶段：</p>
<ol>
<li>第一阶段：构建负责编译源码的构建者镜像；</li>
<li>第二阶段：将第一阶段的输出作为输入，构建出最终的目标镜像。</li>
</ol>
<p>我们选择golang:1.9.2作为builder base image，构建者镜像的Dockerfile.build如下：</p>
<pre><code>// Dockerfile.build

FROM golang:1.9.2

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go
</code></pre>
<p>执行构建：</p>
<pre><code># docker build -t repodemo/httpd-builder:latest -f Dockerfile.build .
</code></pre>
<p>构建好的应用程序httpd放在了镜像repodemo/httpd-builder中的/go/src目录下，我们需要一些“胶水”命令来连接两个构建阶段，这些命令将httpd从<strong>构建者镜像</strong>中取出并作为下一阶段构建的输入：</p>
<pre><code># docker create --name extract-httpserver repodemo/httpd-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-builder
</code></pre>
<p>通过上面的命令，我们将编译好的httpd程序拷贝到了本地。下面是目标镜像的Dockerfile：</p>
<pre><code>//Dockerfile.target
From ubuntu:14.04

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>接下来我们来构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd:latest -f Dockerfile.target .
</code></pre>
<p>我们来看看这个镜像的“体格”：</p>
<pre><code># docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              e3d009d6e919        12 seconds ago      200MB
</code></pre>
<p>200MB！目标镜像的Size降为原来的 1/2 还多。</p>
<h3>四、“像赛车那样减去所有不必要的东西”：追求最小镜像</h3>
<p>前面我们构建出的镜像的Size已经缩小到200MB，但这还不够。200MB的“体格”在我们的网络环境下缓存和传输仍然很难令人满意。我们要为镜像进一步减重，减到尽可能的小，就像赛车那样，为了能减轻重量将所有不必要的东西都拆除掉：我们仅保留能支撑我们的应用运行的必要库、命令，其余的一律不纳入目标镜像。当然不仅仅是Size上的原因，小镜像还有额外的好处，比如：内存占用小，启动速度快，更加高效；不会因其他不必要的工具、库的漏洞而被攻击，减少了“攻击面”，更加安全。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-4-1.png" alt="img{512x368}" /><br />
图5：目标镜像还能更小些吗？</p>
<p>一般应用开发者不会从scratch镜像从头构建自己的base image以及目标镜像的，开发者会挑选适合的base image。一些“蝇量级”甚至是“草量级”的官方base image的出现为这种情况提供了条件。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-size.png" alt="img{512x368}" /><br />
图6：一些base image的Size比较(来自imagelayers.io截图)</p>
<p>从图中看，我们有两个选择：<a href="https://www.busybox.net/">busybox</a>和<a href="https://alpinelinux.org/">alpine</a>。</p>
<p>单从image的size上来说，busybox更小。不过busybox默认的libc实现是uClibc，而我们通常运行环境使用的libc实现都是glibc，因此我们要么选择静态编译程序，要么使用busybox:glibc镜像作为base image。</p>
<p>而 alpine image 是另外一种蝇量级 base image，它使用了比 glibc 更小更安全的 <a href="http://www.musl-libc.org/">musl libc</a> 库。 不过和 busybox image 相比，alpine image 体积还是略大。除了因为 musl比uClibc 大一些之外，alpine还在镜像中添加了自己的包管理系统apk，开发者可以使用apk在基于alpine的镜像中添 加需要的包或工具。因此，对于普通开发者而言，alpine image显然是更佳的选择。不过alpine使用的libc实现为<a href="http://www.musl-libc.org/">musl</a>，与基于glibc上编译出来的应用程序不兼容。如果直接将前面构建出的httpd应用塞入alpine，在容器启动时会遇到下面错误，因为加载器找不到glibc这个动态共享库文件：</p>
<pre><code>standard_init_linux.go:185: exec user process caused "no such file or directory"
</code></pre>
<p>对于Go应用来说，我们可以采用静态编译的程序，但一旦采用静态编译，也就意味着我们将失去一些libc提供的原生能力，比如：在linux上，你无法使用系统提供的DNS解析能力，只能使用Go自实现的DNS解析器。</p>
<p>我们还可以采用基于alpine的builder image，golang base image就提供了alpine 版本。 我们就用这种方式构建出一个基于alpine base image的极小目标镜像。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-4-2.png" alt="img{512x368}" /><br />
图7：借助 alpine builder image 进行镜像构建的流程图</p>
<p>我们新建两个用于 alpine 版本目标镜像构建的 Dockerfile：Dockerfile.build.alpine 和Dockerfile.target.alpine：</p>
<pre><code>//Dockerfile.build.alpine
FROM golang:alpine

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go

// Dockerfile.target.alpine
From alpine

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]

</code></pre>
<p>构建builder镜像：</p>
<pre><code>#  docker build -t repodemo/httpd-alpine-builder:latest -f Dockerfile.build.alpine .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED              SIZE
repodemo/httpd-alpine-builder    latest              d5b5f8813d77        About a minute ago   275MB
</code></pre>
<p>执行“胶水”命令：</p>
<pre><code># docker create --name extract-httpserver repodemo/httpd-alpine-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-alpine-builder
</code></pre>
<p>构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd-alpine -f Dockerfile.target.alpine .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-alpine            latest              895de7f785dd        13 seconds ago      16.2MB
</code></pre>
<p>16.2MB！目标镜像的Size降为不到原来的十分之一。我们得到了预期的结果。</p>
<h3>五、“要有光，于是便有了光”：对多阶段构建的支持</h3>
<p>至此，虽然我们实现了目标Image的最小化，但是整个构建过程却是十分繁琐，我们需要准备两个Dockerfile、需要准备“胶水”命令、需要清理中间产物等。作为Docker用户，我们希望用一个Dockerfile就能解决所有问题，于是就有了Docker引擎对多阶段构建(multi-stage build)的支持。注意：这个特性非常新，只有Docker 17.05.0-ce及以后的版本才能支持。</p>
<p>现在我们就按照“多阶段构建”的语法将上面的Dockerfile.build.alpine和Dockerfile.target.alpine合并到一个Dockerfile中：</p>
<pre><code>//Dockerfile

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o httpd ./httpserver.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/httpd .
RUN chmod +x /root/httpd

ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>Dockerfile的语法还是很简明和易理解的。即使是你第一次看到这个语法也能大致猜出六成含义。与之前Dockefile最大的不同在于在支持多阶段构建的Dockerfile中我们可以写多个“From baseimage”的语句了，每个From语句开启一个构建阶段，并且可以通过“as”语法为此阶段构建命名(比如这里的builder)。我们还可以通过COPY命令在两个阶段构建产物之间传递数据，比如这里传递的httpd应用，这个工作之前我们是使用“胶水”代码完成的。</p>
<p>构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd-multi-stage .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-multi-stage       latest              35e494aa5c6f        2 minutes ago       16.2MB
</code></pre>
<p>我们看到通过多阶段构建特性构建的Docker Image与我们之前通过builder模式构建的镜像在效果上是等价的。</p>
<h3>六、来到现实</h3>
<p>沿着时间的轨迹，Docker 镜像构建走到了今天。追求又快又小的镜像已成为了 Docker 社区 的共识。社区在自创 builder 镜像构建的最佳实践后终于迎来了多阶段构建这柄利器，从此构建 出极简的镜像将不再困难。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>理解Docker的多阶段镜像构建</title>
		<link>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/</link>
		<comments>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/#comments</comments>
		<pubDate>Sat, 11 Nov 2017 11:26:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alpine]]></category>
		<category><![CDATA[baseimage]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[CGO_ENABLED]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[DockerHub]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[httpserver]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[libc.a]]></category>
		<category><![CDATA[libc.so.6]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[multi-stage-build]]></category>
		<category><![CDATA[musl]]></category>
		<category><![CDATA[musl-libc]]></category>
		<category><![CDATA[strace]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可移植性]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2459</guid>
		<description><![CDATA[Docker技术从2013年诞生到目前已经4年有余了。对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但这是否意味着Docker的image构建机制已经相对完美了呢？不是的，Docker官方依旧在持续优化镜像构建机制。这不，从今年发布的Docker 17.05版本起，Docker开始支持容器镜像的多阶段构建(multi-stage build)了。 什么是镜像多阶段构建呢？直接给出概念定义太突兀，这里先卖个关子，我们先从日常开发中用到的镜像构建的方式和所遇到的镜像构建的问题说起。 一、同构的镜像构建 我们在做镜像构建时的一个常见的场景就是：应用在开发者自己的开发机或服务器上直接编译，编译出的二进制程序再打入镜像。这种情况一般要求编译环境与镜像所使用的base image是兼容的，比如说：我在Ubuntu 14.04上编译应用，并将应用打入基于ubuntu系列base image的镜像。这种构建我称之为“同构的镜像构建”，因为应用的编译环境与其部署运行的环境是兼容的：我在Ubuntu 14.04下编译出来的应用，可以基本无缝地在基于ubuntu:14.04及以后版本base image镜像(比如：16.04、16.10、17.10等)中运行；但在不完全兼容的base image中，比如centos中就可能会运行失败。 1、同构镜像构建举例 这里举个同构镜像构建的例子(后续的章节也是基于这个例子的)，注意：我们的编译环境为Ubuntu 16.04 x86_64虚拟机、Go 1.8.3和docker 17.09.0-ce。 我们用一个Go语言中最常见的http server作为例子： // github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/httpserver.go package main import ( "net/http" "log" "fmt" ) func home(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Welcome to this website!\n")) } func main() { http.HandleFunc("/", home) fmt.Println("Webserver start") fmt.Println(" -&#62; listen on port:1111") err := [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://tonybai.com/tag/docker">Docker</a>技术从<a href="https://www.infoq.com/news/2013/03/Docker">2013年诞生</a>到目前已经4年有余了。对于已经接纳和使用<a href="https://en.wikipedia.org/wiki/Docker_(software)">Docker技术</a>在日常开发工作中的开发者而言，构建<a href="https://docs.docker.com/get-started">Docker镜像</a>已经是家常便饭。但这是否意味着Docker的image构建机制已经相对完美了呢？不是的，Docker官方依旧在持续优化镜像构建机制。这不，从今年发布的<a href="https://github.com/moby/moby/releases/tag/v17.05.0-ce">Docker 17.05版本</a>起，Docker开始支持容器镜像的<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">多阶段构建(multi-stage build)</a>了。</p>
<p>什么是<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">镜像多阶段构建</a>呢？直接给出概念定义太突兀，这里先卖个关子，我们先从日常开发中用到的镜像构建的方式和所遇到的镜像构建的问题说起。</p>
<h2>一、同构的镜像构建</h2>
<p>我们在做镜像构建时的一个常见的场景就是：应用在开发者自己的开发机或服务器上直接编译，编译出的二进制程序再打入镜像。这种情况一般要求编译环境与镜像所使用的base image是兼容的，比如说：我在<a href="https://hub.docker.com/_/ubuntu/">Ubuntu 14.04</a>上编译应用，并将应用打入基于<a href="https://hub.docker.com/_/ubuntu/">ubuntu系列base image</a>的镜像。这种构建我称之为“同构的镜像构建”，因为应用的编译环境与其部署运行的环境是兼容的：我在Ubuntu 14.04下编译出来的应用，可以基本无缝地在基于ubuntu:14.04及以后版本base image镜像(比如：16.04、16.10、17.10等)中运行；但在不完全兼容的base image中，比如<a href="https://hub.docker.com/_/centos/">centos</a>中就可能会运行失败。</p>
<h3>1、同构镜像构建举例</h3>
<p>这里举个同构镜像构建的例子(后续的章节也是基于这个例子的)，注意：我们的编译环境为<strong>Ubuntu 16.04 x86_64虚拟机、<a href="http://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8.3</a>和docker 17.09.0-ce</strong>。</p>
<p>我们用一个Go语言中最常见的http server作为例子：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/httpserver.go
package main

import (
        "net/http"
        "log"
        "fmt"
)

func home(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Welcome to this website!\n"))
}

func main() {
        http.HandleFunc("/", home)
        fmt.Println("Webserver start")
        fmt.Println("  -&gt; listen on port:1111")
        err := http.ListenAndServe(":1111", nil)
        if err != nil {
                log.Fatal("ListenAndServe:", err)
        }
}

</code></pre>
<p>编译这个程序：</p>
<pre><code># go build -o myhttpserver httpserver.go
# ./myhttpserver
Webserver start
  -&gt; listen on port:1111
</code></pre>
<p>这个例子看起来很简单，也没几行代码，但背后Go net/http包在底层做了大量的事情，包括很多系统调用，能够反映出应用与操作系统的“耦合”，这在后续的讲解中会体现出来。接下来我们就来为这个程序构建一个docker image，并基于这个image来启动一个myhttpserver容器。我们选择ubuntu:14.04作为base image：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile
From ubuntu:14.04

COPY ./myhttpserver /root/myhttpserver
RUN chmod +x /root/myhttpserver

WORKDIR /root
ENTRYPOINT ["/root/myhttpserver"]

执行构建：

# docker build -t myrepo/myhttpserver:latest .
Sending build context to Docker daemon  5.894MB
Step 1/5 : FROM ubuntu:14.04
 ---&gt; dea1945146b9
Step 2/5 : COPY ./myhttpserver /root/myhttpserver
 ---&gt; 993e5129c081
Step 3/5 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in 104d84838ab2
 ---&gt; ebaeca006490
Removing intermediate container 104d84838ab2
Step 4/5 : WORKDIR /root
 ---&gt; 7afdc2356149
Removing intermediate container 450ccfb09ffd
Step 5/5 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in 3182766e2a68
 ---&gt; 77f315e15f14
Removing intermediate container 3182766e2a68
Successfully built 77f315e15f14
Successfully tagged myrepo/myhttpserver:latest

# docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttpserver   latest              77f315e15f14        18 seconds ago      200MB

# docker run myrepo/myhttpserver
Webserver start
  -&gt; listen on port:1111

</code></pre>
<p>以上是最基本的image build方法。</p>
<p>接下来，我们可能会遇到如下需求：<br />
* 搭建一个Go程序的构建环境有时候是很耗时的，尤其是对那些依赖很多第三方开源包的Go应用来说，下载包就需要很长时间。我们最好将这些易变的东西统统打包到一个用于Go程序构建的builder image中；<br />
* 我们看到上面我们构建出的myrepo/myhttpserver image的SIZE是200MB，这似乎有些过于“庞大”了。虽然每个主机node上的docker有cache image layer的能力，但我们还是希望能build出更加精简短小的image。</p>
<h3>2、借助golang builder image</h3>
<p>Docker Hub上提供了一个带有go dev环境的官方<a href="https://hub.docker.com/_/golang/">golang image repository</a>，我们可以直接使用这个golang builder image来辅助构建我们的应用image；对于一些对第三方包依赖较多的Go应用，我们也可以以这个golang image为base image定制我们自己的专用builder image。</p>
<p>我们基于golang:latest这个base image构建我们的golang-builder image，我们编写一个Dockerfile.build用于build golang-builder image:</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.build
FROM golang:latest

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go
</code></pre>
<p>在同目录下构建golang-builder image:</p>
<pre><code># docker build -t myrepo/golang-builder:latest -f Dockerfile.build .
Sending build context to Docker daemon  5.895MB
Step 1/4 : FROM golang:latest
 ---&gt; 1a34fad76b34
Step 2/4 : WORKDIR /go/src
 ---&gt; 2361824677d3
Removing intermediate container 01d8f4e9f0c4
Step 3/4 : COPY httpserver.go .
 ---&gt; 1ff14bb0bc56
Step 4/4 : RUN go build -o myhttpserver ./httpserver.go
 ---&gt; Running in 37a1b76b7b9e
 ---&gt; 2ac5347bb923
Removing intermediate container 37a1b76b7b9e
Successfully built 2ac5347bb923
Successfully tagged myrepo/golang-builder:latest

REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
myrepo/golang-builder   latest              2ac5347bb923        3 minutes ago       739MB
</code></pre>
<p>接下来，我们就基于golang-builder中已经build完毕的myhttpserver来构建我们最终的应用image：</p>
<pre><code># docker create --name appsource myrepo/golang-builder:latest
# docker cp appsource:/go/src/myhttpserver ./
# docker rm -f appsource
# docker rmi myrepo/golang-builder:latest
# docker build -t myrepo/myhttpserver:latest .
</code></pre>
<p>这段命令的逻辑就是从基于golang-builder image启动的容器appsource中将已经构建完毕的myhttpserver拷贝到主机当前目录中，然后删除临时的container appsource以及上面构建的那个golang-builder image；最后的步骤和第一个例子一样，基于本地目录中的已经构建完的myhttpserver构建出最终的image。为了方便，你也可以将这一系列命令放到一个Makefile中去。</p>
<h3>3、使用size更小的alpine image</h3>
<p>builder image并不能帮助我们为最终的应用image“减重”，myhttpserver image的Size依旧停留在200MB。要想“减重”，我们需要更小的base image，我们选择了<a href="https://hub.docker.com/_/alpine/">alpine</a>。<a href="https://news.ycombinator.com/item?id=10782897">Alpine image</a>的size不到4M，再加上应用的size，最终应用Image的Size估计可以缩减到20M以下。</p>
<p>结合builder image，我们只需将Dockerfile的base image改为alpine:latest：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.alpine

From alpine:latest

COPY ./myhttpserver /root/myhttpserver
RUN chmod +x /root/myhttpserver

WORKDIR /root
ENTRYPOINT ["/root/myhttpserver"]
</code></pre>
<p>构建alpine版应用image:</p>
<pre><code># docker build -t myrepo/myhttpserver-alpine:latest -f Dockerfile.alpine .
Sending build context to Docker daemon  6.151MB
Step 1/5 : FROM alpine:latest
 ---&gt; 053cde6e8953
Step 2/5 : COPY ./myhttpserver /root/myhttpserver
 ---&gt; ca0527a62d39
Step 3/5 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in 28d0a8a577b2
 ---&gt; a3833af97b5e
Removing intermediate container 28d0a8a577b2
Step 4/5 : WORKDIR /root
 ---&gt; 667345b78570
Removing intermediate container fa59883e9fdb
Step 5/5 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in adcb5b976ca3
 ---&gt; 582fa2aedc64
Removing intermediate container adcb5b976ca3
Successfully built 582fa2aedc64
Successfully tagged myrepo/myhttpserver-alpine:latest

# docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttpserver-alpine   latest              582fa2aedc64        4 minutes ago       16.3MB
</code></pre>
<p>16.3MB，Size的确降下来了！我们基于该image启动一个容器，看应用运行是否有什么问题：</p>
<pre><code># docker run myrepo/myhttpserver-alpine:latest
standard_init_linux.go:185: exec user process caused "no such file or directory"
</code></pre>
<p>容器启动失败了！为什么呢？因为alpine image并非ubuntu环境的同构image。我们在下面详细说明。</p>
<h2>二、异构的镜像构建</h2>
<p>我们的image builder: myrepo/golang-builder:latest是基于golang:latest这个image。<a href="https://github.com/docker-library/golang/">golang base image</a>有两个模板：Dockerfile-debain.template和Dockerfile-alpine.template。而golang:latest是基于debian模板的，与ubuntu兼容。构建出来的myhttpserver对<a href="http://tonybai.com/2010/12/13/also-talk-about-shared-library/">动态共享链接库</a>的情况如下：</p>
<pre><code> # ldd myhttpserver
    linux-vdso.so.1 =&gt;  (0x00007ffd0c355000)
    libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffa8b36f000)
    libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffa8afa5000)
    /lib64/ld-linux-x86-64.so.2 (0x000055605ea5d000)
</code></pre>
<p><a href="https://www.debian.org/">debian</a>系的linux distribution使用了<a href="http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/">glibc</a>。但alpine则不同，<a href="https://alpinelinux.org/">alpine</a>使用的是<a href="http://www.musl-libc.org/">musl libc</a>的实现，因此当我们运行上面的那个容器时，<a href="http://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/">加载器</a>因找不到myhttpserver依赖的libc.so.6而失败退出。</p>
<p>这种构建环境与运行环境不兼容的情况我这里称之为“异构的镜像构建”。那么如何解决这个问题呢？我们继续看：</p>
<h3>1、静态构建</h3>
<p>在主流编程语言中，<a href="http://tonybai.com/2017/06/27/an-intro-about-go-portability/">Go的移植性</a>已经是数一数二的了，尤其是<a href="http://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5</a>之后，Go将runtime中的C代码都用Go重写了，对libc的依赖已经降到最低了，但仍有一些feature提供了两个版本的实现：<a href="http://tonybai.com/tag/c">C实现</a>和Go实现。并且默认情况下，即在CGO_ENABLED=1的情况下，程序和预编译的标准库都采用了C的实现。关于这方面的详细论述请参见我之前写的<a href="http://tonybai.com/2017/06/27/an-intro-about-go-portability/">《也谈Go的可移植性》</a>一文，这里就不赘述了。于是采用了不同libc实现的debian系和alpine系自然存在不兼容的情况。要解决这个问题，我们首先考虑对Go程序进行静态构建，然后将静态构建后的Go应用放入alpine image中。</p>
<p>我们修改一下Dockerfile.build，在编译Go源文件时加上CGO_ENABLED=0：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.build

FROM golang:latest

WORKDIR /go/src
COPY httpserver.go .

RUN CGO_ENABLED=0 go build -o myhttpserver ./httpserver.go
</code></pre>
<p>构建这个builder image：</p>
<pre><code># docker build -t myrepo/golang-static-builder:latest -f Dockerfile.build .
Sending build context to Docker daemon  4.096kB
Step 1/4 : FROM golang:latest
 ---&gt; 1a34fad76b34
Step 2/4 : WORKDIR /go/src
 ---&gt; 593cd9692019
Removing intermediate container ee005d487ad5
Step 3/4 : COPY httpserver.go .
 ---&gt; a095eb69e716
Step 4/4 : RUN CGO_ENABLED=0 go build -o myhttpserver ./httpserver.go
 ---&gt; Running in d9f3b3a6c36c
 ---&gt; c06fe8dccbad
Removing intermediate container d9f3b3a6c36c
Successfully built c06fe8dccbad
Successfully tagged myrepo/golang-static-builder:latest

# docker images
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
myrepo/golang-static-builder   latest              c06fe8dccbad        31 seconds ago      739MB

</code></pre>
<p>接下来，我们再基于golang-static-builder中已经build完毕的静态连接的myhttpserver来构建我们最终的应用image：</p>
<pre><code># docker create --name appsource myrepo/golang-static-builder:latest
# docker cp appsource:/go/src/myhttpserver ./
# ldd myhttpserver
    not a dynamic executable
# docker rm -f appsource
# docker rmi myrepo/golang-static-builder:latest
# docker build -t myrepo/myhttpserver-alpine:latest -f Dockerfile.alpine .
</code></pre>
<p>运行新image:</p>
<pre><code># docker run myrepo/myhttpserver-alpine:latest
Webserver start
  -&gt; listen on port:1111
</code></pre>
<p>Note: 我们可以用strace来证明静态连接时Go只使用的是Go自己的runtime实现，而并未使用到libc.a中的代码：</p>
<pre><code># CGO_ENABLED=0 strace -f go build httpserver.go 2&gt;&amp;1 | grep open | grep -o '/.*\.a'  &gt; go-static-build-strace-file-open.txt
</code></pre>
<p>打开<a href="http://tonybai.com/wp-content/uploads/go-static-build-strace-file-open.txt">go-static-build-strace-file-open.txt</a>文件查看文件内容，你不会找到libc.a这个文件（在Ubuntu下，一般libc.a躺在/usr/lib/x86_64-linux-gnu/下面），这说明go build根本没有尝试去open libc.a文件并获取其中的符号定义。</p>
<h3>2、使用alpine golang builder</h3>
<p>我们的Go应用运行在alpine based的container中，我们可以使用alpine golang builder来构建我们的应用(无需静态链接)。前面提到过golang有alpine模板：</p>
<pre><code>REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
golang                       alpine              9e3f14138abd        7 days ago          269MB
</code></pre>
<p>alpine版golang builder的Dockerfile内容如下：</p>
<pre><code>//github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.alpine.build

FROM golang:alpine

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go

</code></pre>
<p>后续的操作与前面golang builder的操作并不二致：利用alpine golang builder构建我们的应用，并将其打入alpine image，这里就不赘述了。</p>
<h2>三、多阶段镜像构建：提升开发者体验</h2>
<p>在Docker 17.05以前，我们都是像上面那样构建镜像的。你会发现即便采用异构image builder模式，我们也要维护两个Dockerfile，并且还要在docker build命令之外执行一些诸如从容器内copy应用程序、清理build container和build image等的操作。Docker社区看到了这个问题，于是实现了<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">多阶段镜像构建机制</a>（multi-stage）。</p>
<p>我们先来看一下针对上面例子，multi-stage build所使用Dockerfile：</p>
<pre><code>//github.com/bigwhite/experiments/multi_stage_image_build/multi_stages/Dockerfile

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/myhttpserver .
RUN chmod +x /root/myhttpserver

ENTRYPOINT ["/root/myhttpserver"]
</code></pre>
<p>看完这个Dockerfile的内容，你的第一赶脚是不是把之前的两个Dockerfile合并在一块儿了，每个Dockerfile单独作为一个“阶段”！事实也是这样，但这个Docker也多了一些新的语法形式，用于建立各个“阶段”之间的联系。针对这样一个Dockerfile，我们应该知道以下几点：</p>
<ul>
<li>支持Multi-stage build的Dockerfile在以往的多个build阶段之间建立内在连接，让后一个阶段构建可以使用前一个阶段构建的产物，形成一条构建阶段的chain；</li>
<li>Multi-stages build的最终结果仅产生一个image，避免产生冗余的多个临时images或临时容器对象，这正是我们所需要的：我们只要结果。</li>
</ul>
<p>我们来使用multi-stage来build一下上述例子：</p>
<pre><code># docker build -t myrepo/myhttserver-multi-stage:latest .
Sending build context to Docker daemon  3.072kB
Step 1/9 : FROM golang:alpine as builder
 ---&gt; 9e3f14138abd
Step 2/9 : WORKDIR /go/src
 ---&gt; Using cache
 ---&gt; 7a99431d1be6
Step 3/9 : COPY httpserver.go .
 ---&gt; 43a196658e09
Step 4/9 : RUN go build -o myhttpserver ./httpserver.go
 ---&gt; Running in 9e7b46f68e88
 ---&gt; 90dc73912803
Removing intermediate container 9e7b46f68e88
Step 5/9 : FROM alpine:latest
 ---&gt; 053cde6e8953
Step 6/9 : WORKDIR /root/
 ---&gt; Using cache
 ---&gt; 30d95027ee6a
Step 7/9 : COPY --from=builder /go/src/myhttpserver .
 ---&gt; f1620b64c1ba
Step 8/9 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in e62809993a22
 ---&gt; 6be6c28f5fd6
Removing intermediate container e62809993a22
Step 9/9 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in e4000d1dde3d
 ---&gt; 639cec396c96
Removing intermediate container e4000d1dde3d
Successfully built 639cec396c96
Successfully tagged myrepo/myhttserver-multi-stage:latest

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttserver-multi-stage   latest              639cec396c96        About an hour ago   16.3MB
</code></pre>
<p>我们来Run一下这个image：</p>
<pre><code># docker run myrepo/myhttserver-multi-stage:latest
Webserver start
  -&gt; listen on port:1111
</code></pre>
<h2>四、小结</h2>
<p>多阶段镜像构建可以让开发者通过一个Dockerfile，一次性地、更容易地构建出size较小的image，体验良好并且更容易接入CI/CD等自动化系统。不过当前多阶段构建仅是在Docker 17.05及之后的版本中才能得到支持。如果想学习和实践这方面功能，但又没有环境，可以使用<a href="https://labs.play-with-docker.com/">play-with-docker</a>提供的实验环境。</p>
<p><img src="http://tonybai.com/wp-content/uploads/labs-play-with-docker.png" alt="img{512x368}" /><br />
Play with Docker labs</p>
<blockquote>
<p>以上所有示例代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/multi_stage_image_build">这里</a>下载到。</p>
</blockquote>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>也谈Go的可移植性</title>
		<link>https://tonybai.com/2017/06/27/an-intro-about-go-portability/</link>
		<comments>https://tonybai.com/2017/06/27/an-intro-about-go-portability/#comments</comments>
		<pubDate>Tue, 27 Jun 2017 13:42:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[amd64]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CGO_ENABLED]]></category>
		<category><![CDATA[Concurrency]]></category>
		<category><![CDATA[Darwin]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[link]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[nm]]></category>
		<category><![CDATA[otool]]></category>
		<category><![CDATA[Portability]]></category>
		<category><![CDATA[Pthread]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[stdlib]]></category>
		<category><![CDATA[syscall]]></category>
		<category><![CDATA[Ubuntu]]></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>

		<guid isPermaLink="false">http://tonybai.com/?p=2353</guid>
		<description><![CDATA[Go有很多优点，比如：简单、原生支持并发等，而不错的可移植性也是Go被广大程序员接纳的重要因素之一。但你知道为什么Go语言拥有很好的平台可移植性吗？本着“知其然，亦要知其所以然”的精神，本文我们就来探究一下Go良好可移植性背后的原理。 一、Go的可移植性 说到一门编程语言可移植性，我们一般从下面两个方面考量： 语言自身被移植到不同平台的容易程度； 通过这种语言编译出来的应用程序对平台的适应性。 在Go 1.7及以后版本中，我们可以通过下面命令查看Go支持OS和平台列表： $go tool dist list android/386 android/amd64 android/arm android/arm64 darwin/386 darwin/amd64 darwin/arm darwin/arm64 dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm linux/arm64 linux/mips linux/mips64 linux/mips64le linux/mipsle linux/ppc64 linux/ppc64le linux/s390x nacl/386 nacl/amd64p32 nacl/arm netbsd/386 netbsd/amd64 netbsd/arm openbsd/386 openbsd/amd64 openbsd/arm plan9/386 plan9/amd64 plan9/arm solaris/amd64 windows/386 windows/amd64 从上述列表我们可以看出：从linux/arm64的嵌入式系统到linux/s390x的大型机系统，再到Windows、linux和darwin(mac)这样的主流操作系统、amd64、386这样的主流处理器体系，Go对各种平台和操作系统的支持不可谓不广泛。 Go官方似乎没有给出明确的porting guide，关于将Go语言porting到其他平台上的内容更多是在golang-dev这样的小圈子中讨论的事情。但就Go语言这么短的时间就能很好的支持这么多平台来看，Go的porting还是相对easy的。从个人对Go的了解来看，这一定程度上得益于Go独立实现了runtime。 runtime是支撑程序运行的基础。我们最熟悉的莫过于libc（C运行时），它是目前主流操作系统上应用最普遍的运行时，通常以动态链接库的形式(比如：/lib/x86_64-linux-gnu/libc.so.6)随着系统一并发布，它的功能大致有如下几个： 提供基础库函数调用，比如：strncpy； 封装syscall（注:syscall是操作系统提供的API口，当用户层进行系统调用时，代码会trap(陷入)到内核层面执行），并提供同语言的库函数调用，比如：malloc、fread等； [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://tonybai.com/tag/go">Go</a>有很多优点，比如：<a href="http://tonybai.com/2017/04/20/go-coding-in-go-way/">简单</a>、<a href="http://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/">原生支持并发</a>等，而不错的<a href="https://en.wikipedia.org/wiki/Software_portability">可移植性</a>也是Go被广大程序员接纳的重要因素之一。但你知道为什么Go语言拥有很好的平台可移植性吗？本着“知其然，亦要知其所以然”的精神，本文我们就来探究一下Go良好可移植性背后的原理。</p>
<h2>一、Go的可移植性</h2>
<p>说到一门编程语言可移植性，我们一般从下面两个方面考量：</p>
<ul>
<li>语言自身被移植到不同平台的容易程度；</li>
<li>通过这种语言编译出来的应用程序对平台的适应性。</li>
</ul>
<p>在<a href="http://tonybai.com/2016/06/21/some-changes-in-go-1-7/">Go 1.7</a>及以后版本中，我们可以通过下面命令查看Go支持OS和平台列表：</p>
<pre><code>$go tool dist list
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
darwin/arm
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/s390x
nacl/386
nacl/amd64p32
nacl/arm
netbsd/386
netbsd/amd64
netbsd/arm
openbsd/386
openbsd/amd64
openbsd/arm
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
</code></pre>
<p>从上述列表我们可以看出：从<strong>linux/arm64</strong>的嵌入式系统到<strong>linux/s390x</strong>的大型机系统，再到Windows、<a href="http://tonybai.com/tag/ubuntu">linux</a>和darwin(mac)这样的主流操作系统、amd64、386这样的主流处理器体系，Go对各种平台和操作系统的支持不可谓不广泛。</p>
<p>Go官方似乎没有给出明确的porting guide，关于将Go语言porting到其他平台上的内容更多是在<a href="https://groups.google.com/forum/#!forum/golang-dev">golang-dev</a>这样的小圈子中讨论的事情。但就Go语言这么短的时间就能很好的支持这么多平台来看，Go的porting还是相对easy的。从个人对Go的了解来看，这一定程度上得益于Go独立实现了runtime。</p>
<p><img src="http://tonybai.com/wp-content/uploads/go-runtime-vs-c-runtime.png" alt="img{512x368}" /></p>
<p>runtime是支撑程序运行的基础。我们最熟悉的莫过于libc（C运行时），它是目前主流操作系统上应用最普遍的运行时，通常以<a href="http://tonybai.com/2010/12/13/also-talk-about-shared-library/">动态链接库</a>的形式(比如：/lib/x86_64-linux-gnu/libc.so.6)随着系统一并发布，它的功能大致有如下几个：</p>
<ul>
<li>提供基础库函数调用，比如：<a href="http://tonybai.com/2009/04/15/glibc-strncpy-source-analysis/">strncpy</a>；</li>
<li>封装syscall（注:syscall是操作系统提供的API口，当用户层进行系统调用时，代码会trap(陷入)到内核层面执行），并提供同语言的库函数调用，比如：malloc、fread等；</li>
<li>提供程序启动入口函数，比如：linux下的__libc_start_main。</li>
</ul>
<p><a href="http://tonybai.com/2006/07/08/plauger-c-standard-lib-assert-header/">libc</a>等c runtime lib是很早以前就已经实现的了，甚至有些老旧的libc还是单线程的。一些从事c/c++开发多年的程序员早年估计都有过这样的经历：那就是链接runtime库时甚至需要选择链接支持多线程的库还是只支持单线程的库。除此之外，c runtime的版本也参差不齐。这样的c runtime状况完全不能满足go语言自身的需求；另外Go的目标之一是原生支持并发，并使用<a href="http://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/">goroutine模型</a>，c runtime对此是无能为力的，因为c runtime本身是基于线程模型的。综合以上因素，Go自己实现了runtime，并封装了syscall，为不同平台上的go user level代码提供封装完成的、统一的go标准库；同时Go runtime实现了对goroutine模型的支持。</p>
<p>独立实现的go runtime层将Go user-level code与OS syscall解耦，把Go porting到一个新平台时，将runtime与新平台的syscall对接即可(当然porting工作不仅仅只有这些)；同时，runtime层的实现基本摆脱了Go程序对libc的依赖，这样静态编译的Go程序具有很好的平台适应性。比如：一个compiled for linux amd64的Go程序可以很好的运行于不同linux发行版（centos、ubuntu）下。</p>
<blockquote>
<p>以下测试试验环境为:darwin amd64 <a href="http://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8</a>。</p>
</blockquote>
<h2>二、默认”静态链接”的Go程序</h2>
<p>我们先来写两个程序：hello.c和hello.go，它们完成的功能都差不多，在stdout上输出一行文字：</p>
<pre><code>//hello.c
#include &lt;stdio.h&gt;

int main() {
        printf("%s\n", "hello, portable c!");
        return 0;
}

//hello.go
package main

import "fmt"

func main() {
    fmt.Println("hello, portable go!")
}

</code></pre>
<p>我们采用“默认”方式分别编译以下两个程序：</p>
<pre><code>$cc -o helloc hello.c
$go build -o hellogo hello.go

$ls -l
-rwxr-xr-x    1 tony  staff     8496  6 27 14:18 helloc*
-rwxr-xr-x    1 tony  staff  1628192  6 27 14:18 hellogo*
</code></pre>
<p>从编译后的两个文件helloc和hellogo的size上我们可以看到hellogo相比于helloc简直就是“巨人”般的存在，其size近helloc的200倍。略微学过一些Go的人都知道，这是因为hellogo中包含了必需的go runtime。我们通过otool工具(linux上可以用ldd)查看一下两个文件的对外部动态库的依赖情况：</p>
<pre><code>$otool -L helloc
helloc:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
$otool -L hellogo
hellogo:

</code></pre>
<p>通过otool输出，我们可以看到hellogo并不依赖任何外部库，我们将hellog这个二进制文件copy到任何一个mac amd64的平台上，均可以运行起来。而helloc则依赖外部的动态库:/usr/lib/libSystem.B.dylib，而libSystem.B.dylib这个动态库还有其他依赖。我们通过nm工具可以查看到helloc具体是哪个函数符号需要由外部动态库提供：</p>
<pre><code>$nm helloc
0000000100000000 T __mh_execute_header
0000000100000f30 T _main
                 U _printf
                 U dyld_stub_binder
</code></pre>
<p>可以看到：_printf和dyld_stub_binder两个符号是未定义的(对应的前缀符号是U)。如果对hellog使用nm，你会看到大量符号输出，但没有未定义的符号。</p>
<pre><code>$nm hellogo
00000000010bb278 s $f64.3eb0000000000000
00000000010bb280 s $f64.3fd0000000000000
00000000010bb288 s $f64.3fe0000000000000
00000000010bb290 s $f64.3fee666666666666
00000000010bb298 s $f64.3ff0000000000000
00000000010bb2a0 s $f64.4014000000000000
00000000010bb2a8 s $f64.4024000000000000
00000000010bb2b0 s $f64.403a000000000000
00000000010bb2b8 s $f64.4059000000000000
00000000010bb2c0 s $f64.43e0000000000000
00000000010bb2c8 s $f64.8000000000000000
00000000010bb2d0 s $f64.bfe62e42fefa39ef
000000000110af40 b __cgo_init
000000000110af48 b __cgo_notify_runtime_init_done
000000000110af50 b __cgo_thread_start
000000000104d1e0 t __rt0_amd64_darwin
000000000104a0f0 t _callRet
000000000104b580 t _gosave
000000000104d200 T _main
00000000010bbb20 s _masks
000000000104d370 t _nanotime
000000000104b7a0 t _setg_gcc
00000000010bbc20 s _shifts
0000000001051840 t errors.(*errorString).Error
00000000010517a0 t errors.New
.... ...
0000000001065160 t type..hash.time.Time
0000000001064f70 t type..hash.time.zone
00000000010650a0 t type..hash.time.zoneTrans
0000000001051860 t unicode/utf8.DecodeRuneInString
0000000001051a80 t unicode/utf8.EncodeRune
0000000001051bd0 t unicode/utf8.RuneCount
0000000001051d10 t unicode/utf8.RuneCountInString
0000000001107080 s unicode/utf8.acceptRanges
00000000011079e0 s unicode/utf8.first

$nm hellogo|grep " U "

</code></pre>
<p>Go将所有运行需要的函数代码都放到了hellogo中，这就是所谓的“静态链接”。是不是所有情况下，Go都不会依赖外部动态共享库呢？我们来看看下面这段代码：</p>
<pre><code>//server.go
package main

import (
    "log"
    "net/http"
    "os"
)

func main() {
    cwd, err := os.Getwd()
    if err != nil {
        log.Fatal(err)
    }

    srv := &amp;http.Server{
        Addr:    ":8000", // Normally ":443"
        Handler: http.FileServer(http.Dir(cwd)),
    }
    log.Fatal(srv.ListenAndServe())
}
</code></pre>
<p>我们利用Go标准库的net/http包写了一个fileserver，我们build一下该server，并查看它是否有外部依赖以及未定义的符号：</p>
<pre><code>$go build server.go
-rwxr-xr-x    1 tony  staff  5943828  6 27 14:47 server*

$otool -L server
server:
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)

$nm server |grep " U "
                 U _CFArrayGetCount
                 U _CFArrayGetValueAtIndex
                 U _CFDataAppendBytes
                 U _CFDataCreateMutable
                 U _CFDataGetBytePtr
                 U _CFDataGetLength
                 U _CFDictionaryGetValueIfPresent
                 U _CFEqual
                 U _CFNumberGetValue
                 U _CFRelease
                 U _CFStringCreateWithCString
                 U _SecCertificateCopyNormalizedIssuerContent
                 U _SecCertificateCopyNormalizedSubjectContent
                 U _SecKeychainItemExport
                 U _SecTrustCopyAnchorCertificates
                 U _SecTrustSettingsCopyCertificates
                 U _SecTrustSettingsCopyTrustSettings
                 U ___error
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U ___stderrp
                 U _abort
                 U _fprintf
                 U _fputc
                 U _free
                 U _freeaddrinfo
                 U _fwrite
                 U _gai_strerror
                 U _getaddrinfo
                 U _getnameinfo
                 U _kCFAllocatorDefault
                 U _malloc
                 U _memcmp
                 U _nanosleep
                 U _pthread_attr_destroy
                 U _pthread_attr_getstacksize
                 U _pthread_attr_init
                 U _pthread_cond_broadcast
                 U _pthread_cond_wait
                 U _pthread_create
                 U _pthread_key_create
                 U _pthread_key_delete
                 U _pthread_mutex_lock
                 U _pthread_mutex_unlock
                 U _pthread_setspecific
                 U _pthread_sigmask
                 U _setenv
                 U _strerror
                 U _sysctlbyname
                 U _unsetenv

</code></pre>
<p>通过otool和nm的输出结果我们惊讶的看到：默认采用“静态链接”的Go程序怎么也要依赖外部的动态链接库，并且也包含了许多“未定义”的符号了呢？问题在于cgo。</p>
<h2>三、cgo对可移植性的影响</h2>
<p>默认情况下，Go的runtime环境变量CGO_ENABLED=1，即默认开始cgo，允许你在Go代码中调用C代码，Go的pre-compiled标准库的.a文件也是在这种情况下编译出来的。在$GOROOT/pkg/darwin_amd64中，我们遍历所有预编译好的标准库.a文件，并用nm输出每个.a的未定义符号，我们看到下面一些包是对外部有依赖的（动态链接）：</p>
<pre><code>=&gt; crypto/x509.a
                 U _CFArrayGetCount
                 U _CFArrayGetValueAtIndex
                 U _CFDataAppendBytes
                 ... ...
                 U _SecCertificateCopyNormalizedIssuerContent
                 U _SecCertificateCopyNormalizedSubjectContent
                 ... ...
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U __cgo_topofstack
                 U _kCFAllocatorDefault
                 U _memcmp
                 U _sysctlbyname

=&gt; net.a
                 U ___error
                 U __cgo_topofstack
                 U _free
                 U _freeaddrinfo
                 U _gai_strerror
                 U _getaddrinfo
                 U _getnameinfo
                 U _malloc

=&gt; os/user.a
                 U __cgo_topofstack
                 U _free
                 U _getgrgid_r
                 U _getgrnam_r
                 U _getgrouplist
                 U _getpwnam_r
                 U _getpwuid_r
                 U _malloc
                 U _realloc
                 U _sysconf

=&gt; plugin.a
                 U __cgo_topofstack
                 U _dlerror
                 U _dlopen
                 U _dlsym
                 U _free
                 U _malloc
                 U _realpath$DARWIN_EXTSN

=&gt; runtime/cgo.a
                 ... ...
                 U _abort
                 U _fprintf
                 U _fputc
                 U _free
                 U _fwrite
                 U _malloc
                 U _nanosleep
                 U _pthread_attr_destroy
                 U _pthread_attr_getstacksize
                 ... ...
                 U _setenv
                 U _strerror
                 U _unsetenv

=&gt; runtime/race.a
                 U _OSSpinLockLock
                 U _OSSpinLockUnlock
                 U __NSGetArgv
                 U __NSGetEnviron
                 U __NSGetExecutablePath
                 U ___error
                 U ___fork
                 U ___mmap
                 U ___munmap
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U __dyld_get_image_header
                .... ...
</code></pre>
<p>我们以os/user为例，在CGO_ENABLED=1，即cgo开启的情况下，os/user包中的lookupUserxxx系列函数采用了c版本的实现，我们看到在$GOROOT/src/os/user/lookup_unix.go中的build tag中包含了<strong>+build cgo</strong>。这样一来，在CGO_ENABLED=1，该文件将被编译，该文件中的c版本实现的lookupUser将被使用：</p>
<pre><code>// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris
// +build cgo

package user
... ...
func lookupUser(username string) (*User, error) {
    var pwd C.struct_passwd
    var result *C.struct_passwd
    nameC := C.CString(username)
    defer C.free(unsafe.Pointer(nameC))
    ... ...
}

</code></pre>
<p>这样来看，凡是依赖上述包的Go代码最终编译的可执行文件都是要有外部依赖的。不过我们依然可以通过disable CGO_ENABLED来编译出纯静态的Go程序：</p>
<pre><code>$CGO_ENABLED=0 go build -o server_cgo_disabled server.go

$otool -L server_cgo_disabled
server_cgo_disabled:
$nm server_cgo_disabled |grep " U "
</code></pre>
<p>如果你使用build的 “-x -v”选项，你将看到go compiler会重新编译依赖的包的静态版本，包括net、mime/multipart、crypto/tls等，并将编译后的.a(以包为单位)放入临时编译器工作目录($WORK)下，然后再静态连接这些版本。</p>
<h2>四、internal linking和external linking</h2>
<p>问题来了：在CGO_ENABLED=1这个默认值的情况下，是否可以实现纯静态连接呢？答案是可以。在$GOROOT/cmd/cgo/doc.go中，文档介绍了cmd/link的两种工作模式：internal linking和external linking。</p>
<h3>1、internal linking</h3>
<p>internal linking的大致意思是若用户代码中仅仅使用了net、os/user等几个标准库中的依赖cgo的包时，cmd/link默认使用internal linking，而无需启动外部external linker(如:gcc、clang等)，不过由于cmd/link功能有限，仅仅是将.o和pre-compiled的标准库的.a写到最终二进制文件中。因此如果标准库中是在CGO_ENABLED=1情况下编译的，那么编译出来的最终二进制文件依旧是动态链接的，即便在go build时传入-ldflags &#8216;extldflags “-static”&#8216;亦无用，因为根本没有使用external linker：</p>
<pre><code>$go build -o server-fake-static-link  -ldflags '-extldflags "-static"' server.go
$otool -L server-fake-static-link
server-fake-static-link:
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
</code></pre>
<h3>2、external linking</h3>
<p>而external linking机制则是cmd/link将所有生成的.o都打到一个.o文件中，再将其交给外部的链接器，比如<a href="http://tonybai.com/tag/gcc">gcc</a>或clang去做最终链接处理。如果此时，我们在cmd/link的参数中传入-ldflags &#8216;extldflags “-static”&#8216;，那么gcc/clang将会去做静态链接，将.o中undefined的符号都替换为真正的代码。我们可以通过-linkmode=external来强制cmd/link采用external linker，还是以server.go的编译为例：</p>
<pre><code>$go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' server.go
# command-line-arguments
/Users/tony/.bin/go18/pkg/tool/darwin_amd64/link: running clang failed: exit status 1
ld: library not found for -lcrt0.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)
</code></pre>
<p>可以看到，cmd/link调用的clang尝试去静态连接libc的.a文件，但由于我的mac上仅仅有libc的dylib，而没有.a，因此静态连接失败。我找到一个ubuntu 16.04环境：重新执行上述构建命令：</p>
<pre><code># go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' server.go
# ldd server-static-link
    not a dynamic executable
# nm server-static-link|grep " U "
</code></pre>
<p>该环境下libc.a和libpthread.a分别在下面两个位置：</p>
<pre><code>/usr/lib/x86_64-linux-gnu/libc.a
/usr/lib/x86_64-linux-gnu/libpthread.a
</code></pre>
<p>就这样，我们在CGO_ENABLED=1的情况下，也编译构建出了一个纯静态链接的Go程序。</p>
<p>如果你的代码中使用了C代码，并依赖cgo在go中调用这些c代码，那么cmd/link将会自动选择external linking的机制：</p>
<pre><code>//testcgo.go
package main

//#include &lt;stdio.h&gt;
// void foo(char *s) {
//    printf("%s\n", s);
// }
// void bar(void *p) {
//    int *q = (int*)p;
//    printf("%d\n", *q);
// }
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    var s = "hello"
    C.foo(C.CString(s))

    var i int = 5
    C.bar(unsafe.Pointer(&amp;i))

    var i32 int32 = 7
    var p *uint32 = (*uint32)(unsafe.Pointer(&amp;i32))
    fmt.Println(*p)
}
</code></pre>
<p>编译testcgo.go：</p>
<pre><code># go build -o testcgo-static-link  -ldflags '-extldflags "-static"' testcgo.go
# ldd testcgo-static-link
    not a dynamic executable

vs.
# go build -o testcgo testcgo.go
# ldd ./testcgo
    linux-vdso.so.1 =&gt;  (0x00007ffe7fb8d000)
    libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc361000000)
    libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc360c36000)
    /lib64/ld-linux-x86-64.so.2 (0x000055bd26d4d000)

</code></pre>
<h2>五、小结</h2>
<p>本文探讨了Go的可移植性以及哪些因素对Go编译出的程序的移植性有影响：</p>
<ul>
<li>你的程序用了哪些标准库包？如果仅仅是非net、os/user等的普通包，那么你的程序默认将是纯静态的，不依赖任何c lib等外部动态链接库；</li>
<li>如果使用了net这样的包含cgo代码的标准库包，那么CGO_ENABLED的值将影响你的程序编译后的属性：是静态的还是动态链接的；</li>
<li>CGO_ENABLED=0的情况下，Go采用纯静态编译；</li>
<li>如果CGO_ENABLED=1，但依然要强制静态编译，需传递-linkmode=external给cmd/link。</li>
</ul>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/06/27/an-intro-about-go-portability/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>libiconv库链接问题一则</title>
		<link>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/</link>
		<comments>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/#comments</comments>
		<pubDate>Thu, 25 Apr 2013 10:04:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[iconv]]></category>
		<category><![CDATA[ld]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[libiconv]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Unix]]></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=1258</guid>
		<description><![CDATA[与在Solaris系统上不同，Linux的libc库中包含了libiconv库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的链接问题，到底是怎样一回事呢？这里分享一下问题查找过程。 一、现场重现 这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程： /*test.c */ &#8230; #include &#60;iconv.h&#62; int main(void) { &#160;&#160;&#160; int r; &#160;&#160;&#160; char *sin, *sout; &#160;&#160;&#160; size_t lenin, lenout; &#160;&#160;&#160; char *src = &#34;你好!&#34;; &#160;&#160;&#160; char dst[256] = {0}; &#160;&#160;&#160; iconv_t c_pt;&#160;&#160; &#160;&#160;&#160; sin = src; &#160;&#160;&#160; lenin = strlen(src)+1; &#160;&#160;&#160; sout = dst; &#160;&#160;&#160; lenout = 256; &#160;&#160;&#160; [...]]]></description>
			<content:encoded><![CDATA[<p>与在<a href="http://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/">Solaris</a>系统上不同，<a href="http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/">Linux</a>的libc库中包含了<a href="http://tonybai.com/2009/10/31/internal-code-transform-by-iconv/">libiconv</a>库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的<a href="http://tonybai.com/2007/12/08/those-things-about-symbol-linkage/">链接问题</a>，到底是怎样一回事呢？这里分享一下问题查找过程。</p>
<p><b>一、现场重现</b></p>
<p>这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程：<br />
	/*test.c */</p>
<p>&#8230;<br />
	<font face="Courier New">#include &lt;iconv.h&gt;</font></p>
<p><font face="Courier New">int main(void)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; int r;<br />
	&nbsp;&nbsp;&nbsp; char *sin, *sout;<br />
	&nbsp;&nbsp;&nbsp; size_t lenin, lenout;<br />
	&nbsp;&nbsp;&nbsp; char *src = &quot;你好!&quot;;<br />
	&nbsp;&nbsp;&nbsp; char dst[256] = {0};<br />
	&nbsp;&nbsp;&nbsp; iconv_t c_pt;&nbsp;&nbsp;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; sin = src;<br />
	&nbsp;&nbsp;&nbsp; lenin = strlen(src)+1;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; sout = dst;<br />
	&nbsp;&nbsp;&nbsp; lenout = 256;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ((c_pt = iconv_open(&quot;UTF-8&quot;, &quot;GB2312&quot;)) == (iconv_t)(-1)){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;iconv_open error!. errno[%d].\n&quot;, errno);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ((r = iconv(c_pt, (char **)&amp;sin, &amp;lenin, &amp;sout, &amp;lenout)) != 0){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;iconv error!. errno[%d].\n&quot;, r);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }&nbsp;&nbsp;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; iconv_close(c_pt);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; printf(&quot;SRC[%s], DST[%s].\n&quot;, src, dst);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return 0;<br />
	}</font></p>
<p>根据之前的经验，我们按如下命令编译该程序：</p>
<p><font face="Courier New">$&gt; gcc -g -o test test.c</font></p>
<p><font face="Courier New">/tmp/ccyQ5blC.o: In function `main&#39;:<br />
	/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open&#39;<br />
	/home/tonybai/tmp/test.c:33: undefined reference to `libiconv&#39;<br />
	/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close&#39;</font></p>
<p>咦，这是咋搞的呢？怎么找不到iconv库的符号！！！显式加上iconv的链接指示再试试。</p>
<p><font face="Courier New">$&gt; gcc -g -o test test.c -liconv</font></p>
<p>这回编译OK了。的确如那位同事所说出现了怪异的情况。</p>
<p><b>二、现场取证</b></p>
<p>惯性思维让我<b>首先</b>提出疑问：难道是这台机器上的<a href="http://www.gnu.org/s/libc/">libc</a>版本有差异，检查一下<a href="http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/">libc</a>中是否定义了iconv相关符号。</p>
<p><font face="Courier New">$ nm /lib64/libc.so.6 |grep iconv<br />
	000000397141e040 T iconv<br />
	000000397141e1e0 T iconv_close<br />
	000000397141ddc0 T iconv_open</font></p>
<p>iconv的函数都定义了呀！怎么会链接不到？</p>
<p>我们<b>再来</b>看看已经编译成功的那个test到底连接到哪个iconv库了。</p>
<p><font face="Courier New">$ ldd test<br />
	&nbsp;&nbsp;&nbsp; linux-vdso.so.1 =&gt;&nbsp; (0x00007fff77d6b000)<br />
	&nbsp;&nbsp;&nbsp; libiconv.so.2 =&gt; /usr/local/lib/libiconv.so.2 (0x00002abbeb09e000)<br />
	&nbsp;&nbsp;&nbsp; libc.so.6 =&gt; /lib64/libc.so.6 (0&#215;0000003971400000)<br />
	&nbsp;&nbsp;&nbsp; /lib64/ld-linux-x86-64.so.2 (0&#215;0000003971000000)</font></p>
<p>哦，系统里居然在/usr/local/lib下面单独安装了一份libiconv。gcc显然是链接到这里的libiconv了，但gcc怎么会链接到这里了呢？</p>
<p><b>三、</b><b>大侦探的分析^_^</b></p>
<p><a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/">Gcc</a>到底做了什么呢？我们看看其verbose的输出结果。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -liconv -v<br />
	使用内建 specs。<br />
	目标：x86_64-redhat-linux<br />
	配置为：../configure &#8211;prefix=/usr &#8211;mandir=/usr/share/man &#8211;infodir=/usr/share/info &#8211;enable-shared &#8211;enable-threads=posix &#8211;enable-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; checking=release &#8211;with-system-zlib &#8211;enable-__cxa_atexit &#8211;disable-libunwind-exceptions &#8211;enable-libgcj-multifile &#8211;enable-languages=c,c++,&nbsp;&nbsp; objc,obj-c++,java,fortran,ada &#8211;enable-java-awt=gtk &#8211;disable-dssi &#8211;disable-plugin &#8211;with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre &#8211;with-cpu=generic &#8211;host=x86_64-redhat-linux<br />
	线程模型：posix<br />
	gcc 版本 4.1.2 20080704 (Red Hat 4.1.2-50)<br />
	&nbsp;/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/cc1 -quiet -v test.c -quiet -dumpbase test.c -mtune=generic -auxbase test -g -version -o /tmp/&nbsp;&nbsp;&nbsp;&nbsp; ccypZm0v.s<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。<br />
	GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) (x86_64-redhat-linux)<br />
	&nbsp;&nbsp;&nbsp; 由 GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) 编译。<br />
	GGC 准则：&#8211;param ggc-min-expand=100 &#8211;param ggc-min-heapsize=131072<br />
	Compiler executable checksum: ef754737661c9c384f73674bd4e06594<br />
	&nbsp;as -V -Qy -o /tmp/ccaqvDgX.o /tmp/ccypZm0v.s<br />
	GNU assembler version 2.17.50.0.6-14.el5 (x86_64-redhat-linux) using BFD version 2.17.50.0.6-14.el5 20061020<br />
	&nbsp;/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/collect2 &#8211;eh-frame-hdr -m elf_x86_64 &#8211;hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.&nbsp; 2 -o test /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crti.o /usr/&nbsp;&nbsp; lib/gcc/x86_64-redhat-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/ x86_64-redhat-linux/4.1.2/../../../../lib64 -L/lib/../lib64<br />
	-L/usr/lib/../lib64 /tmp/ccaqvDgX.o -liconv -lgcc &#8211;as-needed -lgcc_s &#8211;no-as-needed -lc -lgcc &#8211;as-needed -lgcc_s &#8211;no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.1.2/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crtn.o</font></p>
<p>从这个结果来看，gcc在search iconv.h这个头文件时，首先找到的是/usr/local/include/iconv.h，而不是/usr/include/iconv.h。这两个文件有啥不同么？</p>
<p>在/usr/local/include/iconv.h中，我找到如下代码：</p>
<p><font face="Courier New">&#8230;</font><br />
	<font face="Courier New">#ifndef LIBICONV_PLUG<br />
	#define iconv_open libiconv_open<br />
	#endif<br />
	extern iconv_t iconv_open (const char* tocode, const char* fromcode);<br />
	&#8230;</font></p>
<p>libiconv_open vs iconv_open，卧槽！！！再对比一下前面编译时输出的错误信息：</p>
<p><font face="Courier New">/tmp/ccyQ5blC.o: In function `main&#39;:<br />
	/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open&#39;<br />
	/home/tonybai/tmp/test.c:33: undefined reference to `libiconv&#39;<br />
	/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close&#39;</font></p>
<p>大侦探醒悟了！大侦探带你还原一下真实情况。</p>
<p>我们在执行<font face="Courier New">gcc -g -o test test.c</font>时， 根据gcc -v中include search dir的顺序，gcc首先search到的是/usr/local/include/iconv.h，而这里iconv_open等函数被预编译器替换成 了libiconv_open等加上了lib前缀的函数，而这些函数符号显然在libc中是无法找到的，libc中只有不带lib前缀的 iconv_open等函数的定义。大侦探也是一时眼拙了，没有细致查看gcc的编译错误信息中的内容，这就是问题所在！</p>
<p>而<font face="Courier New">gcc -g -o test test.c -liconv</font>为何可以顺利编译通过呢？gcc是如何找到/usr/local/lib下的libiconv的呢？大侦探再次为大家还原一下真相。</p>
<p>我们在执行<font face="Courier New">gcc -g -o test test.c -liconv</font>时，gcc同 样首先search到的是/usr/local/include/iconv.h，然后编译test.c源码，ok；接下来启动ld程序进行链接；ld找 到了libiconv，ld是怎么找到iconv的呢，libiconv在/usr/local/lib下，ld显然是到这个目录下search了。我们 通过执行下面命令可以知晓ld的默认搜索路径：</p>
<p><font face="Courier New">$&gt; ld -verbose|grep SEARCH<br />
	SEARCH_DIR(&quot;/usr/x86_64-redhat-linux/lib64&quot;); SEARCH_DIR(&quot;/usr/local/lib64&quot;); SEARCH_DIR(&quot;/lib64&quot;); SEARCH_DIR(&quot;/usr/lib64&quot;); SEARCH_DIR(&quot;/usr/x86_64-redhat-linux/lib&quot;); SEARCH_DIR(&quot;/usr/lib64&quot;); SEARCH_DIR(&quot;/usr/local/lib&quot;); SEARCH_DIR(&quot;/lib&quot;); SEARCH_DIR(&quot;/usr/lib&quot;);</font></p>
<p>ld的默认search路径中有/usr/local/lib(我之前一直是以为/usr/local/lib不是gcc/ld的默认搜索路径的)，因此找到libiconv就不足为奇了。</p>
<p><b>四、问题解决</b></p>
<p>我们不想显式的加上-liconv，那如何解决这个问题呢？我们是否可以强制gcc先找到/usr/include/iconv.h呢？我们先来做个试验。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -liconv -I ~/include -v<br />
	&#8230;<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;<b>/home/</b><b>tonybai</b><b>/include</b><br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。</font></p>
<p><font face="Courier New">&#8230;</font></p>
<p>试验结果似乎让我们觉得可行，我们通过-I指定的路径被放在了第一的位置进行search。我们来尝试一下强制gcc先search /usr/include。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -I ~/include -v<br />
	&#8230;<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	忽略重复的目录&ldquo;/usr/include&rdquo;<br />
	&nbsp; 因为它是一个重复了系统目录的非系统目录<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。<br />
	&#8230;</font></p>
<p>糟糕！/usr/include被忽略了！还是从/usr/local/include开始，方案失败。</p>
<p>似乎剩下的唯一方案就是将/usr/local/lib下的那份libiconv卸载掉！那就这么做吧^_^！</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/25/a-libiconv-linkage-problem/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>简说GLIBC strncpy实现</title>
		<link>https://tonybai.com/2009/04/15/glibc-strncpy-source-analysis/</link>
		<comments>https://tonybai.com/2009/04/15/glibc-strncpy-source-analysis/#comments</comments>
		<pubDate>Wed, 15 Apr 2009 15:25:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[字符串]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[标准库]]></category>

		<guid isPermaLink="false">http://tonybai.com/2009/04/15/%e7%ae%80%e8%af%b4glibc-strncpy%e5%ae%9e%e7%8e%b0/</guid>
		<description><![CDATA[<p>比较以下两组代码，你认为哪组运行的更快些呢？<br />Example1：<br />&#160;&#160;&#160;&#160;&#160;&#160;&#160; int n&#160;&#160; = 100;<br />&#160;&#160;&#160;&#160;&#160;&#160;&#160; int n4&#160; = n &#62;&#62; 2;<br />&#160;&#160;&#160;&#160;&#160;&#160;&#160; int i&#160;&#38;nbsp...</p>]]></description>
			<content:encoded><![CDATA[<p>比较以下两组代码，你认为哪组运行的更快些呢？<br />
	Example1：<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int n&nbsp;&nbsp; = 100;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int n4&nbsp; = n &gt;&gt; 2;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int i&nbsp;&nbsp; = 0;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int a[100];</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (i = 0; i &lt; n4 ;i += 4) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a[i] = i;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a[i+1] = i+1;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a[i+2] = i+2;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a[i+3] = i+3;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>Example2：<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (i = 0;i &lt; 100;i++) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a[i] = i;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>其实这个问题在&quot;<a href="http://bigwhite.blogbus.com/logs/31299446.html" target="_blank">代码大全2nd</a>&quot;中也有讨论，从&quot;代码大全&quot;中的统计结果来看，一般来说Example1更占有优势。我在solaris上做了测试，在未开优化的情况下：两者运行时间分别为2ms和6ms；在打开<a href="http://bigwhite.blogbus.com/logs/2062606.html" target="_blank">-O2</a>优化后，两者均为1ms。这种通过减少循环次数的方法在GLIBC中也有体现，比如说strncpy的实现：</p>
<p>下面是strncpy的GLIBC源码：<br />
	char *<br />
	x_strncpy (s1, s2, n)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *s1;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const char *s2;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; size_t n;<br />
	{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; reg_char c;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *s = s1;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211;s1;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (n &gt;= 4)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; size_t n4 = n &gt;&gt; 2; /* n4 = n / 4， n4表示下面的循环执行的次数*/</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (;;)<br />
	&nbsp;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c = *s2++;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *++s1 = c;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (c == &#039;&#039;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c = *s2++;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *++s1 = c;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (c == &#039;&#039;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c = *s2++;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *++s1 = c;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (c == &#039;&#039;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c = *s2++;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *++s1 = c;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (c == &#039;&#039;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (&#8211;n4 == 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; goto last_chars;&nbsp; /* 如果n = 10，s2 = &quot;hello world&quot;，则两轮循环后，还有&quot;尾巴&quot;没有copy完，在last_chars处继续处理 */<br />
	&nbsp;&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; n = n &#8211; (s1 &#8211; s) &#8211; 1;&nbsp; /* 还没有copy完n个字节，s2就到达末尾了，跳到zero_fill处继续为s1补零 */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (n == 0)<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; return s;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; goto zero_fill;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>last_chars:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; n &#038;= 3;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* n = n &#038; 3 结果 n &lt;= 3，n即为上面循环过后&quot;尾巴字符&quot;的数量 */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (n == 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return s;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; do<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c = *s2++;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *++s1 = c;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (&#8211;n == 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return s;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } while (c != &#039;&#039;);</p>
<p>zero_fill:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; do<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *++s1 = &#039;&#039;;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; while (&#8211;n &gt; 0);</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return s;<br />
	}</p>
<p>相比于<a href="http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/" target="_blank">strlen的实现</a>，strncpy的实现更易理解。其字面上的逻辑就是每四个字节(n&gt;&gt;2)作为一组，每组逐个字节进行拷贝赋值，其内在目的则是减少循环次数，以获得性能的提升。要想知道为什么减少循环次数能提升性能的话，那就要深入到<a href="http://tonybai.com/2005/11/24/assembly-series-review-stack-operation/" target="_blank">汇编层面</a>去了，这里不再详述。另外还要一提的是GLIBC中的strncmp，strncat的实现也遵循着与上面同样的逻辑。</p>
<p style='text-align:left'>&copy; 2009, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2009/04/15/glibc-strncpy-source-analysis/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
