<?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; Compile</title>
	<atom:link href="http://tonybai.com/tag/compile/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 语言的“魔法”时刻：如何用 -toolexec 实现零侵入式自动插桩？</title>
		<link>https://tonybai.com/2026/01/19/unleashing-the-go-toolchain/</link>
		<comments>https://tonybai.com/2026/01/19/unleashing-the-go-toolchain/#comments</comments>
		<pubDate>Mon, 19 Jan 2026 00:07:38 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AbstractSyntaxTree]]></category>
		<category><![CDATA[AOP]]></category>
		<category><![CDATA[AOP配置]]></category>
		<category><![CDATA[AspectOrientedProgramming]]></category>
		<category><![CDATA[ast]]></category>
		<category><![CDATA[Autoinstrumentation]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[CompileTimeInjection]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[datadog]]></category>
		<category><![CDATA[ddtracego]]></category>
		<category><![CDATA[DistributedTracing]]></category>
		<category><![CDATA[GLS]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherConUK2025]]></category>
		<category><![CDATA[GoroutineLocalStorage]]></category>
		<category><![CDATA[KemalAkkoyun]]></category>
		<category><![CDATA[link]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[opentelemetry]]></category>
		<category><![CDATA[Orchestrion]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[toolchain]]></category>
		<category><![CDATA[toolexec]]></category>
		<category><![CDATA[上下文]]></category>
		<category><![CDATA[临时目录]]></category>
		<category><![CDATA[分布式追踪]]></category>
		<category><![CDATA[可观测性]]></category>
		<category><![CDATA[基础设施]]></category>
		<category><![CDATA[容器化友好]]></category>
		<category><![CDATA[工具链]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[手术式注入]]></category>
		<category><![CDATA[抽象语法树]]></category>
		<category><![CDATA[样板代码]]></category>
		<category><![CDATA[治理]]></category>
		<category><![CDATA[源码修改]]></category>
		<category><![CDATA[狸猫换太子]]></category>
		<category><![CDATA[监控体验]]></category>
		<category><![CDATA[维护性]]></category>
		<category><![CDATA[编译时注入]]></category>
		<category><![CDATA[编译过程拦截]]></category>
		<category><![CDATA[自动插桩]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[遗留代码]]></category>
		<category><![CDATA[隐式传递]]></category>
		<category><![CDATA[零侵入]]></category>
		<category><![CDATA[静态编译]]></category>
		<category><![CDATA[领域特定规则]]></category>
		<category><![CDATA[魔法时刻]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5743</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/01/19/unleashing-the-go-toolchain 大家好，我是Tony Bai。 “Go 语言以简洁著称，但在可观测性（Observability）领域，这种简洁有时却是一种负担。手动埋点、繁琐的初始化代码、版本升级带来的破坏性变更……这些都让 Gopher 们痛苦不已。 可观测性的三大支柱 相比之下，Java 和 Python 开发者享受着“零代码修改”的自动插桩福利。Go 开发者能否拥有同样的体验？ 在 GopherCon UK 2025 上，来自 DataDog 的资深工程师 Kemal Akkoyun 给出了肯定的答案。他通过挖掘 Go 工具链中一个鲜为人知的特性，不仅实现了这一目标，还将其开源为一个名为 Orchestrion 的工具。今天，就让我们一起揭秘这背后的“黑魔法”。 痛点：Go 语言的“反自动化”体质 在 Go 中集成分布式追踪（如 OpenTelemetry），通常意味着你需要： 手动修改代码：在 main 函数中初始化 Tracer Provider。 到处传递 Context：在每个函数签名中添加 ctx context.Context。 OpenTelemetry Go SDK难于集成。 样板代码爆炸：在每个关键路径上通过 defer span.End() 开启和结束 Span。 这种手动方式不仅效率低下，而且容易出错。如果有遗漏，追踪链路就会断裂；如果库升级，你可能需要重写大量代码。 与 Java [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/01/19/unleashing-the-go-toolchain">本文永久链接</a> &#8211; https://tonybai.com/2026/01/19/unleashing-the-go-toolchain</p>
<p>大家好，我是Tony Bai。</p>
<p>“Go 语言以简洁著称，但在可观测性（Observability）领域，这种简洁有时却是一种负担。手动埋点、繁琐的初始化代码、版本升级带来的破坏性变更……这些都让 Gopher 们痛苦不已。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-2.png" alt="" /><br />
<center>可观测性的三大支柱</center></p>
<p>相比之下，Java 和 Python 开发者享受着“零代码修改”的自动插桩福利。Go 开发者能否拥有同样的体验？</p>
<p>在 GopherCon UK 2025 上，来自 DataDog 的资深工程师 Kemal Akkoyun <a href="https://www.youtube.com/watch?v=8Rw-fVEjihw">给出了肯定的答案</a>。他通过挖掘 Go 工具链中一个鲜为人知的特性，不仅实现了这一目标，还将其开源为一个名为 <a href="https://github.com/DataDog/orchestrion"><strong>Orchestrion</strong></a> 的工具。今天，就让我们一起揭秘这背后的“黑魔法”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/distributed-system-guide-qr.png" alt="" /></p>
<h2>痛点：Go 语言的“反自动化”体质</h2>
<p>在 Go 中集成分布式追踪（如 OpenTelemetry），通常意味着你需要：</p>
<ul>
<li>手动修改代码：在 main 函数中初始化 Tracer Provider。</li>
<li>到处传递 Context：在每个函数签名中添加 ctx context.Context。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-3.png" alt="" /></p>
<ul>
<li>OpenTelemetry Go SDK难于集成。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-4.png" alt="" /></p>
<ul>
<li>样板代码爆炸：在每个关键路径上通过 defer span.End() 开启和结束 Span。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-5.png" alt="" /></p>
<p>这种手动方式不仅效率低下，而且容易出错。如果有遗漏，追踪链路就会断裂；如果库升级，你可能需要重写大量代码。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-6.png" alt="" /></p>
<p>与 Java Agent 的字节码注入或 Python 的动态装饰器不同，Go 是静态编译语言，运行时极其简单，没有虚拟机层面的“后门”可走。这似乎是一个死局。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-8.png" alt="" /></p>
<p>Gopher强烈希望 Go 也能像其他语言那样，轻松实现插桩从而注入追踪(trace)能力：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-7.png" alt="" /></p>
<h2>破局：编译时“大挪移”</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-9.png" alt="" /></p>
<p>Kemal 及其团队发现，Go 虽然没有运行时魔法，但在<strong>编译时</strong>却留了一扇窗：<strong>-toolexec 标志</strong>。</p>
<pre><code>$go help build|grep -A6 toolexec
    -toolexec 'cmd args'
        a program to use to invoke toolchain programs like vet and asm.
        For example, instead of running asm, the go command will run
        'cmd args /path/to/asm &lt;arguments for asm&gt;'.
        The TOOLEXEC_IMPORTPATH environment variable will be set,
        matching 'go list -f {{.ImportPath}}' for the package being built.
</code></pre>
<p>这是一个鲜为人知的 go build 参数。它允许你指定一个程序，拦截并包装构建过程中的每一个工具调用（如 compile、link、asm 等），让你可以在真正的compile、link 等之前对Go源码文件 (以compile等命令行工具的命令行参数形式传入) 做点什么。</p>
<p>为了让大家直观感受 -toolexec 的作用，我们先来看一个最简单的“拦截器”示例。</p>
<p>假设我们写了一个名为 mytool 的小程序，它的作用仅仅是打印出它接收到的命令，然后再原样执行该命令：</p>
<pre><code class="go">// mytool.go
package main

import (
    "fmt"
    "os"
    "os/exec"
)

func main() {
    // 注意：将日志打印到 Stderr，避免干扰 go build 读取工具的标准输出（如 Build ID）
    fmt.Fprintf(os.Stderr, "[Interceptor] Running: %v\n", os.Args[1:])

    // 原样执行被拦截的命令
    cmd := exec.Command(os.Args[1], os.Args[2:]...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        os.Exit(1)
    }
}
</code></pre>
<p>现在，当我们使用 -toolexec 参数来编译一个普通的 Go 程序时：</p>
<pre><code class="bash"># 先编译我们的拦截器
go build -o mytool mytool.go

# 使用拦截器来编译目标程序
go build -toolexec="./mytool" main.go  // 这里的main.go只是一个"hello, world"的Go程序
</code></pre>
<p>你会看到类似这样的输出：</p>
<pre><code class="text">[Interceptor] Running: /usr/local/go/pkg/tool/darwin_amd64/compile -o ...
[Interceptor] Running: /usr/local/go/pkg/tool/darwin_amd64/link -o ...
</code></pre>
<p>看到了吗？go build 并没有直接调用编译器，而是先调用了我们的 mytool，并将真正的编译器路径和参数作为参数传给了它。之后再调用回原命令，在上面示例执行完go build -toolexec=”./mytool” main.go后，我们同样看到了编译成功后的可执行二进制文件main。</p>
<p>这就给了我们一个惊人的机会：<strong>既然我们拦截了编译指令，我们当然可以修改它，甚至修改它即将编译的源文件！</strong></p>
<p>但是，仅仅打印几个日志、拦截一下命令，离真正的“自动插桩”还有很远的距离。要在真实复杂的 Go 项目中，安全、准确地修改成千上万行代码，同时还要处理依赖管理、缓存失效、语法兼容等棘手问题，绝非易事。</p>
<p>这正是 <strong>Orchestrion</strong> 登场的时刻。它不仅将 -toolexec 的潜力发挥到了极致，更将这套复杂的流程封装成了一个开箱即用的产品。</p>
<h2>深度解构：Orchestrion 的“编译时手术”</h2>
<p><strong>Orchestrion 是什么？</strong></p>
<p>简单来说，它是 DataDog 开源的一个<strong>编译时自动插桩工具</strong>。它的名字来源于一种模仿管弦乐队声音的机械乐器（Orchestrion），寓意它能像指挥家一样，协调并增强你的代码，而无需你亲自演奏每一个音符。</p>
<p>有了 -toolexec 这把钥匙，Orchestrion 就开启了一场编译时的“精密手术”。这不仅仅是简单的拦截，而是一场与 Go 编译器配合默契的“双人舞”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-12.png" alt="" /></p>
<p>安装下面图片中步骤，你就可以自动完成对你的go程序的插桩：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-13.png" alt="" /></p>
<p>Kemal 在演讲中展示了一个复杂的时序图，Orchestrion 的工作流远比我们想象的要精细：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-14.png" alt="" /></p>
<ol>
<li>
<p>精准拦截：<br />
当 go build 启动时，Orchestrion 守在门口。它并不关心链接器（linker）或汇编器（asm），它的目光紧紧锁定在 compile 命令上。每当 Go 编译器准备编译一个包（Package），Orchestrion 就会叫停。</p>
</li>
<li>
<p>AST 级解析与“无损”操作：<br />
它读取即将被编译的 .go 源文件，将其解析为 AST（抽象语法树）。</p>
</li>
<li>
<p>手术式注入 (Injection)：<br />
根据预定义的规则（YAML 配置），Orchestrion 开始在 AST 上动刀：</p>
<ul>
<li>添加 Import：自动引入 dd-trace-go 等依赖包。</li>
<li>函数入口插桩：在函数体的第一行插入 span, ctx := tracer.Start(&#8230;)。</li>
<li>函数出口兜底：利用 defer span.End() 确保追踪闭环。<br />
甚至，它还能识别 database/sql 的调用，自动将其替换为带有追踪功能的 Wrapper。</li>
</ul>
</li>
<li>
<p>狸猫换太子：<br />
手术完成后，Orchestrion 将修改后的 AST 重新生成为 .go 文件，保存在一个临时目录中。<br />
最后，它修改传递给编译器的参数，将原始源文件的路径替换为这些临时文件的路径。</p>
</li>
<li>
<p>透明编译：<br />
真正的 Go 编译器（compile）被唤醒，它毫不知情地编译了这些被“加料”的代码。</p>
</li>
</ol>
<p>最终生成的二进制文件，包含了完整的、生产级的可观测性代码，而你的源代码仓库里，依然是那份清清爽爽、没有任何第三方依赖的业务逻辑。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-10.png" alt="" /><br />
<img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-16.png" alt="" /></p>
<h2>Orchestrion：将“魔法”产品化</h2>
<p>Orchestrion 不仅仅是一个概念验证，它是 DataDog 已经在生产环境中使用的成熟工具（现已捐赠给 OpenTelemetry 社区）。它解决了一系列工程难题：</p>
<h3>1. 像 AOP 一样思考</h3>
<p>Orchestrion 引入了类似 <strong>AOP（面向切面编程）</strong> 的概念。通过 YAML 配置文件，你可以定义“切入点”（Join Points）和“建议”（Advice）。</p>
<p>例如，你可以定义一条规则：<br />
*   <strong>切入点</strong>：所有调用 database/sql 包 Query 方法的地方。<br />
*   <strong>建议</strong>：在调用前后包裹一段计时和记录代码。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-11.png" alt="" /><br />
<img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-15.png" alt="" /></p>
<h3>2. 解决 Context 丢失的终极“黑魔法”</h3>
<p>Go 的许多老旧库或设计不规范的代码并没有在参数中传递 context.Context。为了在这些地方也能传递追踪 ID，Orchestrion 做了一件极其硬核的事情：<strong>它修改了 Go 的运行时（Runtime）！</strong></p>
<p>通过修改 runtime.g 结构体，它引入了类似 <strong>GLS (Goroutine Local Storage)</strong> 的机制。这允许在同一个 Goroutine 的不同函数调用栈之间隐式传递上下文，彻底解决了 Context 断链的问题。虽然这听起来很危险，但在受控的编译时注入环境下，它变得可行且强大。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/unleashing-the-go-toolchain-17.png" alt="" /></p>
<h3>3. 零依赖与容器化友好</h3>
<p>Orchestrion 支持通过环境变量注入。这意味着平台工程师可以构建一个包含 Orchestrion 的基础镜像，只需要在 CI/CD 流水线中设置几个环境变量，就可以让所有基于该镜像构建的 Go 应用自动获得可观测性能力，而无需应用开发者修改一行代码。</p>
<h2>未来：社区驱动的标准</h2>
<p>DataDog 已将 Orchestrion 捐赠给 <a href="https://github.com/open-telemetry/opentelemetry-go-compile-instrumentation"><strong>OpenTelemetry</strong></a>，并与阿里巴巴（其有类似的 Go 自动插桩工具）合作，共同在 OpenTelemetry Go SIG 下推进这一技术的标准化。</p>
<p>这意味着，未来 Go 开发者可能只需要执行类似 otel-go-instrument my-app 的命令，就能获得与 Java/Python 同等便捷的监控体验。</p>
<h2>小结：工具链的无限可能</h2>
<p>Kemal 的演讲不仅展示了一个工具，更展示了一种思维方式：<strong>当语言本身的特性限制了你时，不妨向下看一层，去挖掘工具链本身的潜力。</strong></p>
<p>虽然“编译时注入”听起来像是一种对 Go 简洁哲学的“背叛”，但在解决大规模微服务治理、遗留代码维护等现实难题时，它无疑是一剂强有力的解药。</p>
<p>对于那些渴望从重复劳动中解脱出来的 Gopher 来说，这或许就是你们一直在等待的“魔法”。</p>
<h2>参考资料</h2>
<ul>
<li>https://www.youtube.com/watch?v=8Rw-fVEjihw</li>
<li>https://www.datadoghq.com/blog/go-instrumentation-orchestrion/</li>
<li>https://x.com/felixge/status/1865034549832368242</li>
<li>https://github.com/DataDog/orchestrion</li>
<li>https://datadoghq.dev/orchestrion/docs/architecture</li>
<li>https://github.com/open-telemetry/opentelemetry-go-compile-instrumentation</li>
</ul>
<hr />
<p><strong>你的插桩之痛</strong></p>
<p>自动插桩无疑是未来的方向。<strong>在你的项目中，目前是如何处理链路追踪埋点的？是忍受手动埋点的繁琐，还是已经尝试过类似的自动化工具？你对<br />
这种修改 AST 甚至 Runtime 的“黑魔法”持什么态度？</strong></p>
<p><strong>欢迎在评论区分享你的看法或踩坑经历！</strong> 让我们一起探索 Go 可观测性的最佳实践。</p>
<p><strong>如果这篇文章为你打开了 Go 编译工具链的新大门，别忘了点个【赞】和【在看】，并转发给你的架构师朋友，让他也来学两招！</strong></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/19/unleashing-the-go-toolchain/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道</title>
		<link>https://tonybai.com/2025/05/31/six-smells-in-go/</link>
		<comments>https://tonybai.com/2025/05/31/six-smells-in-go/#comments</comments>
		<pubDate>Sat, 31 May 2025 02:36:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[closure]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomod]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[XML]]></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=4769</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/31/six-smells-in-go 大家好，我是Tony Bai。 在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。 今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。 坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？” 在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。 典型场景：先异步通知，后更新状态 想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。 package main import ( "fmt" "sync" "time" ) type Order struct { ID string Status string // "pending", "paid", "notified" } func updateOrderStatusInDB(order *Order, status string) { fmt.Printf("数据库：订单 %s 状态更新为 %s\n", order.ID, status) [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/six-smells-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/31/six-smells-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/05/31/six-smells-in-go</p>
<p>大家好，我是Tony Bai。</p>
<p>在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。</p>
<p>今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<h2>坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？”</h2>
<p>在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。</p>
<p><strong>典型场景：先异步通知，后更新状态</strong></p>
<p>想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。</p>
<pre><code class="go">package main

import (
    "fmt"
    "sync"
    "time"
)

type Order struct {
    ID     string
    Status string // "pending", "paid", "notified"
}

func updateOrderStatusInDB(order *Order, status string) {
    fmt.Printf("数据库：订单 %s 状态更新为 %s\n", order.ID, status)
    order.Status = status // 模拟数据库更新
}

func asyncSendNotification(order *Order) {
    fmt.Printf("营销系统：收到订单 %s 通知，当前状态：%s。准备发送优惠券...\n", order.ID, order.Status)
    // 模拟耗时操作
    time.Sleep(50 * time.Millisecond)
    fmt.Printf("营销系统：订单 %s 优惠券已发送 (基于状态：%s)\n", order.ID, order.Status)
}

func main() {
    order := &amp;Order{ID: "123", Status: "pending"}
    var wg sync.WaitGroup

    fmt.Printf("主流程：订单 %s 支付成功，准备处理...\n", order.ID)

    // 坏味道：先启动异步通知，再更新数据库状态
    wg.Add(1)
    go func(o *Order) { // 注意这里传递了指针
        defer wg.Done()
        asyncSendNotification(o)
    }(order) // goroutine 捕获的是 order 指针

    // 模拟主流程的其他操作，或者数据库更新前的延时
    time.Sleep(500 * time.Millisecond) 

    updateOrderStatusInDB(order, "paid") // 更新数据库状态

    wg.Wait()
    fmt.Printf("主流程：订单 %s 处理完毕，最终状态：%s\n", order.ID, order.Status)
}
</code></pre>
<p>该示例的可能输出：</p>
<pre><code>主流程：订单 123 支付成功，准备处理...
营销系统：收到订单 123 通知，当前状态：pending。准备发送优惠券...
营销系统：订单 123 优惠券已发送 (基于状态：pending)
数据库：订单 123 状态更新为 paid
主流程：订单 123 处理完毕，最终状态：paid
</code></pre>
<p>我们看到营销系统拿到的优惠券居然是基于“pending”状态。</p>
<p><strong>问题分析：</strong></p>
<p>在上面的代码中，asyncSendNotification goroutine 和 updateOrderStatusInDB 是并发执行的。由于 asyncSendNotification 启动在先，并且捕获的是 order 指针，它很可能在 updateOrderStatusInDB 将订单状态更新为 “paid” <strong>之前</strong> 就读取了 order.Status。这就导致营销系统基于一个过时的状态（”pending”）发送了通知或优惠券，引发业务逻辑错误。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>确保关键操作的同步性或顺序性：</strong> 对于有严格先后顺序要求的操作，不要轻易异步化。如果必须异步，确保依赖的操作完成后再执行。</li>
<li><strong>使用同步原语：</strong> 利用 sync.WaitGroup、channel 等确保操作的正确顺序。例如，可以先更新数据库，再启动异步通知。</li>
<li><strong>传递值而非指针（如果适用）：</strong> 如果异步操作仅需快照数据，考虑传递值的副本，而不是指针。但在很多场景下，我们确实需要操作同一个对象。</li>
<li><strong>在异步回调中重新获取最新状态：</strong> 如果异步回调依赖最新状态，应在回调函数内部重新从可靠数据源（如数据库）获取，而不是依赖启动时捕获的状态。</li>
</ol>
<p><strong>修正示例思路：</strong></p>
<pre><code class="go">// ... (Order, updateOrderStatusInDB, asyncSendNotification 定义不变) ...
func main() {
    order := &amp;Order{ID: "123", Status: "pending"}
    var wg sync.WaitGroup

    fmt.Printf("主流程：订单 %s 支付成功，准备处理...\n", order.ID)

    updateOrderStatusInDB(order, "paid") // 先更新数据库状态

    // 再启动异步通知
    wg.Add(1)
    go func(o Order) { // 传递结构体副本，或者在异步函数内部重新获取
        defer wg.Done()
        // 实际场景中，如果 asyncSendNotification 依赖的是更新后的状态，
        // 它应该有能力从某个地方（比如参数，或者内部重新查询）获取到 "paid" 这个状态。
        // 这里简化为直接使用传入时的状态，但强调其应为 "paid"。
        // 或者，更好的方式是 asyncSendNotification 接受一个 status 参数。
        clonedOrderForNotification := o // 假设我们传递的是更新后的状态的副本
        asyncSendNotification(&amp;clonedOrderForNotification)
    }(*order) // 传递 order 的副本，此时 order.Status 已经是 "paid"

    wg.Wait()
    fmt.Printf("主流程：订单 %s 处理完毕，最终状态：%s\n", order.ID, order.Status)
}
</code></pre>
<h2>坏味道二：指针与闭包的“爱恨情仇”——“我以为它没变，结果它却跑了！”</h2>
<p>闭包是 Go 语言中一个强大的特性，它能够捕获其词法作用域内的变量。然而，当闭包捕获的是指针，并且这个指针指向的数据在 goroutine 启动后可能被外部修改，或者指针本身被重新赋值时，就可能导致并发问题和难以预料的行为。虽然 Go 1.22+ 通过实验性的 GOEXPERIMENT=loopvar 改变了 for 循环变量的捕获语义，解决了经典的循环变量闭包陷阱，但指针与闭包结合时对共享可变状态的考量依然重要。</p>
<p><strong>典型场景：闭包捕获指针，外部修改指针或其指向内容</strong></p>
<p>我们来看一个不涉及循环变量，但同样能体现指针与闭包问题的场景：</p>
<pre><code class="go">package main

import (
    "fmt"
    "sync"
    "time"
)

type Config struct {
    Version string
    Timeout time.Duration
}

func watchConfig(cfg *Config, wg *sync.WaitGroup) {
    defer wg.Done()
    // 这个 goroutine 期望在其生命周期内使用 cfg 指向的配置
    // 但如果外部在它执行期间修改了 cfg 指向的内容，或者 cfg 本身被重新赋值，
    // 那么这个 goroutine 看到的内容就可能不是启动时的那个了。
    fmt.Printf("Watcher: 开始监控配置 (Version: %s, Timeout: %v)\n", cfg.Version, cfg.Timeout)
    time.Sleep(100 * time.Millisecond) // 模拟监控工作
    fmt.Printf("Watcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\n", cfg.Version, cfg.Timeout)
}

func main() {
    currentConfig := &amp;Config{Version: "v1.0", Timeout: 5 * time.Second}
    var wg sync.WaitGroup

    fmt.Printf("主流程：初始配置 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)

    // 启动一个 watcher goroutine，它捕获了 currentConfig 指针
    wg.Add(1)
    go watchConfig(currentConfig, &amp;wg) // currentConfig 指针被传递

    // 主流程在 watcher goroutine 执行期间，修改了 currentConfig 指向的内容
    time.Sleep(10 * time.Millisecond) // 确保 watcher goroutine 已经启动并打印了初始配置
    fmt.Println("主流程：检测到配置更新，准备在线修改...")
    currentConfig.Version = "v2.0" // 直接修改了指针指向的内存内容
    currentConfig.Timeout = 10 * time.Second
    fmt.Printf("主流程：配置已修改为 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)

    // 或者更极端的情况，主流程让 currentConfig 指向了一个全新的 Config 对象
    // time.Sleep(10 * time.Millisecond)
    // fmt.Println("主流程：检测到配置需要完全替换...")
    // currentConfig = &amp;Config{Version: "v3.0", Timeout: 15 * time.Second} // currentConfig 指向了新的内存地址
    // fmt.Printf("主流程：配置已替换为 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)
    // 注意：如果 currentConfig 被重新赋值指向新对象，原 watchConfig goroutine 仍然持有旧对象的指针。
    // 但如果原意是让 watchConfig 感知到“最新的配置”，那么这种方式是错误的。

    wg.Wait()
    fmt.Println("主流程：所有处理完毕。")

    fmt.Println("\n--- 更安全的做法：传递副本或不可变快照 ---")
    // 更安全的做法：如果 goroutine 需要的是启动时刻的配置快照
    stableConfig := &amp;Config{Version: "v1.0-stable", Timeout: 5 * time.Second}
    configSnapshot := *stableConfig // 创建一个副本

    wg.Add(1)
    go func(cfgSnapshot Config, wg *sync.WaitGroup) { // 传递的是 Config 值的副本
        defer wg.Done()
        fmt.Printf("SafeWatcher: 开始监控配置 (Version: %s, Timeout: %v)\n", cfgSnapshot.Version, cfgSnapshot.Timeout)
        time.Sleep(100 * time.Millisecond)
        // 即使外部修改了 stableConfig，cfgSnapshot 依然是启动时的值
        fmt.Printf("SafeWatcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\n", cfgSnapshot.Version, cfgSnapshot.Timeout)
    }(configSnapshot, &amp;wg)

    time.Sleep(10 * time.Millisecond)
    stableConfig.Version = "v2.0-stable" // 修改原始配置
    stableConfig.Timeout = 10 * time.Second
    fmt.Printf("主流程：stableConfig 已修改为 (Version: %s, Timeout: %v)\n", stableConfig.Version, stableConfig.Timeout)

    wg.Wait()
    fmt.Println("主流程：所有安全处理完毕。")
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>在第一个示例中，watchConfig goroutine 通过闭包（函数参数也是一种闭包形式）捕获了 currentConfig 指针。这意味着 watchConfig 内部对 cfg 的访问，实际上是访问 main goroutine 中 currentConfig 指针所指向的那块内存。</p>
<ul>
<li><strong>当外部修改指针指向的内容时：</strong> 如代码中 currentConfig.Version = “v2.0&#8243;，watchConfig goroutine 在后续访问 cfg.Version 时，会看到这个被修改后的新值，这可能不是它启动时期望的行为。</li>
<li><strong>当外部修改指针本身时 (注释掉的极端情况)：</strong> 如果 currentConfig = &amp;Config{Version: “v3.0&#8243;, &#8230;}，那么 watchConfig 捕获的 cfg 仍然指向<strong>原始的 Config 对象</strong>（即 “v1.0&#8243; 那个）。如果此时的业务逻辑期望 watchConfig 使用“最新的配置对象”，那么这种捕获指针的方式就会导致错误。</li>
</ul>
<p>这些问题的根源在于对<strong>共享可变状态</strong>的并发访问缺乏控制，以及对指针生命周期和闭包捕获机制的理解不够深入。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li>
<p><strong>明确 goroutine 需要的数据快照还是共享状态：</strong></p>
<ul>
<li>如果 goroutine 只需要启动时刻的数据快照，并且不希望受外部修改影响，那么应该<strong>传递值的副本</strong>给 goroutine（或者在闭包内部创建副本）。如第二个示例中的 configSnapshot。</li>
<li>如果 goroutine 需要与外部共享并感知状态变化，那么必须使用<strong>同步机制</strong>（如 mutex、channel、atomic 操作）来保护对共享状态的访问，确保数据一致性和避免竞态条件。</li>
</ul>
</li>
<li>
<p><strong>谨慎捕获指针，特别是那些可能在 goroutine 执行期间被修改的指针：</strong></p>
<ul>
<li>如果捕获了指针，要清楚地知道这个指针的生命周期，以及它指向的数据是否会被其他 goroutine 修改。</li>
<li>如果指针指向的数据是可变的，并且多个 goroutine 会并发读写，<strong>必须加锁保护</strong>。</li>
</ul>
</li>
<li>
<p><strong>考虑数据的不可变性：</strong> 如果可能，尽量使用不可变的数据结构。将不可变的数据传递给 goroutine 是最安全的并发方式之一。</p>
</li>
<li>
<p><strong>对于经典的 for 循环启动 goroutine 捕获循环变量的问题：</strong></p>
<ul>
<li><strong>Go 1.22+ (启用 GOEXPERIMENT=loopvar) 或未来版本：</strong> 语言层面已经解决了每次迭代共享同一个循环变量的问题，每次迭代会创建新的变量实例。此时，直接在闭包中捕获循环变量是安全的。</li>
<li><strong>Go 1.21 及更早版本 (或未启用 loopvar 实验特性)：</strong> 仍然需要通过<strong>函数参数传递</strong>的方式来确保每个 goroutine 捕获到正确的循环变量值。例如：</li>
</ul>
</li>
</ol>
<pre><code class="go">for i, v := range values {
    valCopy := v // 如果 v 是复杂类型，可能需要更深的拷贝
    indexCopy := i
    go func() {
        // 使用 valCopy 和 indexCopy
    }()
}
// 或者更推荐的方式：
for i, v := range values {
    go func(idx int, valType ValueType) { // ValueType 是 v 的类型
        // 使用 idx 和 valType
    }(i, v)
}
</code></pre>
<p>虽然 Go 语言在 for 循环变量捕获方面做出了改进，但指针与闭包结合时对共享状态和生命周期的审慎思考，仍然是编写健壮并发程序的关键。</p>
<h2>坏味道三：错误处理的哲学——“是Bug就让它崩！”真的好吗？</h2>
<p>Go 语言通过返回 error 值来处理可预期的错误，而 panic 则用于表示真正意外的、程序无法继续正常运行的严重错误，通常由运行时错误（如数组越界、空指针解引用）或显式调用 panic() 引发。当 panic 发生且未被 recover 时，程序会崩溃并打印堆栈信息。</p>
<p>一种常见的观点是：“如果是 Bug，就应该让它尽快崩溃 (Fail Fast)”，以便问题能被及时发现和修复。这种观点在很多情况下是合理的。然而，在某些 <strong>mission-critical（关键任务）系统</strong>中，例如金融交易系统、空中交通管制系统、重要的基础设施服务等，一次意外的宕机重启可能导致不可估量的损失或严重后果。在这些场景下，即使因为一个未捕获的 Bug 导致了 panic，我们也可能期望系统能有一定的“韧性”，而不是轻易“放弃治疗”。</p>
<p><strong>典型场景：一个关键服务在处理请求时因 Bug 发生 Panic</strong></p>
<pre><code class="go">package main

import (
    "fmt"
    "net/http"
    "runtime/debug"
    "time"
)

// 模拟一个关键数据处理器
type CriticalDataProcessor struct {
    // 假设有一些内部状态
    activeConnections int
    lastProcessedID   string
}

// 处理数据的方法，这里故意引入一个可能导致 panic 的 bug
func (p *CriticalDataProcessor) Process(dataID string, payload map[string]interface{}) error {
    fmt.Printf("Processor: 开始处理数据 %s\n", dataID)
    p.activeConnections++
    defer func() { p.activeConnections-- }() // 确保连接数正确管理

    // 模拟一些复杂逻辑
    time.Sleep(50 * time.Millisecond)

    // ！！！潜在的 Bug ！！！
    // 假设 payload 中 "user" 字段应该是一个结构体指针，但有时可能是 nil
    // 或者，某个深层嵌套的访问可能导致空指针解引用
    // 为了演示，我们简单模拟一个 nil map 访问导致的 panic
    var userDetails map[string]string
    // userDetails = payload["user"].(map[string]string) // 这本身也可能 panic 如果类型断言失败
    // 为了稳定复现 panic，我们直接让 userDetails 为 nil
    if dataID == "buggy-data-001" { // 特定条件下触发 bug
        fmt.Printf("Processor: 触发 Bug，尝试访问 nil map '%s'\n", userDetails["name"]) // 这里会 panic
    }

    p.lastProcessedID = dataID
    fmt.Printf("Processor: 数据 %s 处理成功\n", dataID)
    return nil
}

// HTTP Handler - 版本1: 不做任何 recover
func handleRequestVersion1(processor *CriticalDataProcessor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        dataID := r.URL.Query().Get("id")
        if dataID == "" {
            http.Error(w, "缺少 id 参数", http.StatusBadRequest)
            return
        }

        // 模拟从请求中获取 payload
        payload := make(map[string]interface{})
        // if dataID == "buggy-data-001" {
        //  // payload["user"] 可能是 nil 或错误类型，导致 Process 方法 panic
        // }

        err := processor.Process(dataID, payload) // 如果 Process 发生 panic，整个 HTTP server goroutine 会崩溃
        if err != nil {
            http.Error(w, fmt.Sprintf("处理失败: %v", err), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "请求 %s 处理成功\n", dataID)
    }
}

// HTTP Handler - 版本2: 在每个请求处理的 goroutine 顶层 recover
func handleRequestVersion2(processor *CriticalDataProcessor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Fprintf(os.Stderr, "!!!!!!!!!!!!!! PANIC 捕获 !!!!!!!!!!!!!!\n")
                fmt.Fprintf(os.Stderr, "错误: %v\n", err)
                fmt.Fprintf(os.Stderr, "堆栈信息:\n%s\n", debug.Stack())
                fmt.Fprintf(os.Stderr, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")

                // 向客户端返回一个通用的服务器错误
                http.Error(w, "服务器内部错误，请稍后重试", http.StatusInternalServerError)

                // 可以在这里记录更详细的错误到日志系统、发送告警等
                // 例如：log.Errorf("Panic recovered: %v, Stack: %s", err, debug.Stack())
                // metrics.Increment("panic_recovered_total")

                // 重要：根据系统的 mission-critical 程度和业务逻辑，
                // 这里可能还需要做一些清理工作，或者尝试让系统保持在一种“安全降级”的状态。
                // 但要注意，recover 后的状态可能是不确定的，需要非常谨慎。
            }
        }()

        dataID := r.URL.Query().Get("id")
        if dataID == "" {
            http.Error(w, "缺少 id 参数", http.StatusBadRequest)
            return
        }
        payload := make(map[string]interface{})

        err := processor.Process(dataID, payload)
        if err != nil {
            // 正常错误处理
            http.Error(w, fmt.Sprintf("处理失败: %v", err), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "请求 %s 处理成功\n", dataID)
    }
}

func main() {
    processor := &amp;CriticalDataProcessor{}

    // mux1 使用 Version1 handler (不 recover)
    // mux2 使用 Version2 handler (recover)

    // 启动 HTTP 服务器 (这里为了演示，只启动一个，实际中会选择一个)
    // 你可以注释掉一个，运行另一个来观察效果

    // http.HandleFunc("/v1/process", handleRequestVersion1(processor))
    // fmt.Println("V1 Server (不 recover) 启动在 :8080/v1/process")
    // go http.ListenAndServe(":8080", nil)

    http.DefaultServeMux.HandleFunc("/v2/process", handleRequestVersion2(processor))
    fmt.Println("V2 Server (recover) 启动在 :8081/v2/process")
    go http.ListenAndServe(":8081", nil)

    fmt.Println("\n请在浏览器或使用 curl 测试:")
    fmt.Println("  正常请求: curl 'http://localhost:8081/v2/process?id=normal-data-002'")
    fmt.Println("  触发Bug的请求: curl 'http://localhost:8081/v2/process?id=buggy-data-001'")
    fmt.Println("  (如果启动V1服务，触发Bug的请求会导致服务崩溃)")

    select {} // 阻塞 main goroutine，保持服务器运行
}
</code></pre>
<p><strong>问题分析：</strong></p>
<ul>
<li><strong>不 Recover (handleRequestVersion1)：</strong> 当 processor.Process 方法因为 Bug（例如访问 nil map userDetails["name"]）而发生 panic 时，如果这个 panic 没有在当前 goroutine 的调用栈中被 recover，它会一直向上传播。对于由 net/http 包为每个请求创建的 goroutine，如果 panic 未被处理，将导致该 goroutine 崩溃。在某些情况下（取决于 Go 版本和 HTTP server 实现的细节），这可能导致整个 HTTP 服务器进程终止，或者至少是该连接的处理异常中断，影响服务可用性。</li>
<li><strong>Recover (handleRequestVersion2)：</strong> 通过在每个请求处理的 goroutine 顶层使用 defer func() { recover() }()，我们可以捕获这个由 Bug 引发的 panic。捕获后，我们可以：
<ul>
<li>记录详细的错误信息和堆栈跟踪，便于事后分析和修复 Bug。</li>
<li>向当前请求的客户端返回一个通用的错误响应（例如 HTTP 500），而不是让连接直接断开或无响应。</li>
<li><strong>关键在于：</strong> 阻止了单个请求处理中的 Bug 导致的 panic 扩散到导致整个服务不可用的地步。服务本身仍然可以继续处理其他正常的请求。</li>
</ul>
</li>
</ul>
<p><strong>“是Bug就让它崩！”的观点在很多开发和测试环境中是值得提倡的，因为它能让我们更快地发现和定位问题。然而，在线上，特别是对于 mission-critical 系统：</strong></p>
<ul>
<li><strong>可用性是第一要务：</strong> 一次意外的全面宕机，可能比单个请求处理失败带来的损失大得多。</li>
<li><strong>数据一致性风险：</strong> 如果 panic 发生在关键数据操作的中间状态，直接崩溃可能导致数据不一致或损坏。recover 之后虽然也需要谨慎处理状态，但至少给了我们一个尝试回滚或记录问题的机会。</li>
<li><strong>用户体验：</strong> 对用户而言，遇到一个“服务器内部错误”然后重试，通常比整个服务长时间无法访问要好一些。</li>
</ul>
<p><strong>避坑与决策指南：</strong></p>
<ol>
<li><strong>在关键服务的请求处理入口或 goroutine 顶层设置 recover 机制：</strong> 这是构建健壮服务的推荐做法。
<ul>
<li>recover 应该与 defer 配合使用。</li>
<li>在 recover 逻辑中，务必记录详细的错误信息、堆栈跟踪，并考虑集成到告警系统。</li>
</ul>
</li>
<li><strong>recover 之后做什么？——视情况而定，但要极其谨慎：</strong>
<ul>
<li><strong>对于单个请求处理 goroutine：</strong> 通常的做法是记录错误，向当前客户端返回错误响应，然后让该 goroutine 正常结束。避免让这个 panic 影响其他请求。</li>
<li><strong>对于核心的、管理全局状态的 goroutine：</strong> 如果发生 panic，表明系统可能处于一种非常不稳定的状态。recover 后，可能需要执行一些清理操作，尝试将系统恢复到一个已知的安全状态，或者进行优雅关闭并重启。<strong>绝对不应该假装什么都没发生，继续使用可能已损坏的状态。</strong></li>
<li><strong>“苟活”的度：</strong> “苟活”不代表对 Bug 视而不见。recover 的目的是保障服务的整体可用性，同时为我们争取定位和修复 Bug 的时间。捕获到的 panic 必须被视为高优先级事件进行处理。</li>
</ul>
</li>
<li><strong>库代码应极度克制 panic：</strong> 库不应该替应用程序做“是否崩溃”的决策。</li>
<li><strong>测试，测试，再测试：</strong> 通过充分的单元测试、集成测试和压力测试，尽可能在上线前发现和消除潜在的 Bug，减少线上发生 panic 的概率。可以使用 Go 的 race detector 来检测并发代码中的竞态条件。</li>
<li><strong>不要滥用 panic/recover 作为正常的错误处理机制：</strong> panic/recover 主要用于处理不可预料的、灾难性的运行时错误或程序缺陷，而不是替代 error 返回值来处理业务逻辑中的预期错误。</li>
</ol>
<p>“是Bug就让它崩！”在开发阶段有助于快速发现问题，但在生产环境，特别是 mission-critical 系统中，<strong>“有控制地恢复，详细记录，并保障整体服务可用性”</strong> 往往是更明智的选择。这并不意味着容忍 Bug，而是采用一种更成熟、更负责任的方式来应对突发状况，确保系统在面对未知错误时仍能表现出足够的韧性。</p>
<h2>坏味道四：http.Client 的“一次性”误区——“每次都新建，省心又省事？”</h2>
<p>Go 标准库的 net/http 包提供了强大的 HTTP客户端功能。但有些开发者（尤其是初学者）在使用 http.Client 时，会为每一个 HTTP 请求都创建一个新的 http.Client 实例。</p>
<p><strong>典型场景：函数内部频繁创建 http.Client</strong></p>
<pre><code class="go">package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

// 坏味道：每次调用都创建一个新的 http.Client
func fetchDataFromAPI(url string) (string, error) {
    client := &amp;http.Client{ // 每次都新建 Client
        Timeout: 10 * time.Second,
    }
    resp, err := client.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

// 正确的方式：复用 http.Client
var sharedClient = &amp;http.Client{ // 全局或适当范围复用的 Client
    Timeout: 10 * time.Second,
    // 可以配置 Transport 以控制连接池等
    // Transport: &amp;http.Transport{
    //  MaxIdleConns:        100,
    //  MaxIdleConnsPerHost: 10,
    //  IdleConnTimeout:     90 * time.Second,
    // },
}

func fetchDataFromAPIReusable(url string) (string, error) {
    resp, err := sharedClient.Get(url) // 复用 Client
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

func main() {
    // 模拟多次调用
    // 如果使用 fetchDataFromAPI，每次都会创建新的 TCP 连接
    // _,_ = fetchDataFromAPI("https://www.example.com")
    // _,_ = fetchDataFromAPI("https://www.example.com")

    // 使用 fetchDataFromAPIReusable，会复用连接
    data, err := fetchDataFromAPIReusable("https://httpbin.org/get")
    if err != nil {
        fmt.Printf("请求错误: %v\n", err)
        return
    }
    fmt.Printf("获取到数据 (部分): %s...\n", data[:50])

    data, err = fetchDataFromAPIReusable("https://httpbin.org/get")
    if err != nil {
        fmt.Printf("请求错误: %v\n", err)
        return
    }
    fmt.Printf("再次获取到数据 (部分): %s...\n", data[:50])
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>http.Client 的零值或通过 &amp;http.Client{} 创建的实例，其内部的 Transport 字段（通常是 *http.Transport）会维护一个 TCP 连接池，并处理 HTTP keep-alive 等机制以复用连接。<strong>如果为每个请求都创建一个新的 http.Client，那么每次请求都会经历完整的 TCP 连接建立过程（三次握手），并在请求结束后关闭连接。</strong></p>
<p><strong>危害：</strong></p>
<ol>
<li><strong>性能下降：</strong> 频繁的 TCP 连接建立和关闭开销巨大。</li>
<li><strong>资源消耗增加：</strong> 短时间内大量创建连接可能导致客户端耗尽可用端口，或者服务器端累积大量 TIME_WAIT 状态的连接，最终影响整个系统的吞吐量和稳定性。</li>
</ol>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>复用 http.Client 实例：</strong> 这是官方推荐的最佳实践。可以在全局范围创建一个 http.Client 实例（如 http.DefaultClient，或者一个自定义配置的实例），并在所有需要发起 HTTP 请求的地方复用它。</li>
<li><strong>http.Client 是并发安全的：</strong> 你可以放心地在多个 goroutine 中共享和使用同一个 http.Client 实例。</li>
<li><strong>自定义 Transport：</strong> 如果需要更细致地控制连接池大小、超时时间、TLS 配置等，可以创建一个自定义的 http.Transport 并将其赋给 http.Client 的 Transport 字段。</li>
</ol>
<h2>坏味道五：API 设计的“文档缺失”——“这参数啥意思？猜猜看！”</h2>
<p>良好的 API 设计是软件质量的基石，而清晰、准确的文档则是 API 可用性的关键。然而，在实际项目中，我们常常会遇到一些 API，其参数、返回值、错误码、甚至行为语义都缺乏明确的文档说明，导致用户（调用方）在集成时只能靠“猜”或者阅读源码，极易产生误用。</p>
<p><strong>典型场景：一个“凭感觉”调用的服务发现 API</strong></p>
<p>假设我们有一个类似 Nacos Naming 的服务发现客户端，其 GetInstance API 的文档非常简略，或者干脆没有文档，只暴露了函数签名：</p>
<pre><code class="go">package main

import (
    "errors"
    "fmt"
    "math/rand"
    "time"
)

// 假设这是 Nacos Naming 客户端的一个简化接口
type NamingClient interface {
    // GetInstance 获取服务实例。
    // 关键问题：
    // 1. serviceName 需要包含 namespace/group 信息吗？格式是什么？
    // 2. clusters 是可选的吗？如果提供多个，是随机选一个还是有特定策略？
    // 3. healthyOnly 如果为 true，是否会过滤掉不健康的实例？如果不健康实例是唯一选择呢？
    // 4. 返回的 instance 是什么结构？如果找不到实例，是返回 nil, error 还是空对象？
    // 5. error 可能有哪些类型？调用方需要如何区分处理？
    // 6. 这个调用是阻塞的吗？超时机制是怎样的？
    // 7. 是否有本地缓存机制？缓存刷新策略是？
    GetInstance(serviceName string, clusters []string, healthyOnly bool) (instance interface{}, err error)
}

// 一个非常简化的模拟实现 (坏味道的 API 设计，文档缺失)
type MockNamingClient struct{}

func (c *MockNamingClient) GetInstance(serviceName string, clusters []string, healthyOnly bool) (interface{}, error) {
    fmt.Printf("尝试获取服务: %s, 集群: %v, 只获取健康实例: %t\n", serviceName, clusters, healthyOnly)

    // 模拟一些内部逻辑和不确定性
    if serviceName == "" {
        return nil, errors.New("服务名不能为空 (错误码: Naming-1001)") // 文档里有这个错误码说明吗？
    }

    // 假设我们内部有一些实例数据
    instances := map[string][]string{
        "OrderService":   {"10.0.0.1:8080", "10.0.0.2:8080"},
        "PaymentService": {"10.0.1.1:9090"},
    }

    // 模拟集群选择逻辑 (文档缺失，用户只能猜)
    selectedCluster := ""
    if len(clusters) &gt; 0 {
        selectedCluster = clusters[rand.Intn(len(clusters))] // 随机选一个？
        fmt.Printf("选择了集群: %s\n", selectedCluster)
    }

    // 模拟健康检查和实例返回 (文档缺失)
    if healthyOnly &amp;&amp; rand.Float32() &lt; 0.3 { // 30% 概率找不到健康实例
        return nil, fmt.Errorf("在集群 %s 中未找到 %s 的健康实例 (错误码: Naming-2003)", selectedCluster, serviceName)
    }

    if insts, ok := instances[serviceName]; ok &amp;&amp; len(insts) &gt; 0 {
        return insts[rand.Intn(len(insts))], nil // 返回一个实例地址
    }

    return nil, fmt.Errorf("服务 %s 未找到 (错误码: Naming-4004)", serviceName)
}

func main() {
    client := &amp;MockNamingClient{}

    // 用户A的调用 (基于猜测)
    fmt.Println("用户A 调用:")
    instA, errA := client.GetInstance("OrderService", []string{"clusterA", "clusterB"}, true)
    if errA != nil {
        fmt.Printf("用户A 获取实例失败: %v\n", errA)
    } else {
        fmt.Printf("用户A 获取到实例: %v\n", instA)
    }

    fmt.Println("\n用户B 的调用 (换一种猜测):")
    // 用户B 可能不知道 serviceName 需要什么格式，或者 clusters 参数的意义
    instB, errB := client.GetInstance("com.example.PaymentService", nil, false) // serviceName 格式？clusters 为 nil 会怎样？
    if errB != nil {
        fmt.Printf("用户B 获取实例失败: %v\n", errB)
    } else {
        fmt.Printf("用户B 获取到实例: %v\n", instB)
    }
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>当 API 的设计者没有提供清晰、详尽的文档来说明每个参数的含义、取值范围、默认行为、边界条件、错误类型以及API的整体行为和副作用时，API 的使用者就只能依赖猜测、尝试，甚至阅读源码（如果开源的话）来理解如何正确调用。</p>
<p><strong>危害：</strong></p>
<ol>
<li><strong>极易误用：</strong> 用户可能以 API 设计者未预期的方式调用接口，导致程序行为不符合预期，甚至引发错误。</li>
<li><strong>集成成本高：</strong> 理解和调试一个文档不清晰的 API 非常耗时。</li>
<li><strong>脆弱的依赖：</strong> 当 API 的内部实现或未明确定义的行为发生变化时，依赖这些隐性行为的调用方代码很可能会中断。</li>
<li><strong>难以排查问题：</strong> 出现问题时，很难判断是调用方使用不当，还是 API 本身的缺陷。</li>
</ol>
<p><strong>避坑指南 (针对 API 设计者)：</strong></p>
<ol>
<li><strong>编写清晰、准确、详尽的文档是 API 设计不可或缺的一部分！</strong> 这不仅仅是注释，可能还包括独立的 API 参考手册、用户指南和最佳实践。</li>
<li><strong>参数和返回值要有明确的语义：</strong> 名称应自解释，复杂类型应有结构和字段说明。
<ul>
<li>例如，serviceName 是否需要包含命名空间或分组信息？格式是什么？</li>
<li>clusters 参数是可选的吗？如果提供多个，选择策略是什么？是轮询、随机还是有特定优先级？</li>
<li>healthyOnly 的确切行为是什么？如果没有健康的实例，是返回错误还是有其他回退逻辑？</li>
</ul>
</li>
<li><strong>明确约定边界条件和错误情况：</strong>
<ul>
<li>哪些参数是必需的，哪些是可选的？可选参数的默认值是什么？</li>
<li>对于无效输入，API 会如何响应？返回哪些具体的错误码或错误信息？（例如，示例中的 Naming-1001, Naming-2003, Naming-4004 是否有统一的文档说明其含义和建议处理方式？）</li>
<li>API 调用可能产生的副作用是什么？</li>
</ul>
</li>
<li><strong>提供清晰的调用示例：</strong> 针对常见的用例，提供可运行的代码示例。</li>
<li><strong>考虑 API 的易用性和健壮性：</strong>
<ul>
<li>是否需要版本化？</li>
<li>是否需要幂等性保证？</li>
<li>认证和授权机制是否清晰？</li>
<li>超时和重试策略是怎样的？</li>
</ul>
</li>
<li><strong>将 API 的使用者视为首要客户：</strong> 站在使用者的角度思考，他们需要哪些信息才能轻松、正确地使用你的 API。</li>
</ol>
<p><strong>对于 API 的使用者：</strong> 当遇到文档不清晰的 API 时，除了“猜测”，更积极的做法是向 API 提供方寻求澄清，或者在有条件的情况下，参与到 API 文档的改进和完善中。</p>
<p>在之前《<a href="https://tonybai.com/2025/05/23/go-api-design-mcp-sdk/">API设计的“Go境界”：Go团队设计MCP SDK过程中的取舍与思考</a>》一文中，我们了见识了Go团队的API设计艺术，大家可以认知阅读和参考。</p>
<h2>坏味道六：匿名函数类型签名的“笨拙感”——“这函数参数看着眼花缭乱！”</h2>
<p>Go 语言的函数是一等公民，可以作为参数传递，也可以作为返回值。这为编写高阶函数和实现某些设计模式提供了极大的灵活性。然而，当匿名函数的类型签名（特别是嵌套或包含多个复杂函数类型参数时）直接写在函数定义中时，代码的可读性会大大降低，显得冗余和笨拙。</p>
<p><strong>典型场景：复杂的函数签名</strong></p>
<pre><code class="go">package main

import (
    "errors"
    "fmt"
    "strings"
)

// 坏味道：函数签名中直接嵌入复杂的匿名函数类型
func processData(
    data []string,
    filterFunc func(string) bool, // 参数1：一个过滤函数
    transformFunc func(string) (string, error), // 参数2：一个转换函数
    aggregatorFunc func([]string) string, // 参数3：一个聚合函数
) (string, error) {
    var filteredData []string
    for _, d := range data {
        if filterFunc(d) {
            transformed, err := transformFunc(d)
            if err != nil {
                // 注意：这里为了简化，直接返回了第一个遇到的错误
                // 实际应用中可能需要更复杂的错误处理逻辑，比如收集所有错误
                return "", fmt.Errorf("转换 '%s' 失败: %w", d, err)
            }
            filteredData = append(filteredData, transformed)
        }
    }
    if len(filteredData) == 0 {
        return "", errors.New("没有数据需要聚合")
    }
    return aggregatorFunc(filteredData), nil
}

// 使用 type 定义函数类型别名，代码更清晰
type StringFilter func(string) bool
type StringTransformer func(string) (string, error)
type StringAggregator func([]string) string

func processDataWithTypeAlias(
    data []string,
    filter StringFilter,
    transform StringTransformer,
    aggregate StringAggregator,
) (string, error) {
    // 函数体与 processData 相同
    var filteredData []string
    for _, d := range data {
        if filter(d) {
            transformed, err := transform(d)
            if err != nil {
                return "", fmt.Errorf("转换 '%s' 失败: %w", d, err)
            }
            filteredData = append(filteredData, transformed)
        }
    }
    if len(filteredData) == 0 {
        return "", errors.New("没有数据需要聚合")
    }
    return aggregate(filteredData), nil
}

func main() {
    sampleData := []string{"  apple  ", "Banana", "  CHERRY  ", "date"}

    // 使用原始的 processData，函数调用时也可能显得冗长
    result, err := processData(
        sampleData,
        func(s string) bool { return len(strings.TrimSpace(s)) &gt; 0 },
        func(s string) (string, error) {
            trimmed := strings.TrimSpace(s)
            if strings.ToLower(trimmed) == "banana" { // 假设banana是不允许的
                return "", errors.New("包含非法水果banana")
            }
            return strings.ToUpper(trimmed), nil
        },
        func(s []string) string { return strings.Join(s, ", ") },
    )

    if err != nil {
        fmt.Printf("处理错误 (原始方式): %v\n", err)
    } else {
        fmt.Printf("处理结果 (原始方式): %s\n", result)
    }

    // 使用 processDataWithTypeAlias，定义和调用都更清晰
    filter := func(s string) bool { return len(strings.TrimSpace(s)) &gt; 0 }
    transformer := func(s string) (string, error) {
        trimmed := strings.TrimSpace(s)
        if strings.ToLower(trimmed) == "banana" {
            return "", errors.New("包含非法水果banana")
        }
        return strings.ToUpper(trimmed), nil
    }
    aggregator := func(s []string) string { return strings.Join(s, ", ") }

    resultTyped, errTyped := processDataWithTypeAlias(sampleData, filter, transformer, aggregator)
    if errTyped != nil {
        fmt.Printf("处理错误 (类型别名方式): %v\n", errTyped)
    } else {
        fmt.Printf("处理结果 (类型别名方式): %s\n", resultTyped)
    }
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>Go 语言的类型系统是强类型且显式的。函数类型本身也是一种类型。当我们将一个函数类型（特别是具有多个参数和返回值的复杂函数类型）直接作为另一个函数的参数类型或返回值类型时，会导致函数签名变得非常长，难以阅读和理解。这与 Go 追求简洁和可读性的哲学在观感上有所冲突。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>使用 type 关键字定义函数类型别名：</strong> 这是解决此类问题的最推荐、最地道也是最常见的方法。通过为复杂的函数签名定义一个有意义的类型名称，可以极大地提高代码的可读性和可维护性。如示例中的 StringFilter, StringTransformer, StringAggregator。</li>
<li><strong>何时可以不使用类型别名：</strong>
<ul>
<li>当函数签名非常简单（例如 func() 或 func(int) int）且该函数类型只在局部、极少数地方使用时，直接写出可能问题不大。</li>
<li>但一旦函数签名变复杂，或者该函数类型需要在多个地方使用（作为不同函数的参数或返回值，或者作为结构体字段类型），就应该毫不犹豫地使用类型别名。</li>
</ul>
</li>
<li><strong>理解背后的设计考量：</strong> Go 语言强调类型的明确性。虽然直接写出函数类型显得“笨拙”，但也保证了类型信息在代码中的完全显露，避免了某些动态语言中因类型不明确可能导致的困惑。类型别名则是在这种明确性的基础上，提供了提升可读性的手段。</li>
</ol>
<p>为了更好地简化匿名函数，Go团队也提出了关于引入轻量级匿名函数语法的提案（Issue #21498），该提案一直是社区讨论的焦点，它旨在提供一种更简洁的方式来定义匿名函数，尤其是当函数类型可以从上下文推断时，从而减少样板代码，提升代码的可读性和编写效率。</p>
<h2>小结：于细微处见真章，持续打磨代码品质</h2>
<p>今天我们复盘的这六个 Go 编码“坏味道”——异步时序混乱、指针闭包陷阱、不当的错误处理、http.Client 误用、文档缺失的 API 以及冗长的函数签名——可能只是我们日常开发中遇到问题的冰山一角。</p>
<p>它们中的每一个，看似都是细节问题，但“千里之堤，溃于蚁穴”。正是这些细节的累积，最终决定了我们软件产品的质量、系统的稳定性和团队的开发效率。</p>
<p>识别并规避这些“坏味道”，需要我们：</p>
<ul>
<li><strong>深入理解 Go 语言的特性和设计哲学。</strong></li>
<li><strong>培养严谨的工程思维和对细节的关注。</strong></li>
<li><strong>重视代码审查，从他人的错误和经验中学习。</strong></li>
<li><strong>持续学习，不断反思和总结自己的编码实践。</strong></li>
</ul>
<p>希望今天的分享能给大家带来一些启发。让我们一起努力，写出更少“坑”、更高质量的 Go 代码！</p>
<hr />
<p><strong>聊一聊，也帮个忙：</strong></p>
<ul>
<li><strong>在你日常的 Go 开发或 Code Review 中，还遇到过哪些让你印象深刻的“编码坏味道”？</strong></li>
<li><strong>对于今天提到的这些问题，你是否有自己独特的解决技巧或更深刻的理解？</strong></li>
<li><strong>你认为在团队中推广良好的编码规范和实践，最有效的方法是什么？</strong></li>
</ul>
<p>欢迎在<strong>评论区</strong>留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助，也请<strong>转发给你身边的 Gopher 朋友们</strong>，让我们一起在 Go 的道路上精进！</p>
<p><strong>想与我进行更深入的 Go 语言、编码实践与 AI 技术交流吗？</strong> 欢迎加入我的<strong>“Go &amp; AI 精进营”知识星球</strong>。</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p>我们星球见！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/31/six-smells-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性</title>
		<link>https://tonybai.com/2025/05/30/gopher-girlfriend/</link>
		<comments>https://tonybai.com/2025/05/30/gopher-girlfriend/#comments</comments>
		<pubDate>Fri, 30 May 2025 11:33:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomod]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[XML]]></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=4765</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/30/gopher-girlfriend 大家好，我是Tony Bai。 最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。 那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。 “Gopher 女友”的可爱特质大揭秘！ 让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照： 1. “cute” (可爱) Meme 解读： 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。 Go语言真相： 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的语法简洁、核心概念少、没有过多的“语法糖”，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。 2. “low-maintenance” (低维护) Meme 解读： 她不“作”，好相处，不需要你花太多心思去“伺候”。 Go语言真相： 这简直是 Go 语言的真实写照！ gofmt 强制统一代码风格，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。 强大的工具链 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-girlfriend-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/30/gopher-girlfriend">本文永久链接</a> &#8211; https://tonybai.com/2025/05/30/gopher-girlfriend</p>
<p>大家好，我是Tony Bai。</p>
<p>最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-girlfriend-2.jpg" alt="" /></p>
<p>那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。</p>
<h2>“Gopher 女友”的可爱特质大揭秘！</h2>
<p>让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照：</p>
<p><strong>1. “cute” (可爱)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。</li>
<li><strong>Go语言真相：</strong> 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的<strong>语法简洁、核心概念少、没有过多的“语法糖”</strong>，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。</li>
</ul>
<p><strong>2. “low-maintenance” (低维护)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她不“作”，好相处，不需要你花太多心思去“伺候”。</li>
<li><strong>Go语言真相：</strong> 这简直是 Go 语言的真实写照！
<ul>
<li><strong>gofmt 强制统一代码风格</strong>，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。</li>
<li><strong>强大的工具链</strong> (go build, go test, go mod 等) 让构建、测试、依赖管理变得异常简单。</li>
<li><strong>静态编译生成单个可执行文件</strong>，部署过程干净利落，没有复杂的运行时依赖和“DLL地狱”。</li>
<li><strong>内置垃圾回收 (GC)</strong> 机制，虽然不是“银弹”，但也极大地减轻了开发者的内存管理负担。</li>
</ul>
</li>
</ul>
<p>这些特性使得Go项目的<strong>维护成本相对较低</strong>，开发者可以将更多精力聚焦在业务逻辑上。</p>
<p><strong>3. “leaves you love letters in go.mod” (在 go.mod 里给你留情书)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 多么浪漫的表达！她把对你的“心意”（依赖）都清清楚楚地写在了 go.mod 这封“情书”里。</li>
<li><strong>Go语言真相：</strong> 自从 Go Modules 成为官方推荐的依赖管理方案后，go.mod 文件就成了每个 Go 项目的“标准配置”。它清晰、明确地记录了项目的模块路径、Go 版本以及所有直接和间接依赖及其版本号。这种<strong>依赖关系的透明化和可追溯性</strong>，就像一封真挚的“情书”，让你对项目的“家底”一目了然，极大地方便了依赖管理和构建复现。</li>
</ul>
<p><strong>4. “panics but quickly recovers” (会panic但能快速恢复)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她偶尔也会有小情绪（panic），但总能很快调整过来（recover），不至于让关系彻底崩溃。</li>
<li><strong>Go语言真相：</strong> Go 语言通过 panic 来表示严重的、通常是程序缺陷导致的运行时错误。但与其他一些语言遇到类似情况直接崩溃不同，Go 提供了 recover 机制。通过在 defer 函数中调用 recover()，我们可以捕获 panic，记录错误信息，执行一些清理操作，甚至尝试让程序从一个可控的状态恢复或优雅降级，而不是让整个服务“一蹶不振”。这种设计赋予了 Go 程序更强的<strong>韧性</strong>。</li>
</ul>
<p><strong>5. “shares her emotions by communicating” (通过沟通分享她的情感)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她乐于沟通，而不是让你猜她的心思。</li>
<li><strong>Go 语言真相：</strong> 这无疑是在致敬 Go 并发编程的核心原语——<strong>channel</strong>！Go 语言信奉“不要通过共享内存来通信，而要通过通信来共享内存” (Don&#8217;t communicate by sharing memory, share memory by communicating) 的并发哲学。Channel 正是 goroutine 之间进行<strong>数据传递和状态同步</strong>的主要桥梁，它使得并发逻辑的表达更加清晰和安全。</li>
</ul>
<p><strong>6. “thinks mutexes are romantic” (认为互斥锁是浪漫的)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 这个有点“硬核”的浪漫！她认为互斥锁 (mutex) 这种保护共享资源、确保“二人世界”不被打扰的机制，是充满“安全感”的浪漫。</li>
<li><strong>Go语言真相：</strong> sync.Mutex 是 Go 中最常用的并发同步原语之一，用于在并发访问共享资源时避免竞态条件。虽然 Go 推崇通过 channel 进行通信，但在某些场景下，使用互斥锁保护共享数据仍然是必要且高效的。这个梗幽默地反映了 Gopher 对<strong>并发安全</strong>的极致追求和对底层同步机制的熟悉。</li>
</ul>
<p><strong>7. “doesn&#8217;t cry when invalid memory address or nil pointer dereference” (当无效内存地址或空指针解引用时不会哭)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 遇到问题，她不“哭哭啼啼”（难以追踪的错误），而是直接“告诉你”（panic）。</li>
<li><strong>Go 语言真相：</strong> 当 Go 程序遇到空指针解引用、数组越界等严重的运行时错误时，它会立即 panic，并打印出清晰的错误信息和堆栈跟踪。这与某些语言可能产生的段错误 (segmentation fault) 或未定义行为，导致问题难以定位和复现相比，无疑是一种更“直接”和有助于<strong>快速暴露和定位 Bug</strong> 的行为。</li>
</ul>
<p><strong>8. “thinks ORM is astrology for devs” (认为 ORM 对开发者来说是占星术)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她对那些过度封装、隐藏细节、让人感觉像“玄学”的 ORM 框架持保留态度。</li>
<li><strong>Go语言真相：</strong> 这是 Go 社区一个广为人知的“文化梗”。许多 Gopher 更倾向于使用标准库的 database/sql 包配合轻量级的 SQL 构建库（如 sqlx等），或者直接编写原生 SQL。这背后是对<strong>数据层掌控力、性能透明度以及避免不必要的“魔法”和复杂抽象</strong>的追求。他们认为，SQL 本身就是一种强大的 DSL，过度封装反而可能引入新的问题。</li>
</ul>
<p><strong>9. “cooks you meals from scratch” (从零开始为你做饭)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她心灵手巧，能用最新鲜的食材（标准库）为你烹制美味佳肴，而不是依赖各种半成品（重型框架或过多第三方库）。</li>
<li><strong>Go 语言真相：</strong> Go 拥有一个异常<strong>强大且设计精良的标准库</strong>。无论是网络编程 (net/http, net)、JSON/XML 处理 (encoding/json, encoding/xml)、文件操作 (os, io)、加密解密 (crypto/*)，还是并发原语 (sync, sync/atomic)，标准库都提供了高质量的实现。这使得 Go 开发者在很多场景下可以“自给自足”，减少对外部依赖，构建出更轻量、更可控的系统。</li>
</ul>
<p><strong>10. “reviews your code every night” (每晚都审查你的代码)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她非常关心你的代码质量，时刻帮你把关。</li>
<li><strong>Go 语言真相：</strong> 这可以从几个层面理解：
<ul>
<li><strong>静态类型检查：</strong> Go 是一门静态类型语言，编译器在编译阶段就能帮你发现大量的类型错误和低级 Bug，就像一位尽职的“审查员”。</li>
<li><strong>go vet 等工具：</strong> Go 工具链内置了 go vet 等静态分析工具，可以帮助检查代码中潜在的错误或可疑构造。</li>
<li><strong>社区文化：</strong> Go 社区非常重视 Code Review 的实践，鼓励通过同行评审来提升代码质量。</li>
<li><strong>语言设计本身：</strong> Go 语言的简洁性和一些强制性规范（如未使用变量的编译错误），也在某种程度上“迫使”开发者写出更清晰、更规范的代码，更易于审查。</li>
</ul>
</li>
</ul>
<p><strong>11. “compiles fast” (编译快)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她做事麻利，从不拖沓。</li>
<li><strong>Go 语言真相：</strong> 这绝对是 Go 语言最令人称道的特性之一！Go 的<strong>编译速度极快</strong>，即使是中大型项目，编译过程通常也只需要十几秒钟。这极大地提升了开发者的工作效率和迭代速度，减少了漫长的等待时间，让开发体验如丝般顺滑。快速编译使得“编码-编译-测试”的循环非常高效。</li>
</ul>
<h2>小结：“Go语言女友”，为何如此理想？</h2>
<p>看完了对 “gopher gf” Meme 图的逐条解读，我们不难发现，这位“理想女友”的每一个“可爱特质”，都精准地映射了 Go 语言在现实世界中的核心优势：</p>
<ul>
<li><strong>简洁易学 (cute)</strong></li>
<li><strong>维护成本低 (low-maintenance)</strong></li>
<li><strong>依赖管理清晰 (leaves you love letters in go.mod)</strong></li>
<li><strong>具备韧性的错误处理 (panics but quickly recovers)</strong></li>
<li><strong>推崇通信共享内存的并发模型 (shares her emotions by communicating)</strong></li>
<li><strong>重视并发安全 (thinks mutexes are romantic)</strong></li>
<li><strong>明确的运行时错误反馈 (doesn&#8217;t cry when invalid memory address or nil pointer dereference)</strong></li>
<li><strong>崇尚直接、避免过度抽象 (thinks ORM is astrology for devs)</strong></li>
<li><strong>强大的标准库 (cooks you meals from scratch)</strong></li>
<li><strong>利于代码质量保障的特性与文化 (reviews your code every night)</strong></li>
<li><strong>闪电般的编译速度 (compiles fast)</strong></li>
</ul>
<p>正是这些特性，使得 Go 语言在云原生、微服务、分布式系统、网络编程、命令行工具等众多领域大放异彩，成为越来越多开发者和企业的首选。它就像一位可靠、高效、易于相处且不乏生活情趣的“伴侣”，帮助我们更轻松、更愉快地构建出色的软件系统。</p>
<p>当然，Meme 终归是 Meme，它用一种轻松幽默的方式，概括了 Go 语言的诸多美好。现实中的 Go 语言也并非完美无缺，它依然在不断发展和进化。但不可否认的是，这些“可爱”的特质，正是 Go 语言独特魅力和强大生命力的源泉。</p>
<p>那么，你心中的“Go 语言女友”又是怎样的呢？或者，你最欣赏 Go 语言的哪个“可爱”特质？</p>
<p>欢迎在<strong>评论区</strong>分享你的看法和脑洞！如果你觉得这篇文章有趣且让你对 Go 语言有了更深的（或者说更“萌”的）理解，也请<strong>转发给你身边的 Gopher 朋友们</strong>，一起感受这份来自代码世界的“浪漫”与“可爱”！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<blockquote>
<p>注：本文部分内容经过AI润色和优化，以提升读者阅读体验。</p>
</blockquote>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/30/gopher-girlfriend/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>编译Go应用的黑盒挑战：无源码只有.a文件，你能搞定吗？</title>
		<link>https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go/</link>
		<comments>https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go/#comments</comments>
		<pubDate>Wed, 30 Aug 2023 12:58:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[buildmode]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-build]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[GOCACHE]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[goinstall]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[gorun]]></category>
		<category><![CDATA[gotool]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[importcfg]]></category>
		<category><![CDATA[ldflags]]></category>
		<category><![CDATA[library]]></category>
		<category><![CDATA[link]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[source]]></category>
		<category><![CDATA[std]]></category>
		<category><![CDATA[uber]]></category>
		<category><![CDATA[zap]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[编译]]></category>
		<category><![CDATA[链接]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3974</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go 上周末，一个Gopher在微信上与我交流了一个有关Go程序编译的问题。他的述求说起来也不复杂，那就是合作公司提供的API包仅包括golang archive(使用go build -buildmode=archive构建的.a文件)，没有Go包的源码。如何将这个.a链接到项目构建出的最终可执行程序中呢？ 对于C、C++、Java程序员来说，仅提供静态链接库或动态链接库(包括头文件)、jar包而不提供源码的API是十分寻常的。但对于Go来说，仅提供Go包的archive(.a)文件，而不提供Go包源码的情况却是极其不常见的。究其原因，简单来说就是go build或go run不支持！ 注：《Go语言精进之路vo1》一书的第16条“理解Go语言的包导入”对Go的编译过程和原理做了系统说明。 那么真的就没有方法实现没有source、仅基于.a文件的Go应用构建了吗？也不是。的确有一些hack的方法可以实现这点，本文就来从技术角度来探讨一下这些hack方法，但并不推荐使用！ 1. 回顾go build不支持”no source, only .a” 我们首先来回顾一下go build在”no source, only .a”下的表现。为此，我们先建立一个实验环境，其目录和文件布局如下： // 没有外部依赖的api包: foo $tree goarchive-nodeps goarchive-nodeps ├── Makefile ├── foo.a ├── foo.go └── go.mod $tree library library └── github.com └── bigwhite └── foo.a // 依赖foo包的app工程 $tree app-link-foo app-link-foo ├── Makefile ├── go.mod [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/how-to-build-with-only-archive-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go">本文永久链接</a> &#8211; https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go</p>
<p>上周末，一个Gopher在微信上与我交流了一个有关Go程序编译的问题。他的述求说起来也不复杂，那就是合作公司提供的API包仅包括golang archive(使用go build -buildmode=archive构建的.a文件)，没有Go包的源码。如何将这个.a链接到项目构建出的最终可执行程序中呢？</p>
<p>对于C、C++、Java程序员来说，仅提供静态链接库或<a href="https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/">动态链接库</a>(包括头文件)、jar包而不提供源码的API是十分寻常的。但对于Go来说，仅提供Go包的archive(.a)文件，而不提供Go包源码的情况却是极其不常见的。究其原因，简单来说就是<strong>go build或go run不支持</strong>！</p>
<blockquote>
<p>注：<a href="https://item.jd.com/13694000.html">《Go语言精进之路vo1》</a>一书的第16条“理解Go语言的包导入”对Go的编译过程和原理做了系统说明。</p>
</blockquote>
<p>那么真的就没有方法实现没有source、仅基于.a文件的Go应用构建了吗？也不是。的确有一些hack的方法可以实现这点，本文就来从技术角度来探讨一下这些hack方法，但并<strong>不推荐使用</strong>！</p>
<h2>1. 回顾go build不支持”no source, only .a”</h2>
<p>我们首先来回顾一下go build在”no source, only .a”下的表现。为此，我们先建立一个实验环境，其目录和文件布局如下：</p>
<pre><code>// 没有外部依赖的api包: foo

$tree goarchive-nodeps
goarchive-nodeps
├── Makefile
├── foo.a
├── foo.go
└── go.mod

$tree library
library
└── github.com
    └── bigwhite
        └── foo.a

// 依赖foo包的app工程
$tree app-link-foo
app-link-foo
├── Makefile
├── go.mod
└── main.go
</code></pre>
<p>这里我们已经将app-link-foo依赖的foo.a构建了出来(通过go build -buildmode=arhive)，并放入了library对应的目录下。</p>
<blockquote>
<p>注：可通过ar -x foo.a命令可以查看foo.a的组成。</p>
</blockquote>
<p>现在我们使用go build来构建app-link-foo工程：</p>
<pre><code>$cd app-link-foo
$go build
main.go:6:2: no required module provides package github.com/bigwhite/foo; to add it:
    go get github.com/bigwhite/foo
</code></pre>
<p>我们看到：go build会分析app-link-foo的依赖，并要求获取其依赖的foo包的代码，但我们无法满足go build这一要求！</p>
<p>有人可能会说：go build支持向go build支持向compiler和linker传递参数，是不是将foo.a的位置告知compiler和linker就可以了呢？我们来试试：</p>
<pre><code>$go build -x -v -gcflags '-I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library' -ldflags '-L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library' -o main main.go
main.go:6:2: no required module provides package github.com/bigwhite/foo; to add it:
    go get github.com/bigwhite/foo
make: *** [build] Error 1
</code></pre>
<p>我们看到：即便向go build传入gcflags和ldflags参数，告知了foo.a的搜索路径，go build依然报错，仍然提示需要foo包的源码！也就是说go build还没到调用go tool compile和go tool link那一步就开始报错了！</p>
<p>go build不支持在无源码情况下链接.a，那么我们<strong>只能绕过go build</strong>了！</p>
<h2>2. 绕过go bulid</h2>
<p>认真读过<a href="https://item.jd.com/13694000.html">《Go语言精进之路vo1》</a>一书的朋友都会知道：go build实质是调用go tool compile和go tool link两个命令来完成go应用的构建过程的，使用go build -x -v可以查看到go build的详细构建过程。</p>
<p>接下来，我们就来扮演一下”go build”，以手动的方式分别调用go tool compile和go tool link，看看是否能达到无需依赖包源码就能成功构建的目标。</p>
<p>我们以foo.a这个自身没有外部依赖的go archive为例，用手动方式构建一下app-link-foo这个工程。</p>
<p>首先确保通过-buildmode=archive构建出的foo.a被正确放入library/github.com/bigwhite下面。</p>
<p>接下来，我们通过go tool compile编译一下app-link-foo：</p>
<pre><code>$cd app-link-foo
$go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go
</code></pre>
<p>我们看到：手动执行go tool compile在通过-I传入依赖库的.a文件时是可以正常编译出object file(目标文件)的。go tool compile的手册告诉我们-I选项为compile提供了搜索包导入路径的目录：</p>
<pre><code>$go tool compile -h
  ... ...
  -I directory
        add directory to import search path
  ... ...
</code></pre>
<p>接下来我们用go tool link将main.o和foo.a链接在一起形成可执行二进制文件main：</p>
<pre><code>$cd app-link-foo
$go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o
</code></pre>
<p>通过go tool link并在-L传入foo.a的链接路径的情况下，我们成功地将main.o和foo.a链接在了一起，形成了最终的可执行文件main。</p>
<p>go tool link的-L选项为link提供了搜索.a的路径：</p>
<pre><code>$go tool link -h
  ... ...
  -L directory
        add specified directory to library path
  ... ...
</code></pre>
<p>执行一下编译链接后的二进制文件main，我们将看到与预期相同的输出结果：</p>
<pre><code>$./main
invoke foo.Add
11
</code></pre>
<p>有些童鞋在执行go tool compile时可能会遇到找不到fmt.a或fmt.o的错误！这是因为Go 1.20版本及以后，Go安装包默认将不会在\$GOROOT/pkg/\$GOOS_\$GOARCH下面安装标准库的.a文件集合，这样go tool compile在这个路径下面就找不到app-link-foo所依赖的fmt.a：</p>
<pre><code>➜  /Users/tonybai/.bin/go1.20/pkg git:(master) ✗ $ls
darwin_amd64/    include/    tool/
➜  /Users/tonybai/.bin/go1.20/pkg git:(master) ✗ $cd darwin_amd64
➜  /Users/tonybai/.bin/go1.20/pkg/darwin_amd64 git:(master) ✗ $ls
</code></pre>
<p>解决方法也很简单，那就是手动执行下面命令编译和安装一下标准库的.a文件：</p>
<pre><code>$GODEBUG=installgoroot=all  go install std

➜  /Users/tonybai/.bin/go1.20/pkg/darwin_amd64 git:(master) ✗ $ls
archive/    database/    fmt.a        index/        mime/        plugin.a    strconv.a    time/
bufio.a        debug/        go/        internal/    mime.a        reflect/    strings.a    time.a
bytes.a        embed.a        hash/        io/        net/        reflect.a    sync/        unicode/
compress/    encoding/    hash.a        io.a        net.a        regexp/        sync.a        unicode.a
container/    encoding.a    html/        log/        os/        regexp.a    syscall.a    vendor/
context.a    errors.a    html.a        log.a        os.a        runtime/    testing/
crypto/        expvar.a    image/        math/        path/        runtime.a    testing.a
crypto.a    flag.a        image.a        math.a        path.a        sort.a        text/
</code></pre>
<p>这样无论是go tool compile，还是go tool link都会找到对应的标准库包了！</p>
<p>在这个例子中，foo.a仅依赖标准库，没有依赖第三方库，这样相对简单一些。通常合作伙伴提供的.a中的包都是依赖第三方的包的，下面我们就来看看如果.a有第三方依赖，上面的编译链接方法是否还能奏效！</p>
<h2>3. 要链接的.a文件自身也依赖第三方包</h2>
<p>goarchive-with-deps目录下的bar.a就是一个自身也依赖第三方包的go archive文件，它依赖的是uber的<a href="https://tonybai.com/2021/07/14/uber-zap-advanced-usage">zap日志包</a>以及zap包的依赖链，下面是bar的go.mod文件的内容：</p>
<pre><code>// goarchive-with-deps/go.mod

module github.com/bigwhite/bar

go 1.20

require go.uber.org/zap v1.25.0

require go.uber.org/multierr v1.10.0
</code></pre>
<p>我们先来安装app-link-foo的思路来编译链接一下app-link-bar：</p>
<pre><code>$cd app-link-bar
$make
go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go
go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o
/Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/link: cannot open file /Users/tonybai/.bin/go1.20/pkg/darwin_amd64/go.uber.org/zap.o: open /Users/tonybai/.bin/go1.20/pkg/darwin_amd64/go.uber.org/zap.o: no such file or directory
make: *** [all] Error 1
</code></pre>
<p>上面报的错误符合预期，因为zap.a尚没有放入build-with-archive-only/library下面。接下来我们基于uber zap的源码构建出一个zap.a并放入指定目录。bar.a依赖的uber zap的版本为v1.25.0，于是我们git clone一下uber zap，checkout出v1.25.0并执行构建：</p>
<pre><code>$cd go/src/go.uber.org/zap
$go build -o zap.a -buildmode=archive .
$cp zap.a /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library/go.uber.org/
</code></pre>
<p>再来编译一下app-link-bar：</p>
<pre><code>$make
go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go
go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o
/Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/link: fingerprint mismatch: go.uber.org/zap has b259b1e07032c6d9, import from github.com/bigwhite/bar expecting 8118f660c835360a
make: *** [all] Error 1
</code></pre>
<p>我们看到go tool link报错，提示“fingerprint mismatch”。这个错误的意思是bar.a期望的zap包的指纹与我们提供的在Library目录下的zap包的指纹不一致！</p>
<p>我们重新用go build -v -x来看一下bar.a的构建过程：</p>
<pre><code>$go build -x -v  -o bar.a -buildmode=archive
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3367014838
github.com/bigwhite/bar
mkdir -p $WORK/b001/
cat &gt;/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3367014838/b001/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile fmt=/Users/tonybai/Library/Caches/go-build/d3/d307b52dabc7d78a8ff219fb472fbc0b0a600038f43cd4c737914f8ccbd2bd70-d
packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d
EOF
cd /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/goarchive-with-deps
/Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=&gt;" -p github.com/bigwhite/bar -lang=go1.20 -complete -buildid mIMNOXMPJH00mEpw6WVc/mIMNOXMPJH00mEpw6WVc -goversion go1.20 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./bar.go
/Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/60/604b60360d384c49eb9c030a2726f02588f54375748ce1421e334bedfda2af47-d # internal
mv $WORK/b001/_pkg_.a bar.a
rm -r $WORK/b001/
</code></pre>
<p>我们看到在编译bar.a的过程中，go tool compile用的是-importcfg来得到的go.uber.org/zap的位置，而从打印的内容来看，go.uber.org/zap指向的是go module cache中的某个文件：packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d。</p>
<p>那是不是在build app-link-bar时也使用这个同样的go.uber.org/zap就可以成功通过go tool link的过程呢？我们来试一下：</p>
<pre><code>$cd app-link-bar
$make build-with-importcfg
go tool compile -importcfg import.link -o main.o main.go
go tool link -importcfg import.link -o main main.o

$./main
invoke foo.Add
{"level":"info","ts":1693203940.0701509,"caller":"goarchive-with-deps/bar.go:14","msg":"invoke bar.Add\n"}
11
</code></pre>
<p>使用-importcfg的确成功的编译链接了app-link-bar，其执行结果也符合预期！注意：这里我们放弃了之前使用的-I和-L，即便应用-I和-L，在与-importcfg联合使用时，go tool compile和link也会以-importcfg的信息为准！</p>
<p>现在还有一个问题摆在面前，那就是上述命令行中的import.link这个文件的内容是啥，又是如何生成的呢？这里的import.link文件十分“巨大”，有500多行，其内容大致如下：</p>
<pre><code>// app-link-bar/import.link

# import config
packagefile internal/goos=/Users/tonybai/Library/Caches/go-build/fa/facce9766a2b3c19364ee55c509863694b205190c504a3831cde7c208bb09f37-d
packagefile vendor/golang.org/x/crypto/chacha20=/Users/tonybai/Library/Caches/go-build/e0/e042b43b78d3596cc00e544a40a13e8cd6b566eb8f59c2d47aeb0bbcbd52aa56-d
... ...

packagefile github.com/bigwhite/bar=/Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library/github.com/bigwhite/bar.a
packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d
packagefile go.uber.org/zap/zapcore=/Users/tonybai/Library/Caches/go-build/e0/e0d81701b5d15628ce5bf174e5c1b7482c13ac3a3c868e9b054da8b1596eaace-d
packagefile go.uber.org/zap/internal/pool=/Users/tonybai/Library/Caches/go-build/bf/bfa96ebb89429b870e2c50c990c1945384e50d10ba354a3dab2b995a813c56a3-d
packagefile go.uber.org/zap/internal=/Users/tonybai/Library/Caches/go-build/33/33cb66c30939b8be915ddc1e237a04688f52c492d3ae58bfbc6196fff8b6b2b5-d
packagefile go.uber.org/zap/internal/bufferpool=/Users/tonybai/Library/Caches/go-build/68/68e58338a5acd96ee1733de78547720f26f4e13d8333defbc00099ac8560c8e8-d
packagefile go.uber.org/zap/buffer=/Users/tonybai/Library/Caches/go-build/7b/7bf00a1d4a69ddb1712366f45451890f3205b58ba49627ed4254acd9b0938ef8-d
packagefile go.uber.org/multierr=/Users/tonybai/Library/Caches/go-build/e7/e7cc278d56fc8262d9cf9de840a04aa675c75f8ac148e955c1ae9950c58c8034-d
packagefile go.uber.org/zap/internal/exit=/Users/tonybai/Library/Caches/go-build/18/187b2b490c810f37c3700132fba12b805e74bd3c59303972bcf74894a63de604-d
packagefile go.uber.org/zap/internal/color=/Users/tonybai/Library/Caches/go-build/e4/e419c93bea7ff2782b2047cf9e7ad37b07cf4a5a5b7f361bf968730e107a495b-d
</code></pre>
<p>这里包含了编译链接app-link-bar是依赖的标准库包、bar.a以及bar包依赖的所有第三方包的实际包.a文件的位置，显然这里用的大多数都是go module cache中的包缓存。</p>
<p>那么这个import.link如何得到呢？Go在golang.org/x/tools包中有一个<a href="https://raw.githubusercontent.com/golang/tools/master/internal/goroot/importcfg.go">importcfg.go文件</a>，基于该文件中的Importcfg函数可以获取标准库相关所有包的package link信息。我将该文件放在了build-with-archive-only/importcfg下了，大家可以自行取用。</p>
<p>importcfg生成了大部分package link，但仍会有一些bar.a依赖的第三方的包的link没有着落，go tool link在链接时会报错，根据报错信息中提供的包导入路径信息，比如：找不到go.uber.org/zap/internal/exit、go.uber.org/zap/internal/color，我们可以利用下面go list命令找到这些包的在本地go module cache中的link位置：</p>
<pre><code>$go list -export -e -f "{{.ImportPath}} {{.Export}}" go.uber.org/zap/internal/exit go.uber.org/zap/internal/color
go.uber.org/zap/internal/exit /Users/tonybai/Library/Caches/go-build/18/187b2b490c810f37c3700132fba12b805e74bd3c59303972bcf74894a63de604-d
go.uber.org/zap/internal/color /Users/tonybai/Library/Caches/go-build/e4/e419c93bea7ff2782b2047cf9e7ad37b07cf4a5a5b7f361bf968730e107a495b-d
</code></pre>
<p>然后可以手工将这些信息copy到import.link中。import.link文件就是在这样自动化+手工的过程中生成的（当然你完全可以自己编写一个工具，获取app-link-bar所需的所有package的link信息）。</p>
<h2>4. 小结</h2>
<p>到这里，我们通过hack的方法实现了在没有源码只有.a文件情况下的可执行程序的编译。</p>
<p>不过上述仅仅是纯技术上的探索，并非标准答案，也更非理想的答案。经过上述探索后，更巩固了我的观点：<strong>不要仅使用.a来构建go应用</strong>。</p>
<p>但非要这么做，如果你是.a的提供方，考虑fingerprint mismatch的情况，你估计要考虑在提供.a的同时，还要提供import.link、你构建.a时所有用到的go module cache的副本，并提供安装这些副本到目标主机上的脚本。这样你的.a用户才可能使用相同的依赖版本完成对.a文件的链接过程。</p>
<p>本文试验的代码都是在<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20/">Go 1.20版本</a>下编译链接的。如果编译.a的Go版本与编译链接可执行文件的Go版本不同，是否会失败呢？这个问题就当做作业留个大家去探索了！</p>
<p>本文涉及的代码可以从<a href="https://github.com/bigwhite/experiments/blob/master/build-with-archive-only">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/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://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>2023年的Rust与Go[译]</title>
		<link>https://tonybai.com/2023/02/22/rust-vs-go-in-2023/</link>
		<comments>https://tonybai.com/2023/02/22/rust-vs-go-in-2023/#comments</comments>
		<pubDate>Wed, 22 Feb 2023 13:49:14 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[rustfmt]]></category>
		<category><![CDATA[scale]]></category>
		<category><![CDATA[内存安全]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[基准测试]]></category>
		<category><![CDATA[多范式]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[机器]]></category>
		<category><![CDATA[类型安全]]></category>
		<category><![CDATA[系统编程]]></category>
		<category><![CDATA[线程]]></category>
		<category><![CDATA[编程]]></category>
		<category><![CDATA[编译]]></category>
		<category><![CDATA[编译型语言]]></category>
		<category><![CDATA[语法]]></category>
		<category><![CDATA[通用语言]]></category>
		<category><![CDATA[静态语言]]></category>
		<category><![CDATA[面向对象]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3804</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/02/22/rust-vs-go-in-2023 本文译自《Rust vs Go in 2023》。 注：从2022年下半年开始，我们研发团队的产品研发不再局限于云端，车端也是将来的一个重要方向。于是我除了继续对Go语言保持常规的高度关注之外，也逐步开始留意Rust语言的发展。 Rust和Go哪个更好？Go还是Rust？在2023年，你应该为你的下一个项目选择哪种语言，为什么？两者在性能、简单性、安全性、功能、规模和并发性等方面如何比较？它们的共同点是什么，它们有哪些根本性的不同？让我们在这个友好而公平的Rust和Go的比较中找到答案。 Rust和Go都很棒 首先，我必须要说的是，Go和Rust都是绝对优秀的编程语言。它们都是现代的、强大的、被广泛采用的编程语言，并且都提供出色的性能。 你可能读过一些说Go比Rust好的文章，或者相反。但这真的没有意义；每一种编程语言都代表了一系列的权衡和取舍。每种语言都有自己的优化重点，所以你对语言的选择应该由适合你的东西和你想用它解决的问题决定。 在这篇文章中，我将尝试告诉你何时使用Go是理想选择以及何时使用Rust更佳。我也会试着介绍一下这两种语言的本质（如果你愿意的话，就是Go和Rust的道）。 虽然它们在语法和风格上有很大不同，但Rust和Go都是构建软件的一流工具。接下来，让我们仔细看看这两种语言。 Go和Rust的相似之处 Rust和Go有很多共同点，这也是你经常听到它们一起被提及的原因之一。两种语言的共同目标是什么呢？ Rust是一种低级静态类型的多范式编程语言，专注于安全和性能。 &#8211; Gints Dreimanis Go是一种开源的编程语言，可以轻松构建简单、可靠、高效的软件。 &#8211; go.dev 内存安全 Go和Rust都属于现代编程语言，它们的首要任务是内存安全。经过几十年对C和C++等旧语言的使用，我们可以清楚地看到，导致错误和安全漏洞的最大原因之一是不安全地或不正确地访问内存。 Rust和Go以不同的方式处理这个问题，但它们的目标都是在管理内存方面比其他语言更聪明、更安全，并帮助你写出正确和高性能的程序。 快速、紧凑的可执行文件 Go和Rust都是编译型语言，这意味着你的程序被直接翻译成可执行的机器码，因此你可以以单一二进制文件形式来部署你的程序；与Python和Ruby等解释型语言不同，你不需要将解释器和大量的库和依赖关系与你的程序一起分发，这是一个很大的优点。这也使得Rust和Go的程序与解释型语言相比都非常快。 通用语言 Rust和Go都是强大的、可扩展的通用编程语言，你可以用它们来开发各种现代软件，从网络应用到分布式微服务，或者从嵌入式微控制器到移动应用程序。 两者都有优秀的标准库、繁荣的第三方生态系统以及巨大的商业支持和庞大的用户基础。它们都已经存在了很多年，并将在未来几年内继续被广泛使用。今天学习Go或Rust将是对你时间和精力的合理投资。 务实的编程风格 Go和Rust都不是以函数式编程为主的语言（例如像Scala或Elixir），也不是完全面向对象的语言（像Java和C#）。相反，虽然Go和Rust都有与函数式和面向对象编程相关的特性，但它们是务实的语言，旨在以最合适的方式解决问题，而不是强迫你采用特定的做事方式。 如果你喜欢函数式编程风格，你会在Rust中发现更多对这种风格的支持，因为Rust在语法特性数量上要比Go更多。 我们可以讨论什么是“面向对象”语言，但可以说C++、Java或C#用户所期望的面向对象编程风格在Go或Rust中都不存在。 &#8211; Jack Mott 规模化的开发 Rust和Go都有一些有用的特性，使它们适合于大规模的编程，不管是指大型团队，还是大型代码库，或者两者兼具。 例如，C语言的程序员们多年来一直在争论将括号放在哪里，以及代码应该用制表符还是空格缩进，而Rust和Go通过使用标准的格式化工具（Go为gofmt，Rust为rustfmt）使用规范的风格自动重写你的代码，完全消除了这些问题。 这并不是说这种特殊的风格本身有多好：而是Rust和Go的程序员都喜欢这种标准化。 gofmt的风格是没有人喜欢的，但gofmt却是所有人的最爱。 &#8211; Rob Pike 两种语言的另一个高分领域是构建管道(pipeline)。两种语言都有优秀的、内置的、高性能的标准构建和依赖管理工具；不再需要与复杂的第三方构建系统搏斗，也不再需要每隔几年就学习一个新的系统。 对于早期职业生涯以Java和Ruby为背景的我而言，构建Go和Rust代码感觉就像从我的肩上卸下了一个不可能的重担。当我在谷歌工作时，遇到用Go编写的服务是一种解脱，因为我知道它很容易构建和运行。Rust也是如此，尽管我只在较小规模的Rust项目上工作过。我希望可无限配置的构建系统的时代已经过去了，所有语言都会有自己专门的构建工具，开箱即可使用。- 山姆-罗斯 Rust还是Go？ 综上可知，这两种语言都设计得很好、很强大，那么你可能会想知道那些关于两门语言的“圣战”究竟是怎么回事（我也是）。为什么人们对“Go vs.Rust”如此大惊小怪，在社交媒体上大打出手，并且写长篇博文说只有傻瓜才会使用Rust，或者Go不是真正的编程语言，或者其他什么。 这可能会让他们感觉好些，但这并不能完全帮助你，因为你正试图决定在你的项目中使用哪种语言，或者你应该学习哪种语言来推动你的编程生涯。一个明智的人不会根据谁喊得声最大来做出重要的选择。 现在让我们继续我们成熟的讨论，看看在某些领域，一个有理智的人可能更喜欢哪一种语言。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/rust-vs-go-in-2023-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/02/22/rust-vs-go-in-2023">本文永久链接</a> &#8211; https://tonybai.com/2023/02/22/rust-vs-go-in-2023</p>
<p>本文译自<a href="https://bitfieldconsulting.com/golang/rust-vs-go">《Rust vs Go in 2023》</a>。</p>
<blockquote>
<p>注：从2022年下半年开始，我们研发团队的产品研发不再局限于云端，车端也是将来的一个重要方向。于是我除了继续对Go语言保持常规的高度关注之外，也逐步开始留意Rust语言的发展。</p>
</blockquote>
<hr />
<p>Rust和Go哪个更好？Go还是Rust？在2023年，你应该为你的下一个项目选择哪种语言，为什么？两者在性能、简单性、安全性、功能、规模和并发性等方面如何比较？它们的共同点是什么，它们有哪些根本性的不同？让我们在这个友好而公平的Rust和Go的比较中找到答案。</p>
<h2>Rust和Go都很棒</h2>
<p>首先，我必须要说的是，<strong>Go和Rust都是绝对优秀的编程语言</strong>。它们都是现代的、强大的、被广泛采用的编程语言，并且都提供出色的性能。</p>
<p>你可能读过一些说Go比Rust好的文章，或者相反。但这真的没有意义；每一种编程语言都代表了一系列的权衡和取舍。每种语言都有自己的优化重点，所以你对语言的选择应该由适合你的东西和你想用它解决的问题决定。</p>
<p>在这篇文章中，我将尝试告诉你何时使用Go是理想选择以及何时使用Rust更佳。我也会试着介绍一下这两种语言的本质（如果你愿意的话，就是<a href="https://tonybai.com/2022/09/25/the-tao-of-go">Go和Rust的道</a>）。</p>
<p>虽然它们在语法和风格上有很大不同，但Rust和Go都是构建软件的一流工具。接下来，让我们仔细看看这两种语言。</p>
<h2>Go和Rust的相似之处</h2>
<p>Rust和Go有很多共同点，这也是你经常听到它们一起被提及的原因之一。两种语言的共同目标是什么呢？</p>
<blockquote>
<p>Rust是一种低级静态类型的多范式编程语言，专注于安全和性能。 &#8211; <a href="https://serokell.io/blog/rust-guide">Gints Dreimanis</a></p>
<p>Go是一种开源的编程语言，可以轻松构建简单、可靠、高效的软件。 &#8211; <a href="https://go.dev">go.dev</a></p>
</blockquote>
<h3>内存安全</h3>
<p>Go和Rust都属于现代编程语言，它们的首要任务是内存安全。经过几十年对C和C++等旧语言的使用，我们可以清楚地看到，导致错误和安全漏洞的最大原因之一是不安全地或不正确地访问内存。</p>
<p>Rust和Go以不同的方式处理这个问题，但它们的目标都是在管理内存方面比其他语言更聪明、更安全，并帮助你写出<a href="https://bitfieldconsulting.com/golang/crisp-code">正确</a>和<a href="https://bitfieldconsulting.com/golang/slower">高性能</a>的程序。</p>
<h3>快速、紧凑的可执行文件</h3>
<p>Go和Rust都是编译型语言，这意味着你的程序被直接翻译成可执行的机器码，因此你可以以单一二进制文件形式来部署你的程序；与<a href="https://bitfieldconsulting.com/golang/go-vs-python">Python</a>和Ruby等解释型语言不同，你不需要将解释器和大量的库和依赖关系与你的程序一起分发，这是一个很大的优点。这也使得Rust和Go的程序与解释型语言相比都非常快。</p>
<h3>通用语言</h3>
<p>Rust和Go都是强大的、可扩展的通用编程语言，你可以用它们来开发各种现代软件，从网络应用到分布式微服务，或者从嵌入式微控制器到移动应用程序。</p>
<p>两者都有优秀的标准库、繁荣的第三方生态系统以及巨大的商业支持和庞大的用户基础。它们都已经存在了很多年，并将在未来几年内继续被广泛使用。今天学习Go或Rust将是对你时间和精力的合理投资。</p>
<h3>务实的编程风格</h3>
<p>Go和Rust都不是<a href="https://bitfieldconsulting.com/golang/functional">以函数式编程为主的语言</a>（例如像Scala或Elixir），也不是完全面向对象的语言（像Java和C#）。相反，虽然Go和Rust都有与函数式和面向对象编程相关的特性，但它们是务实的语言，旨在以最合适的方式解决问题，而不是强迫你采用特定的做事方式。</p>
<p>如果你喜欢函数式编程风格，你会在Rust中发现更多对这种风格的支持，因为Rust在语法特性数量上要比Go更多。</p>
<blockquote>
<p>我们可以讨论什么是“面向对象”语言，但可以说C++、Java或C#用户所期望的面向对象编程风格在Go或Rust中都不存在。 &#8211; Jack Mott</p>
</blockquote>
<h3>规模化的开发</h3>
<p>Rust和Go都有一些有用的特性，使它们适合于大规模的编程，不管是指大型团队，还是大型代码库，或者两者兼具。</p>
<p>例如，C语言的程序员们多年来一直在争论将括号放在哪里，以及代码应该用制表符还是空格缩进，而Rust和Go通过使用标准的格式化工具（Go为gofmt，Rust为rustfmt）使用规范的风格自动重写你的代码，完全消除了这些问题。</p>
<p>这并不是说这种特殊的风格本身有多好：而是Rust和Go的程序员都喜欢这种<strong>标准化</strong>。</p>
<blockquote>
<p>gofmt的风格是没有人喜欢的，但gofmt却是所有人的最爱。 &#8211; <a href="https://www.youtube.com/watch?v=PAAkCSZUG1c&amp;t=8m43s">Rob Pike</a></p>
</blockquote>
<p>两种语言的另一个高分领域是<strong>构建管道(pipeline)</strong>。两种语言都有优秀的、内置的、高性能的标准构建和依赖管理工具；不再需要与复杂的第三方构建系统搏斗，也不再需要每隔几年就学习一个新的系统。</p>
<blockquote>
<p>对于早期职业生涯以Java和Ruby为背景的我而言，构建Go和Rust代码感觉就像从我的肩上卸下了一个不可能的重担。当我在谷歌工作时，遇到用Go编写的服务是一种解脱，因为我知道它很容易构建和运行。Rust也是如此，尽管我只在较小规模的Rust项目上工作过。我希望可无限配置的构建系统的时代已经过去了，所有语言都会有自己专门的构建工具，开箱即可使用。- <a href="https://samwho.dev/">山姆-罗斯</a></p>
</blockquote>
<h2>Rust还是Go？</h2>
<p>综上可知，这两种语言都设计得很好、很强大，那么你可能会想知道那些关于两门语言的“圣战”究竟是怎么回事（我也是）。为什么人们对“Go vs.Rust”如此大惊小怪，在社交媒体上大打出手，并且写长篇博文说只有傻瓜才会使用Rust，或者Go不是真正的编程语言，或者其他什么。</p>
<p>这可能会让他们感觉好些，但这并不能完全帮助你，因为你正试图决定在你的项目中使用哪种语言，或者你应该学习哪种语言来推动你的编程生涯。一个明智的人不会根据谁喊得声最大来做出重要的选择。</p>
<p>现在让我们继续我们成熟的讨论，看看在某些领域，一个有理智的人可能更喜欢哪一种语言。</p>
<h2>Go与Rust的性能对比</h2>
<p>我们已经说过，Go和Rust都能生产出高性能的程序，因为它们被编译成了本地机器代码，而不必通过解释器或虚拟机。</p>
<p>然而，Rust的性能尤其突出。它可以与C和C++相媲美，这两种语言通常被认为是性能最高的编译语言，但与这些老语言不同的是，Rust还提供了内存安全和并发安全，并且基本上不会给执行速度上带去没有任何开销。Rust还允许你创建复杂的抽象，而不需要在运行时付出任何性能上的代价。</p>
<p>相比之下，尽管Go程序的性能也非常好，但Go主要是为开发速度（包括编译）而设计的，而不是执行速度。Go程序员<a href="https://bitfieldconsulting.com/golang/slower">更倾向于清晰的代码而不是快速的代码</a>。</p>
<p>Go编译器也不会花很多时间去尝试生成最有效的机器代码；它更关心的是快速编译大量代码。所以Rust通常会在运行时基准测试中击败Go。</p>
<p>Rust的运行时性能也是一致和可预测的，因为它不使用垃圾收集。Go的垃圾收集器非常高效，并且经过优化，使其“STW(停止世界)”的停顿时间尽可能短（每一个新的Go版本都会越来越短）。但是垃圾收集不可避免地在程序的行为方式中引入了一些不可预测的因素，这在某些应用中可能是一个严重的问题，例如嵌入式系统。</p>
<p>因为Rust旨在让程序员完全控制底层硬件，所以有可能将Rust程序优化到相当接近机器的最大理论性能。这使得Rust在执行速度胜过所有其他考虑因素的领域是一个很好的选择，比如游戏编程、操作系统内核、网络浏览器组件和实时控制系统。</p>
<h2>简单性</h2>
<p>如果没有人能够弄清楚如何使用一种编程语言，那么这种语言有多快也无所谓。Go语言是为了应对C++等语言不断增长的复杂性而特意设计的；它的语法非常少，关键字也非常少，事实上，功能特性也很少。</p>
<p>这意味着<a href="http://gk.link/a/10AVZ">学习Go语言</a>不需要很长时间，就可以用它来编写有用的程序。</p>
<blockquote>
<p>Go是非常容易学习的。我知道这是一个经常被吹捧的好处，但我真的很惊讶于我能够如此迅速地提高工作效率。多亏了这个语言、文档和工具，我在两天后就写出了有趣的、可提交的代码。 &#8211; <a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">一个Rust程序员对Go的早期印象</a></p>
</blockquote>
<p>这里的关键词是<strong>简单性</strong>。当然，简单并不等同于容易，但是小而简单的语言比大而复杂的语言更容易学习。Go语言没有提供那么多不同的方法来做一件事情，所以所有写得好的Go代码往往看起来都一样。快速学习一个不熟悉的服务并理解它在做什么很容易。</p>
<pre><code>fmt.Println("Gopher's Diner Breakfast Menu")
for dish, price := range menu {
    fmt.Println(dish, price)
}
</code></pre>
<p>在我的<a href="https://bitfieldconsulting.com/code-club">代码俱乐部视频系列</a>中，我正是这样做的：从GitHub上半随机地挑选Go项目，并与一群Go初学者一起探索它们，看看我们能理解多少的代码。结果总是比我们预期的要多。</p>
<p>虽然核心语言很小，但Go的标准库却非常强大。这意味着你的学习曲线也需要包括你需要的标准库的部分，而不仅仅是Go语法。</p>
<p>另一方面，将功能从语言中转移到标准库中，意味着你可以只专注于学习与你现在相关的库。</p>
<p>Go也是为大规模的软件开发而设计的，支持有大型代码库的大型团队。在这种情况下，新的开发人员能够尽快上手是非常重要的。出于这个原因，Go社区十分看重：<a href="https://bitfieldconsulting.com/golang/commandments">简单、明显、常规、直接的程序</a>。</p>
<blockquote>
<p>使用Go，你可以快速完成工作。Go是我所使用过的生产力最高的语言之一。它的口号是：今天解决实际问题。 &#8211; <a href="https://endler.dev/2017/go-vs-rust/">马蒂亚斯-恩德勒</a></p>
</blockquote>
<h2>特性</h2>
<blockquote>
<p>Rust比其他几种编程语言支持更多的复杂语法特性，因此，你可以用它实现更多。 &#8211; <a href="https://devathon.com/blog/rust-vs-go-which-programming-language-to-choose/">devathon</a></p>
</blockquote>
<p>Rust是专门设计用来帮助程序员用最少的代码做最多的事情，它包括很多强大而有用的功能特性。例如，Rust的match功能可以让你以十分简洁地方式写出灵活的、富有表现力的逻辑：</p>
<pre><code>fn is_prime(n: u64) -&gt; bool {
    match n {
        0...1 =&gt; false,
        _ =&gt; !(2..n).any(|d| n % d == 0),
    }
}
</code></pre>
<p>因为Rust做了很多事情，这意味着有很多东西需要学习，特别是在开始的时候。但这没关系：在C++或Java中也有很多东西要学，而且你不会得到Rust的高级特性，比如内存安全。</p>
<p>批评Rust是一种复杂的语言忽略了一点：它被设计成具有表现力，这意味着有很多功能，而在许多情况下，这正是你想要的编程语言。</p>
<p>当然，Rust有一个学习曲线，但一旦你开始使用它，你就会好起来。</p>
<blockquote>
<p>对于那些准备接受更复杂的语法和语义（以及可能更高的可读性成本）以换取最大可能的性能的程序员来说，Rust将与C++和D语言争夺思想份额。 &#8211; <a href="https://dave.cheney.net/2015/07/02/why-go-and-rust-are-not-competitors">戴夫-切尼</a></p>
</blockquote>
<p>虽然Rust采用了Go的一些特性，而Go也在采用Rust的一些特性（尤其是<a href="https://bitfieldconsulting.com/golang/generics">泛型</a>），但可以说Rust的特性很重，而Go的特性相对较轻。</p>
<h2>并发</h2>
<p>大多数语言都对并发编程（同时做多件事情）有某种形式的支持，但Go从一开始就是为这项工作而设计的。Go不使用操作系统的线程，而是提供了一个轻量级的替代方案：<strong>goroutine</strong>。</p>
<p>每个goroutine是一个独立执行的Go函数，Go调度器会将其映射到其控制下的一个操作系统线程中。这意味着调度器可以非常有效地管理大量并发的goroutine，只使用有限的操作系统线程。</p>
<p>因此，你可以在一个程序中运行数百万个并发的goroutine，而不会产生严重的性能问题。这使得Go成为高规模并发应用程序的完美选择，如网络服务器和微服务。</p>
<p>Go还具有快速、安全、高效的功能特性，可以使用channel让goroutines进行通信和共享数据。Go的并发支持感觉设计得很好，使用起来也很愉快。</p>
<p>一般来说，对并发程序进行推断是很难的，而且在任何语言中建立可靠、正确的并发程序都是一个挑战。但由于它从一开始就内置于语言中，而不是事后才想到的，Go中的并发编程是最简单、最完整的。</p>
<blockquote>
<p>Go语言可以很容易地建立一个很好的多因素的应用程序，充分利用并发性，同时作为一组微服务进行部署。Rust也可以做这些事情，但可以说它更难。 在某些方面，Rust对防止与内存有关的安全漏洞的痴迷意味着程序员必须不遗余力地执行那些在其他语言（包括Go）中会更简单的任务。 &#8211; <a href="https://sdtimes.com/softwaredev/the-developers-dilemma-choosing-between-go-and-rust/">Sonya Koptyev</a></p>
</blockquote>
<p>相比之下，Rust中的并发故事是非常新的，而且还在稳定中，但它正处于非常积极的开发中，所以请关注这个领域。例如，Rust的<a href="https://github.com/rayon-rs/rayon">rayon库</a>提供了一种非常优雅和轻量级的方式来将顺序计算转化为并行计算。</p>
<blockquote>
<p>拥有goroutines和使用channel的轻量级语法真的很好。这真的显示了语法的力量，这些小细节使并发编程比其他语言感觉好得多 &#8211; <a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">一个Rust程序员对Go的早期印象</a></p>
</blockquote>
<p>虽然在Rust中实现并发程序可能不那么简单，但还是有可能的，而且这些程序可以利用Rust的安全保证。</p>
<p>一个很好的例子是标准库的Mutex类：在Go中，你可以忘记在访问某些东西之前获得一个Mutex锁，但Rust不会让你这样做。</p>
<blockquote>
<p>Go专注于将并发性作为一个一等公民的概念。这并不是说你不能在Rust中找到Go的面向actor的并发性，但这是留给程序员的一个练习。 &#8211; <a href="https://dave.cheney.net/2015/07/02/why-go-and-rust-are-not-competitors">Dave Cheney</a></p>
</blockquote>
<h2>安全</h2>
<p>我们在前面看到，Go和Rust都以不同的方式来防止一大类与内存管理有关的常见编程错误。但是Rust尤其努力确保你不会做一些你不想做的不安全的事情。</p>
<blockquote>
<p>Rust的编译器非常严格和学究派，它检查你使用的每个变量和你引用的每个内存地址。它避免了可能的数据竞争条件，并告知你未定义的行为。并发和内存安全问题在Rust的安全子集中根本不可能发生。 &#8211; <a href="https://bitbucket.org/blog/why-rust">为什么是Rust？</a></p>
</blockquote>
<p>这将使Rust编程成为与几乎所有其他语言不同的体验，而且一开始可能是一种挑战。但对很多人来说，这种辛苦是值得的。</p>
<blockquote>
<p>对我来说，Rust的关键优势是一种感觉，即编译器是我的后盾，不会让它可能检测到的任何错误通过（说真的，有时感觉就像魔法一样）。 &#8211; Grzegorz Nosek</p>
</blockquote>
<p>包括Go在内的许多语言都有帮助程序员避免错误的设施，但Rust将这一点提高到了一个新的水平，因此可能不正确的程序甚至不会被编译。</p>
<blockquote>
<p>有了Rust，库程序员有很多工具来防止他/她的用户犯错。Rust让我们有能力说，我们拥有一块特定的数据；其他东西不可能声称拥有，所以我们知道没有其他东西能够修改它。我想不出以前有什么时候我被赋予过这么多工具来防止意外的误用。这是一种奇妙的感觉。 &#8211; <a href="https://samwho.dev/">山姆-罗斯</a></p>
</blockquote>
<p>“与借用检查器(borrow checker)斗争”是Rust程序员新手的常见综合症，但在大多数情况下，它所发现的问题是你的代码中真正的bug（或至少是潜在的bug）。它可能会迫使你从根本上重构你的程序，以避免遇到这些问题；而当正确性和可靠性是你的首要任务时，这是件好事。</p>
<p>一个不改变你编程方式的语言有什么意义呢？当你用其他语言工作时，Rust所教授的关于安全的课程也是有用的。</p>
<blockquote>
<p>如果你选择了Rust，通常你需要该语言提供的保证：针对空指针和数据竞争的安全，可预测的运行时行为，以及对硬件的完全控制。如果你不需要这些功能，Rust可能是你下一个项目的糟糕选择。这是因为这些保证是有代价的：入门时间。你需要戒掉坏习惯，学习新概念。有可能的是，当你开始的时候，你会经常和借用检查器斗争。 &#8211; <a href="https://endler.dev/2017/go-vs-rust/">Matthias Endler</a></p>
</blockquote>
<p>你觉得Rust的编程模型有多大的挑战性，可能取决于你以前有哪些其他语言的经验。Python或Ruby程序员可能会发现它的限制性；其他人会很高兴。</p>
<blockquote>
<p>如果你是一个花了几周的时间来追寻内存安全漏洞的C/C++程序员，你会非常欣赏Rust。”与借用检查器斗争”变成了”编译器可以检测到这个？酷！” -Grzegorz Nosek</p>
</blockquote>
<h2>规模化</h2>
<blockquote>
<p>今天的服务器程序由数千万行代码组成，由数百甚至数千名程序员进行构建，而且每天都在更新。Go的设计和开发是为了使在这种环境中工作更有成效。Go的设计考虑包括严格的依赖性管理，随着系统的发展，软件架构的适应性，以及组件之间的健壮性。 &#8211; <a href="https://talks.golang.org/2012/splash.article">Rob Pike</a></p>
</blockquote>
<p>当你一个人或在小团队中处理问题时，选择简单的语言还是功能丰富的语言是一个偏好的问题。但是当软件越来越大，越来越复杂，团队越来越大时，差异就开始显现出来了。</p>
<p>对于大型应用程序和分布式系统来说，执行速度不如开发速度重要：像Go这样刻意简化的语言可以减少新开发人员的启动时间，并使他们更容易处理大型代码库的工作。</p>
<blockquote>
<p>有了Go，作为初级开发者更容易提高工作效率，而作为中级开发者则更难引入会导致后续问题的脆弱抽象。由于这些原因，Rust在企业软件开发方面不如Go有说服力。 &#8211; <a href="https://kristoff.it/blog/why-go-and-not-rust">Loris Cro</a></p>
</blockquote>
<p>当涉及到大型的软件开发时，清晰的比聪明的好。Go的局限性实际上使它比Rust等更复杂和强大的语言更适合企业和大机构。</p>
<h2>Rust和Go的不同点</h2>
<p>虽然Rust和Go都是流行的、现代的、广泛使用的语言，但它们并不是真正的竞争对手，因为它们故意针对的是完全不同的使用情况。</p>
<p><a href="https://tonybai.com/2022/09/25/the-tao-of-go">Go的整个编程方法</a>与Rust的完全不同，每一种语言都适合一些人，同时也会刺激另一些人。这完全没问题，如果Rust和Go都能以或多或少相同的方式做同样的事情，我们就不会真的需要两种不同的语言。</p>
<p>那么，我们是否可以通过发现Rust和Go所采取的截然不同的方法来了解它们各自的本性呢？让我们拭目以待。</p>
<h3>垃圾回收</h3>
<p>“要不要垃圾回收”是一个没有正确答案的问题。垃圾回收，以及一般的自动内存管理，使得开发可靠、高效的程序变得快速和容易，对于一些人来说，这至关重要。</p>
<p>但也有人说，垃圾回收及其性能开销和停顿，使程序在运行时表现得不可预测，并引入了不可接受的延迟。争论还在继续。</p>
<blockquote>
<p>Go是一种与Rust非常不同的语言。虽然两者都可以被模糊地描述为系统语言或C语言的替代品，但它们有不同的目标和应用、语言设计的风格以及优先级。垃圾回收是一个真正巨大的区别。Go中的GC使语言更简单，更小，更容易推理。在Rust中没有GC会让它变得非常快（尤其是当你需要保证延迟，而不仅仅是高吞吐量的时候），并且可以实现Go中不可能实现的功能和编程模式（或者至少是在不牺牲性能的情况下）。 &#8211; <a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">PingCAP</a></p>
</blockquote>
<h3>接近机器</h3>
<p>计算机编程的历史是一个越来越复杂的抽象的故事，它让程序员在解决问题时不用太担心底层机器的实际运作。</p>
<p>这使得程序更容易编写，也许更容易移植。但是对于许多程序来说，对硬件的访问以及对程序执行方式的精确控制更为重要。</p>
<p>Rust的目标是让程序员“更接近机器”，有更多的控制权，但Go抽象了架构细节，让程序员更接近问题。</p>
<blockquote>
<p>两种语言都有不同的适用范围。Go在编写微服务和典型的”DevOps”任务方面表现出色，但它不是一种系统编程语言。Rust对于那些看重并发性、安全性和性能的任务中更强；但它的学习曲线比Go更陡峭。 &#8211; <a href="https://endler.dev/2017/go-vs-rust/">Matthias Endler</a></p>
</blockquote>
<h3>必须运行更快</h3>
<p>许多人同意，对于大多数程序来说，<a href="https://bitfieldconsulting.com/golang/slower">性能不如可读性重要</a>。但当性能确实重要时，它真的很重要。Rust做了一些设计上的权衡，以达到尽可能好的执行速度。</p>
<p>相比之下，Go更关注简单性，它愿意为此牺牲一些（运行时）性能。但是Go的构建速度是无可匹敌的，这对于大型代码库来说是非常重要的。</p>
<blockquote>
<p>Rust比Go快。在基准测试中，Rust更快，在某些情况下，甚至是数量级的快。但在你选择用Rust写所有东西之前，考虑一下Go在许多基准测试中并不落后于它，而且它仍然比Java、C#、JavaScript、Python等快得多。如果你需要的是顶级的性能，那么选择这两种语言中的任何一种，你都会在游戏中领先。如果你正在构建一个处理高负载的网络服务，你希望能够在纵向和横向上进行扩展，那么这两种语言都会非常适合你。- <a href="https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9">安德鲁-拉德</a></p>
</blockquote>
<h3>正确性</h3>
<p>另一方面，如果一个程序不需要正常工作的话，它可以任意地快。大多数代码不是为长期而写的，但有些程序能在生产中运行多长时间往往是令人惊讶的：在某些情况下，可以保持几十年。</p>
<p>在这种情况下，值得在开发中多花一点时间，以确保程序的正确性、可靠性，并在未来不需要大量的维护。</p>
<p>Go和Rust都旨在帮助你编写正确的程序，但方式不同。例如，Go提供了一个极好的内置测试框架，而Rust则专注于使用其借用检查器消除运行时的错误。</p>
<blockquote>
<p>我认为。Go适用于明天必须交付的代码，而Rust适用于必须在未来五年内保持运行不动的代码。 &#8211; Grzegorz Nosek</p>
</blockquote>
<p>虽然Go和Rust对于任何严肃的项目来说都是很好的选择，但是让自己尽可能地了解每种语言及其特点是一个好主意。</p>
<p>归根结底，别人怎么想并不重要：只有你能决定哪种语言适合你和你的团队。</p>
<blockquote>
<p>如果你想加快开发速度，也许是因为你有许多不同的服务需要编写，或者你有一个庞大的开发团队，那么Go是你的首选语言。Go把并发性作为第一等公民给你，并且不容忍不安全的内存访问（Rust也是如此），但不强迫你管理每一个细节。Go是快速和强大的，但它避免了使开发者陷入困境，而是专注于简单性和统一性。如果在另一方面，拧出每一盎司的性能是必要的，那么Rust应该是你的选择。 &#8211; <a href="https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9">安德鲁-拉德</a></p>
</blockquote>
<h2>结论</h2>
<p>我希望这篇文章能让你相信Rust和Go都值得你认真考虑。如果可能的话，你应该争取在这两种语言中至少获得一定程度的经验，因为它们对你的任何技术职业都会有极大的帮助，甚至如果你仅把编程作为一种业余爱好的话。</p>
<p>如果你只有时间投资学习一门语言，在你将Go和Rust用于各种不同类型的大小程序之前，不要做出最终决定。</p>
<p>而编程语言的知识实际上只是成为一名成功的软件工程师的一小部分。到目前为止，你需要的最重要的技能是设计、工程、架构、沟通和协作。如果你在这些方面表现出色，无论你选择哪种语言，你都会成为一名优秀的软件工程师。学习愉快!</p>
<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/02/22/rust-vs-go-in-2023/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>针对大型数组的迭代，for range真的比经典for loop慢吗？</title>
		<link>https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array/</link>
		<comments>https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array/#comments</comments>
		<pubDate>Fri, 18 Mar 2022 22:19:14 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[for-range]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[range]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[SSA]]></category>
		<category><![CDATA[while]]></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=3466</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array Go语言推崇“一件事情仅有一个作法”！比如：Go仅保留一类循环控制语句，那就是经典版的for loop： for i := 0; i &#60; 100; i++ { ... ... } 而像C语言支持的while、do&#8230;while等循环控制语句都被排除在Go简洁的语法之外。但为了方便Go开发者对复合数据类型的迭代，比如：数组、切片、channel以及map等，Go提供了一个变种for range loop，甚至对于map、channel进行遍历，仅能使用for range loop，经典版for loop根本不支持。 不过for range 带来了方便的同时，也给Go初学者带来了一些烦恼，比如：for range迭代复合类型变量时就有一些常见的且十分容易掉入的“坑”，这些“坑”我在《Go语言第一课》中有全面详细的讲解。这里为了给后面的内容做铺垫，只提一个for range的坑，那就是参与循环的是range表达式的副本。 我们来看一个专栏中的例子： func main() { var a = [5]int{1, 2, 3, 4, 5} var r [5]int fmt.Println("original a =", a) for i, v := range a { if [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/for-range-vs-classic-for-loop-when-iterating-large-array-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array">本文永久链接</a> &#8211; https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array</p>
<p>Go语言推崇“<strong>一件事情仅有一个作法</strong>”！比如：Go仅保留一类循环控制语句，那就是<strong>经典版的for loop</strong>：</p>
<pre><code>for i := 0; i &lt; 100; i++ {
    ... ...
}
</code></pre>
<p>而像C语言支持的while、do&#8230;while等循环控制语句都被排除在Go简洁的语法之外。但为了方便Go开发者对复合数据类型的迭代，比如：数组、切片、channel以及map等，Go提供了一个变种for range loop，甚至对于map、channel进行遍历，仅能使用for range loop，经典版for loop根本不支持。</p>
<p>不过for range 带来了方便的同时，也给Go初学者带来了一些烦恼，比如：for range迭代复合类型变量时就有一些常见的且十分容易掉入的“坑”，这些“坑”我在<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>中有全面详细的讲解。这里为了给后面的内容做铺垫，只提一个for range的坑，那就是<strong>参与循环的是range表达式的副本</strong>。</p>
<p>我们来看一个专栏中的例子：</p>
<pre><code>func main() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int

    fmt.Println("original a =", a)

    for i, v := range a {
        if i == 0 {
            a[1] = 12
            a[2] = 13
        }
        r[i] = v
    }

    fmt.Println("after for range loop, r =", r)
    fmt.Println("after for range loop, a =", a)
}
</code></pre>
<p>大家来猜猜这段代码会输出什么结果？你是不是觉得这段代码会输出如下结果：</p>
<pre><code>original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]
</code></pre>
<p>但实际运行该程序的输出结果却是：</p>
<pre><code>original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]
</code></pre>
<p>我们原以为在第一次迭代过程，也就是i = 0时，我们对a的修改 (a[1] =12,a[2] = 13) 会在第二次、第三次迭代中被v取出，但从结果来看，v 取出的依旧是a被修改前的值：2和3。</p>
<p>为什么会是这种情况呢？原因就是<strong>参与for range循环的是range表达式的副本</strong>。也就是说，在上面这个例子中，真正参与循环的是a的副本，而不是真正的a。</p>
<p>为了方便你理解，我们将上面的例子中的for range循环，用一个等价的伪代码形式重写一下：</p>
<pre><code>for i, v := range a' { //a'是a的一个值拷贝
    if i == 0 {
        a[1] = 12
        a[2] = 13
    }
    r[i] = v
}
</code></pre>
<p>现在真相终于揭开了：这个例子中，每次迭代的都是从数组a的值拷贝a&#8217;中得到的元素。a&#8217;是Go临时分配的连续字节序列，与a完全不是一块内存区域。因此无论a被如何修改，它参与循环的副本a&#8217;依旧保持原值，因此v从a&#8217;中取出的仍旧是a的原值，而不是修改后的值。</p>
<p>好了，问题来了(来自专栏的一位童鞋的留言)！</p>
<p><img src="https://tonybai.com/wp-content/uploads/for-range-vs-classic-for-loop-when-iterating-large-array-2.png" alt="" /></p>
<p>这位童鞋的核心问题就一个：<strong>对于大型数组，由于参与for range的是该数组的拷贝，那么使用for range是不是会比经典for loop更耗资源且性能更差</strong>？</p>
<p>我们通过benchmark例子来验证一下：<strong>针对大型数组，for range是不是一定就比经典for loop跑得更慢</strong>？我们先看第一个例子：</p>
<pre><code>// benchmark1_test.go

package main

import "testing"

func BenchmarkClassicForLoopIntArray(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]int
    for i := 0; i &lt; b.N; i++ {
        for j := 0; j &lt; len(arr); j++ {
            arr[j] = j
        }
    }
}

func BenchmarkForRangeIntArray(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]int
    for i := 0; i &lt; b.N; i++ {
        for j, v := range arr {
            arr[j] = j
            _ = v
        }
    }
}
</code></pre>
<p>在这个例子中，我们分别用for loop与for range对一个拥有10w个int类型元素的数组进行遍历，我们看看benchmark的结果：</p>
<pre><code>// Go 1.18rc1, MacOS
$go test -bench . benchmark1_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkClassicForLoopIntArray-8          22080         55124 ns/op           0 B/op          0 allocs/op
BenchmarkForRangeIntArray-8                34808         34433 ns/op           0 B/op          0 allocs/op
PASS
ok      command-line-arguments  3.321s
</code></pre>
<p>从输出结果我们看到：for range loop非但未受到large array拷贝操作的影响，其性能居然比for range loop的性能还要好，这显然是在编译器层面(通常是静态单一赋值，即SSA环节)做了优化的结果。</p>
<p>我们关闭优化开关，再运行一下压测：</p>
<pre><code>$go test -c -gcflags '-N -l' .
$./demo.test -test.bench .
goos: darwin
goarch: amd64
pkg: demo
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkClassicForLoopIntArray-8           6248        187773 ns/op           0 B/op          0 allocs/op
BenchmarkForRangeIntArray-8                 4768        246512 ns/op           0 B/op          0 allocs/op
PASS
</code></pre>
<p>我们看到：在没有优化的情况下，两种loop的性能都大幅下降，并且for range下降更多，性能显著不如经典for loop。你可以对比一下BenchmarkForRangeIntArray函数在正常优化(go tool compile -S xxx.go)以及关闭优化时(go tool compile -S -N -l)的汇编代码片段，你会发现关闭优化后，汇编代码使用了很多中间变量存储中间结果，而优化后的代码则消除了这些中间状态。</p>
<p>那么接下来你可能会提出这样一个问题：是不是for range迭代任何元素类型的大型数组，其性能都不比经典for loop差呢？我们来看一个对结构体数组遍历的例子：</p>
<pre><code>// benchmark3_test.go
package main

import "testing"

type U5 struct {
    a, b, c, d, e int
}
type U4 struct {
    a, b, c, d int
}
type U3 struct {
    b, c, d int
}
type U2 struct {
    c, d int
}
type U1 struct {
    d int
}

func BenchmarkClassicForLoopLargeStructArrayU5(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U5
    for i := 0; i &lt; b.N; i++ {
        for j := 0; j &lt; len(arr)-1; j++ {
            arr[j].d = j
        }
    }
}
func BenchmarkClassicForLoopLargeStructArrayU4(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U4
    for i := 0; i &lt; b.N; i++ {
        for j := 0; j &lt; len(arr)-1; j++ {
            arr[j].d = j
        }
    }
}
func BenchmarkClassicForLoopLargeStructArrayU3(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U3
    for i := 0; i &lt; b.N; i++ {
        for j := 0; j &lt; len(arr)-1; j++ {
            arr[j].d = j
        }
    }
}
func BenchmarkClassicForLoopLargeStructArrayU2(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U2
    for i := 0; i &lt; b.N; i++ {
        for j := 0; j &lt; len(arr)-1; j++ {
            arr[j].d = j
        }
    }
}

func BenchmarkClassicForLoopLargeStructArrayU1(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U1
    for i := 0; i &lt; b.N; i++ {
        for j := 0; j &lt; len(arr)-1; j++ {
            arr[j].d = j
        }
    }
}

func BenchmarkForRangeLargeStructArrayU5(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U5
    for i := 0; i &lt; b.N; i++ {
        for j, v := range arr {
            arr[j].d = j
            _ = v
        }
    }
}
func BenchmarkForRangeLargeStructArrayU4(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U4
    for i := 0; i &lt; b.N; i++ {
        for j, v := range arr {
            arr[j].d = j
            _ = v
        }
    }
}

func BenchmarkForRangeLargeStructArrayU3(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U3
    for i := 0; i &lt; b.N; i++ {
        for j, v := range arr {
            arr[j].d = j
            _ = v
        }
    }
}
func BenchmarkForRangeLargeStructArrayU2(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U2
    for i := 0; i &lt; b.N; i++ {
        for j, v := range arr {
            arr[j].d = j
            _ = v
        }
    }
}
func BenchmarkForRangeLargeStructArrayU1(b *testing.B) {
    b.ReportAllocs()
    var arr [100000]U1
    for i := 0; i &lt; b.N; i++ {
        for j, v := range arr {
            arr[j].d = j
            _ = v
        }
    }
}
</code></pre>
<p>在这个例子中，我们定义了5种结构体：U1~U5，它们的不同之处就在于包含的int类型字段的个数不同。我们分别用经典for loop与for range loop对以这些类型为元素的大型数组进行遍历，看看结果如何：</p>
<pre><code>$go test -bench . benchmark3_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkClassicForLoopLargeStructArrayU5-8        22030         54116 ns/op           0 B/op          0 allocs/op
BenchmarkClassicForLoopLargeStructArrayU4-8        22131         54145 ns/op           0 B/op          0 allocs/op
BenchmarkClassicForLoopLargeStructArrayU3-8        22257         54001 ns/op           0 B/op          0 allocs/op
BenchmarkClassicForLoopLargeStructArrayU2-8        22063         54580 ns/op           0 B/op          0 allocs/op
BenchmarkClassicForLoopLargeStructArrayU1-8        22105         54408 ns/op           0 B/op          0 allocs/op
BenchmarkForRangeLargeStructArrayU5-8               3022        391232 ns/op           0 B/op          0 allocs/op
BenchmarkForRangeLargeStructArrayU4-8               4563        265919 ns/op           0 B/op          0 allocs/op
BenchmarkForRangeLargeStructArrayU3-8               6602        182224 ns/op           0 B/op          0 allocs/op
BenchmarkForRangeLargeStructArrayU2-8              10000        111966 ns/op           0 B/op          0 allocs/op
BenchmarkForRangeLargeStructArrayU1-8              35380         34005 ns/op           0 B/op          0 allocs/op
PASS
ok      command-line-arguments  15.907s
</code></pre>
<p>我们看到一个奇怪的现象：无论是哪种结构体类型，经典for loop遍历的性能都是一样的，但<strong>for range的遍历性能却会随着结构体字段数量的增多而下降</strong>。</p>
<p>带着疑惑，我找到了与这个问题有关的一个issue：<a href="https://github.com/golang/go/issues/24416">cmd/compile: optimize large structs</a>，这个issue大致是说对于包含特定数量字段的结构体类型，目前是<strong>unSSAable</strong>，如果不能SSA，那么就无法通过SSA优化，这也是出现上述benchmark结果的重要原因。</p>
<p>在Go中，几乎所有使用数组的地方都可以用切片替代，笔者还是建议<strong>尽量用迭代切片替换对数组的迭代</strong>，这样总是可以取得一致且稳定的遍历性能。</p>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go正走在成为下一个企业级编程语言的轨道上</title>
		<link>https://tonybai.com/2019/05/03/go-is-on-a-trajectory-to-become-the-next-enterprise-programming-language/</link>
		<comments>https://tonybai.com/2019/05/03/go-is-on-a-trajectory-to-become-the-next-enterprise-programming-language/#comments</comments>
		<pubDate>Fri, 03 May 2019 06:01:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[BSD]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[error-handling]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go2]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[hotspot]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JIT]]></category>
		<category><![CDATA[JVM]]></category>
		<category><![CDATA[KenThompson]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[OOP]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Ops]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[reader]]></category>
		<category><![CDATA[RobertGriesemer]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[terraform]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[writer]]></category>
		<category><![CDATA[云计算]]></category>
		<category><![CDATA[企业级]]></category>
		<category><![CDATA[包管理]]></category>
		<category><![CDATA[多核]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[并行]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[文档]]></category>
		<category><![CDATA[架构师]]></category>
		<category><![CDATA[注释]]></category>
		<category><![CDATA[维护性]]></category>
		<category><![CDATA[编译]]></category>
		<category><![CDATA[翻译]]></category>
		<category><![CDATA[虚拟机]]></category>
		<category><![CDATA[错误处理]]></category>
		<category><![CDATA[面向对象]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2711</guid>
		<description><![CDATA[发展演化了十年的Go语言已经被证明了是云计算时代的首选编程语言，但Go的用武之地显然不局限于此。Kevin Goslar近期在Hacker Noon发表了一篇名为：《Go is on a Trajectory to Become the Next Enterprise Programming Language》的文章，阐述了Go可能成为下一个企业编程语言的理由，这里是那篇文章的中文译文，分享给大家。 摘要 Go是一种专门为大规模软件开发而设计的编程语言。它提供了强大的开发体验并避免了现有编程语言存在的许多问题。这些因素使其成为最有可能在未来替代Java主导企业软件平台的候选者之一。对于那些寻求在未来几十年内构建大规模云基础架构的安全和前瞻性技术的公司和开源计划而言，我建议它们将Go视为其主要的编程语言。Go的优势如下： 基于现实世界的经验 专注于大型工程 专注于可维护性 保持简单明了 使事情显式且明显 很容易学习 仅提供了一种做事方式 支持简单地内置并发 提供面向计算的语言原语 使用OO &#8211; 好的部分 拥有现代化的标准库 强制执行标准化格式 有一个非常快的编译器 使交叉编译变得容易 执行得非常快 需要较小的内存占用 部署规模小 部署完全独立 支持vendor依赖 提供兼容性保证 鼓励提供良好的文档 商业支持的开源 请继续阅读有关上述每个优势点的更多详细信息。然而，在进入Go之前，你应该注意： 不成熟的库 即将到来的改变 没有“硬实时”支持 简介 Go是Google开发的一种编程语言，在过去几年中取得了很大的成功。大部分现代云计算，网络和DevOps平台都是Go语言编写的，例如：Docker、Kubernetes、Terraform、ETCD或istio等。许多公司也将它用于通用软件开发。Go所具备的功能让这些项目吸引了大量用户，而Go的易用性也使得这些项目有了很多的贡献者。 Go的优势来自于简单和经过验证的想法的结合，同时避免了其他语言中出现的许多问题。这篇博客文章概述了Go背后的一些设计原则和工程智慧，并展示它们是如何结合在一起的 &#8211; 它们使Go成为下一代大型软件开发平台的优秀候选者。许多编程语言在个别领域都比较强大，但是在将所有领域都结合起来时，没有其他语言能够如此一致地“得分”，特别是在大型软件工程方面。 基于现实世界的经验 Go是由经验丰富的软件行业资深人士创建的，他们长期以来一直感受到现有语言的缺点带来的痛苦。几十年前，Rob Pike和Ken Thompson在Unix，C和Unicode的发明中发挥了重要作用。在实现了用于JavaScript和Java的V8和HotSpot虚拟机之后，Robert Griesemer在编译器和垃圾收集方面拥有着数十年的经验。在太多次的不得不等待他们的谷歌规模的C++/Java代码库的编译过程的推动下，他们开始着手创建一门新的编程语言，这门语言中凝聚了他们通过编写半个世纪代码过程中所学到的一切。 专注于大型工程 [...]]]></description>
			<content:encoded><![CDATA[<p>发展演化了<a href="https://tonybai.com/2017/09/24/go-ten-years-and-climbing/">十年的Go语言</a>已经被证明了是云计算时代的首选编程语言，但<a href="https://tonybai.com/tag/go">Go</a>的用武之地显然不局限于此。Kevin Goslar近期在<a href="https://hackernoon.com/">Hacker Noon</a>发表了一篇名为：<a href="https://hackernoon.com/go-is-on-a-trajectory-to-become-the-next-enterprise-programming-language-3b75d70544e">《Go is on a Trajectory to Become the Next Enterprise Programming Language》</a>的文章，阐述了Go可能成为下一个企业编程语言的理由，这里是那篇文章的中文译文，分享给大家。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-enterprise-programming-lang-1.png" alt="img{512x368}" /></p>
<h2>摘要</h2>
<p>Go是一种专门为大规模软件开发而设计的编程语言。它提供了强大的开发体验并避免了现有编程语言存在的许多问题。这些因素使其成为最有可能在未来替代Java主导企业软件平台的候选者之一。对于那些寻求在未来几十年内构建大规模云基础架构的安全和前瞻性技术的公司和开源计划而言，我建议它们将Go视为其主要的编程语言。Go的优势如下：</p>
<ul>
<li>基于现实世界的经验</li>
<li>专注于大型工程</li>
<li>专注于可维护性</li>
<li>保持简单明了</li>
<li>使事情显式且明显</li>
<li>很容易学习</li>
<li>仅提供了一种做事方式</li>
<li>支持简单地内置并发</li>
<li>提供面向计算的语言原语</li>
<li>使用OO &#8211; 好的部分</li>
<li>拥有现代化的标准库</li>
<li>强制执行标准化格式</li>
<li>有一个非常快的编译器</li>
<li>使交叉编译变得容易</li>
<li>执行得非常快</li>
<li>需要较小的内存占用</li>
<li>部署规模小</li>
<li>部署完全独立</li>
<li>支持vendor依赖</li>
<li>提供兼容性保证</li>
<li>鼓励提供良好的文档</li>
<li>商业支持的开源</li>
</ul>
<p>请继续阅读有关上述每个优势点的更多详细信息。然而，在进入Go之前，你应该注意：</p>
<ul>
<li>不成熟的库</li>
<li>即将到来的改变</li>
<li>没有“硬实时”支持</li>
</ul>
<h2>简介</h2>
<p>Go是Google开发的一种编程语言，在过去几年中取得了很大的成功。大部分现代云计算，网络和DevOps平台都是Go语言编写的，例如：<a href="https://tonybai.com/tag/docker">Docker</a>、<a href="https://tonybai.com/tag/kubernetes">Kubernetes</a>、<a href="https://www.terraform.io/">Terraform</a>、<a href="https://coreos.com/etcd/">ETCD</a>或<a href="https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio">istio</a>等。<a href="https://github.com/golang/go/wiki/GoUsers#united-states">许多公司</a>也将它用于通用软件开发。Go所具备的功能让这些项目吸引了大量用户，而Go的易用性也使得这些项目有了很多的贡献者。</p>
<p>Go的优势来自于简单和经过验证的想法的结合，同时避免了其他语言中出现的许多问题。这篇博客文章概述了Go背后的一些设计原则和工程智慧，并展示它们是如何结合在一起的 &#8211; 它们使Go成为下一代大型软件开发平台的优秀候选者。许多编程语言在个别领域都比较强大，但是在将所有领域都结合起来时，没有其他语言能够如此一致地“得分”，特别是在大型软件工程方面。</p>
<h2>基于现实世界的经验</h2>
<p>Go是由经验丰富的软件行业资深人士创建的，他们长期以来一直感受到现有语言的缺点带来的痛苦。几十年前，<a href="https://en.wikipedia.org/wiki/Rob_Pike">Rob Pike</a>和<a href="https://en.wikipedia.org/wiki/Ken_Thompson">Ken Thompson</a>在Unix，C和Unicode的发明中发挥了重要作用。在实现了用于JavaScript和Java的V8和HotSpot虚拟机之后，<a href="https://github.com/griesemer">Robert Griesemer</a>在编译器和垃圾收集方面拥有着数十年的经验。在太多次的不得不等待他们的谷歌规模的<a href="http://radar.oreilly.com/2012/09/golang.html">C++/Java代码库的编译过程</a>的推动下，他们开始着手创建一门新的编程语言，这门语言中凝聚了他们通过编写半个世纪代码过程中所学到的一切。</p>
<h2>专注于大型工程</h2>
<p>几乎任何编程语言都可以成功构建小型工程项目。当成千上万的开发人员在数十年的持续时间压力下在包含数千万行代码的大量代码库上进行协作时，真正痛苦的问题就会发生。这会导致以下问题：</p>
<ul>
<li>超长的编译时长会中断开发过程</li>
<li>代码库由几个人/团队/部门/公司拥有，混合了不同的编程风格</li>
<li>该公司雇佣了数千名工程师，架构师，测试人员，Ops专家，审计员，实习生等，他们需要了解代码库，但需要具有广泛的编码经验</li>
<li>依赖于许多外部库或运行时，其中一些不再以其最初的形式存在</li>
<li>每行代码在代码库的生命周期内平均被<a href="https://www.ybrikman.com/writing/2018/08/12/the-10-to-1-rule-of-writing-and-programming">重写了10次</a>，留下了疤痕，瑕疵和<a href="https://codeclimate.com/blog/are-you-experiencing-technical-drift">技术偏移</a></li>
<li>文档不完整</li>
</ul>
<p>Go专注于<a href="https://talks.golang.org/2012/splash.article">减轻这些大规模的工程难题</a>，有时是以使小型工程变得更加繁琐为代价，例如在这里和那里需要一些额外的代码。</p>
<h2>专注于可维护性</h2>
<p>Go强调尽可能多地将工作转交到自动代码维护工具中。Go工具链提供了最常用的功能，如格式化代码和自动package导入、查找符号的定义和用法、简单的重构以及代码味道的识别。由于标准化的代码格式化和单一的惯用方式，机器生成的代码更改看起来非常接近Go中人为生成的更改。并而使用类似的模式，使得人和机器的协作更加无缝。</p>
<h2>保持简单直接</h2>
<blockquote>
<p>初级程序员为简单问题创建简单的解决方案。高级程序员为复杂问题创建复杂的解决方案。伟大的程序员找到复杂问题的简单解决方案。-  <a href="http://www.chc-3.com/pub/beautifulsoftware_v10.htm">查尔斯康奈尔</a></p>
</blockquote>
<p>很多人都对Go不包含他们喜欢的其他语言概念感到惊讶。Go确实是一种非常小而简单的语言，只包含最少的正交和经过验证的概念。这鼓励开发人员以最少的认知开销编写最简单的代码，以便许多其他人可以理解并使用它。</p>
<h2>使事情显式而明显</h2>
<blockquote>
<p>良好的代码是显而易见的，避免聪明，模糊的语言功能，扭曲的控制流和间接性。</p>
</blockquote>
<p>许多语言都致力于使编写代码变得高效。然而，在其生命周期中，人们将花费大约（100倍）的时间阅读代码，而不是首先编写所需的代码。例如，审查，理解，调试，更改，重构或重用它。在查看代码时，通常只能看到并理解它的一小部分，通常没有对整个代码库的完整理解。为了解释这一点，Go将一切都显式化了。</p>
<p>一个例子是错误处理。让异常在各个点中断代码并使沿着调用链处理可能会更容易。Go需要<a href="https://tour.golang.org/methods/19">手动处理或返回每个错误</a>。这使得它可以准确地显示代码可以被中断的位置以及如何处理或包装错误。总的来说，这使得错误处理更容易编写，但更容易理解。</p>
<h2>简单易学</h2>
<p>Go非常小而且简单，可以在短短几天内研究整个语言及其基本概念。根据我们的经验，经过不超过一周的培训（与其他语言的以月为单位相比），初学者可以理解Go专家编写的代码，并为此做出贡献。为了方便大量人群，Go网站提供了所需的所有教程和深入的文章。这些教程在浏览器中运行，允许人们在将Go安装到本地计算机上之前学习和使用Go。</p>
<h2>一种做事方式</h2>
<p>Go语言通过个人自我表达赋予团队合作能力。</p>
<p>在Go（和Python）中，所有语言特征都是正交的并且彼此互补，通常做某事只有一种方法。如果您要求10位Python或Go程序员解决问题，您将获得10个相对类似的解决方案。不同的程序员在彼此的代码库中感觉更有家的感觉。在查看其他人的代码时，每分钟的<a href="https://www.osnews.com/story/19266/wtfsm">WTF</a>更少，而且人们的工作更好地融合在一起，从而形成一个人人都为之骄傲并且喜欢工作的一致性。这避免了大规模的工程问题，例如：</p>
<ul>
<li>开发人员将良好的工作代码视为“混乱”，并要求在他们可以使用之前重写它，因为他们不会像原作者那样思考。</li>
<li>不同的团队成员在该语言的不同子集中编写相同代码库的部分内容。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/go-enterprise-programming-lang-2.jpeg" alt="img{512x368}" /><br />
来源：https：//www.osnews.com/story/19266/wtfsm</p>
<h2>简单，内置并发</h2>
<blockquote>
<p>Go专为现代多核硬件而设计。</p>
</blockquote>
<p>目前使用的大多数编程语言（Java，JavaScript，Python，Ruby，C，C ++）都是在20世纪80年代到2000年代设计的，当时大多数CPU只有一个计算核心。这就是为什么它们本质上是单线程的，并将并行化视为事后增加的边缘情况，通过诸如线程和同步点之类的附加组件实现，这些附加组件既麻烦又难以正确使用。第三方库提供了更简单的并发形式，如Actor模型，但总有多个选项可用，导致语言生态系统碎片化。今天的硬件拥有越来越多的计算内核，软件必须并行化才能在其上高效运行。Go是在多核CPU时代编写的，并且在语言中内置了简单，高级的<a href="https://en.wikipedia.org/wiki/Communicating_sequential_processes">CSP风格</a>的并发特性。</p>
<h2>面向计算的语言原语</h2>
<p>在基础层面上，计算机系统接收数据，处理它（通常经过几个步骤），并输出结果数据。例如，Web服务器从客户端接收HTTP请求，并将其转换为一系列数据库或后端调用。一旦这些调用返回，它就会将接收到的数据转换为HTML或JSON并将其输出给调用者。Go的内置语言原语直接支持这种范例：</p>
<ul>
<li>结构体代表数据</li>
<li>reader和writer代表流式IO</li>
<li>函数处理数据</li>
<li>goroutines提供（几乎无限制的）并发</li>
<li>通道用于管理并发处理步骤之间的数据</li>
</ul>
<p>由于所有计算原语都是由语言以直接的形式提供的，因此Go源代码可以更直接地表达服务器执行的操作。</p>
<h2>OO &#8211; 好的部分</h2>
<p><img src="https://tonybai.com/wp-content/uploads/go-enterprise-programming-lang-3.gif" alt="img{512x368}" /><br />
在基类中改变某些东西的副作用</p>
<p>面向对象非常有用。这几十年OO的应用是富有成效的，并且让我们了解它的哪些部分比其他部分可以更好地扩展。基于这些认知，Go采用面向对象的新方法。它保留了封装和消息传递等优点。Go避免了继承，因为它现在被认为是<a href="https://www.javaworld.com/article/2073649/why-extends-is-evil.html">有害的</a>，Go为组合提供<a href="https://golang.org/doc/effective_go.html#embedding">头等的支持</a>。</p>
<h2>现代标准库</h2>
<p>许多当前使用的编程语言（Java，JavaScript，Python，Ruby）是在互联网成为当今无处不在的计算平台之前设计的。因此，这些语言的标准库仅为未针对现代互联网优化的网络提供相对通用的支持。Go是十年前创建的，当时互联网已经全面展开。Go的标准库允许在没有第三方库的情况下创建更复杂的网络服务。这可以防止使用第三方库的常见问题：</p>
<ul>
<li>碎片化：实现相同功能的总有多种选择</li>
<li>膨胀：库通常实现的不仅仅是它们的用途</li>
<li>依赖地狱：库通常依赖于特定版本的其他库</li>
<li>质量未知：第三方代码可能具有可疑的质量和安全性</li>
<li>未知支持：第三方库的开发可以随时停止</li>
<li>意外更改：第三方库通常不像标准库那样进行严格的版本管理</li>
</ul>
<p>Russ Cox的<a href="https://research.swtch.com/deps">更多背景</a>信息。</p>
<h2>标准化格式</h2>
<blockquote>
<p>Gofmt的风格是没有人喜欢的，但gofmt是每个人的最爱。 &#8211; Rob Pike</p>
</blockquote>
<p>Gofmt是一种以标准化方式格式化Go代码的程序。它不是最漂亮的格式化方式，而是最简单，最不讨厌的方式。标准化的源代码格式化具有惊人的积极影响：</p>
<ul>
<li>重点讨论重要主题：它消除了围绕标签与空格，缩进深度，每行长度，空行，花括号放置等的一系列<a href="https://en.wikipedia.org/wiki/Law_of_triviality">无意义的争论</a>。</li>
<li>开发人员在彼此的代码库中感到宾至如归，因为其他代码看起来很像他们编写的代码。每个人都喜欢自由地按照自己喜欢的方式格式化代码，但如果其他人冒昧地按照他们自己喜欢的方式格式化>代码，那么每个人都讨厌它。</li>
<li>自动代码更改不会弄乱手写代码的格式，例如通过引入意外的空白更改。</li>
</ul>
<p>许多其他语言社区现在正在开发gofmt等价物。当构建为第三方解决方案时，通常会有几种竞争格式标准。例如，JavaScript世界提供<a href="https://prettier.io/">Prettier</a>和<a href="https://standardjs.com/">StandardJS</a>。可以一起使用其中之一或两者。许多JS项目都没有采用它们，因为这是一个额外的决定。Go的格式化程序内置于该语言的标准工具链中，因此只有一个标准，每个人都在使用它。</p>
<h2>快速编译</h2>
<p><img src="https://tonybai.com/wp-content/uploads/go-enterprise-programming-lang-4.png" alt="img{512x368}" /><br />
来源：https://xkcd.com/303</p>
<p>大型代码库的长编译时间是引发Go语言起源的一个微小的原因。Google主要使用C++和Java，与Haskell，Scala或Rust等更复杂的语言相比，它可以相对快速地编译。尽管如此，当编译大型代码库时，即使是少量的慢速也会把人激怒，编译工作流中断导致编译延迟。Go是从头开始设计的，以使编译更有效，因此编译器速度非常快，几乎没有编译延迟。这为Go开发人员提供了类似于脚本语言的即时反馈，并具有静态类型检查的额外好处。</p>
<h2>交叉编译</h2>
<p>由于语言运行时非常简单，因此它已被移植到许多平台，如macOS，Linux，Windows，BSD，ARM等。Go可以开箱即用于编译所有这些平台的二进制文件。这使得我们可以轻松地从一台机器来进行部署。</p>
<h2>快速执行</h2>
<p>Go有着接近C的速度。与JITed(即时编译)语言（Java，JavaScript，Python等）不同，Go二进制文件不需要启动或预热时间，因为它们作为已编译和完全优化的本机代码提供。Go垃圾收集器仅以<a href="https://twitter.com/brianhatfield/status/804355831080751104">微秒</a>的指令引入可忽略的暂停。在其快速的单核性能上面，Go使得利用所有的CPU内核<a href="https://tour.golang.org/concurrency/1">更容易</a>。</p>
<h2>小内存占用</h2>
<p>像JVM，Python或Node这样的运行时不仅仅在运行时加载程序代码。它们还会加载大型且高度复杂的基础架构，以便在每次运行时编译和优化程序。这使得它们的启动时间变慢并导致它们使用大量（数百MB）的RAM。Go进程的开销较小，因为它们已经完全编译和优化，只需要运行。Go还以<a href="https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast">非常节省内存的方式存储数据</a>。这在内存有限且昂贵的云环境中以及在开发期间非常重要，在开发期间我们希望在单个机器上快速启动整个堆栈，同时为其他软件留下内存。</p>
<h2>小部署规模</h2>
<p>Go二进制文件的大小非常简洁。Go应用程序的Docker镜像通常比用Java或Node编写的等效文件<a href="https://derickbailey.com/2017/03/09/selecting-a-node-js-image-for-docker">小10倍</a>，因为它不需要包含编译器，JIT，并且需要更少的运行时基础结构。这在部署大型应用程序时很重要。想象一下，将一个简单的应用程序部署到100个生产服务器上 使用Node / JVM时，我们的docker仓库必须提供100个docker镜像@ 200 MB = 20 GB(总共)。这需要镜像仓库耗费一些时间来服务。想象一下，我们希望每天部署100次。使用Go服务时，Docker镜像仓库只需提供100个Docker镜像@ 20 MB = 2 GB。可以更快，更频繁地部署大型Go应用程序，从而允许重要更新更快地实现生产。</p>
<h2>自包含部署</h2>
<p>Go应用程序部署为包含所有依赖项的单个可执行文件。不需要安装特定版本的JVM，Node或Python运行时。不必将库下载到生产服务器上。不需要对运行Go二进制文件的机器进行任何更改。甚至不需要将Go二进制文件包装到Docker中来共享它们。您只需将Go二进制文件拖放到服务器上，无论该服务器上运行的是什么，它都会在那里运行。上述描述的唯一例外是使用net和os/user包时的动态链接glibc库时。</p>
<h2>vendor依赖关系</h2>
<p>Go故意避免使用第三方库的中央存储库。Go应用程序直接链接到相应的Git存储库，并将所有相关代码下载（vendor保存）到他们自己的代码库中。这有很多好处：</p>
<ul>
<li>我们可以在使用之前查看，分析和测试第三方代码。此代码与我们自己的代码一样，是我们应用程序的一部分，应符合相同的质量，安全性和可靠性标准。</li>
<li>无需永久访问存储依赖项的各个位置。可以一次性的从任何地方（包括私人Git仓库）获取您的第三方库，并永久拥有它们。</li>
<li>在checkout后编译代码库不需要进一步下载依赖项。</li>
<li>如果互联网上某处的代码存储库突然提供不同的代码，也不会造成surprises。</li>
<li>即使软件包存储库服务性能变慢或托管软件包不再存在，部署也不会中断。</li>
</ul>
<h2>兼容性保证</h2>
<p>Go团队<a href="https://golang.org/doc/go1compat">承诺</a>，现有的程序将继续适用于新版本语言。这使得即使是大型项目也可以轻松升级到更新编译器的版本，并从新版本带来的许多性能和安全性改进中受益。同时，由于Go二进制文件包含了他们需要的所有依赖项，因此可以在同一服务器计算机上并行运行使用不同版本的Go编译器编译的二进制文件，而无需进行复杂的设置多个版本的运行时或虚拟化。</p>
<h2>文档</h2>
<p>在大型工程中，文档对于使软件易于访问和维护非常重要。与其他功能类似，Go中的文档简单实用：</p>
<ul>
<li>它嵌入在源代码中，因此两者可以同时维护。</li>
<li>它不需要特殊的语法 &#8211; 文档只是普通的源代码注释。</li>
<li>可运行的单元测试通常是最好的文档形式，所以Go允许你将它们<a href="https://blog.golang.org/examples">嵌入到文档中</a>。</li>
<li>所有文档<a href="https://blog.golang.org/godoc-documenting-go-code">实用程序</a>都内置在工具链中，因此每个人都使用它们。</li>
<li>Go linter需要导出元素的文档，以防止“文档债务”的积累。</li>
</ul>
<h2>商业支持的开源</h2>
<p>当商业实体在公开场合发展时，一些最流行和最全面设计的软件就会发生。这种设置结合了商业软件开发的优势 &#8211; 一致性和优化，使系统健壮，可靠，高效 &#8211; 具有开放式开发的优势，如来自许多行业的广泛支持，来自多个大型实体和许多用户的支持，以及长期支持，即使商业支持停止。Go就是这样开发的。</p>
<h2>缺点</h2>
<p>当然，Go并不完美，每种技术选择总是有利有弊。在进入Go之前，这里有一小部分需要考虑的方面。</p>
<h2>未成熟</h2>
<p>虽然Go的标准库在<a href="https://blog.golang.org/h2push">支持HTTP/2服务器推送</a>等许多新概念方面处于行业领先地位，但与JVM生态系统中存在的相比，用于外部API的第三方Go库可能还不那么成熟。</p>
<h2>即将到来的变化</h2>
<p>Go团队知道几乎不可能改变现有的语言元素，因此只有在完全开发后才会添加新功能。在经历了10年稳定的故意阶段后，Go团队正在考虑对语言进行<a href="https://blog.golang.org/go2draft">一系列更大的改进</a>，作为Go 2.0之旅的一部分。</p>
<h2>没有硬实时</h2>
<p>虽然Go的垃圾收集器只引入了非常短的中断，但支持硬实时需要没有垃圾收集的技术，例如Rust。</p>
<h2>结论</h2>
<p>这篇博客文章给出了一些明智的背景知识，但往往没有那么明显的选择进入Go的设计，以及当他们的代码库和团队成数量级增长时，他们将如何从许多痛苦中拯救大型工程项目。总的来说，他们将Go定位为寻求Java之外的现代编程语言的大型开发项目的绝佳选择。</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>我的联系方式：</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; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/05/03/go-is-on-a-trajectory-to-become-the-next-enterprise-programming-language/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Golang跨平台交叉编译</title>
		<link>https://tonybai.com/2014/10/20/cross-compilation-with-golang/</link>
		<comments>https://tonybai.com/2014/10/20/cross-compilation-with-golang/#comments</comments>
		<pubDate>Mon, 20 Oct 2014 08:40:52 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[6g]]></category>
		<category><![CDATA[6l]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[Darwin]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GOARCH]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOOS]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[链接器]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1571</guid>
		<description><![CDATA[近期在某本书上看到Go跨平台交叉编译的强大功能，于是想自己测试一下。以下记录了测试过程以及一些结论，希望能给大家带来帮助。 我的Linux环境如下： uname -a Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ go version go version go1.3.1 linux/amd64 跨平台交叉编译涉及两个重要的环境变量：GOOS和GOARCH，分别代表Target Host OS和Target Host ARCH，如果没有显式设置这些环境变量，我们通过go env可以看到go编译器眼中这两个环境变量的当前值： $ go env GOARCH=&#34;amd64&#34; GOOS=&#34;linux&#34; GOHOSTARCH=&#34;amd64&#34; GOHOSTOS=&#34;linux&#34; &#8230; &#8230; 这里还有两个变量GOHOSTOS和GOHOSTARCH，分别表示的是当前所在主机的的OS和CPU ARCH。我的Go是采用安装包安装的，因此默认情况下，这两组环境变量的值都是来自当前主机的信息。 现在我们就来交叉编译一下：在linux/amd64平台下利用Go编译器编译一个可以运行在linux/amd64下的程序，样例程序如下： //testport.go package main import ( &#160;&#160;&#160;&#160;&#160;&#160;&#160; &#34;fmt&#34; &#160;&#160;&#160;&#160;&#160;&#160;&#160; &#34;os/exec&#34; &#160;&#160;&#160;&#160;&#160;&#160;&#160; &#34;bytes&#34; [...]]]></description>
			<content:encoded><![CDATA[<p>近期在某本书上看到<a href="http://golang.org">Go</a>跨平台交叉编译的强大功能，于是想自己测试一下。以下记录了测试过程以及一些结论，希望能给大家带来帮助。</p>
<p>我的Linux环境如下：</p>
<p><font face="Courier New">uname -a<br />
	Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux</font></p>
<p><font face="Courier New">$ go version<br />
	go version go1.3.1 linux/amd64</font></p>
<p>跨平台交叉编译涉及两个重要的环境变量：GOOS和GOARCH，分别代表Target Host OS和Target Host ARCH，如果没有显式设置这些环境变量，我们通过go env可以看到go编译器眼中这两个环境变量的当前值：</p>
<p><font face="Courier New">$ go env<br />
	GOARCH=&quot;amd64&quot;<br />
	GOOS=&quot;linux&quot;</font><br />
	<font face="Courier New">GOHOSTARCH=&quot;amd64&quot;<br />
	GOHOSTOS=&quot;linux&quot;</font><br />
	&#8230; &#8230;</p>
<p>这里还有两个变量GOHOSTOS和GOHOSTARCH，分别表示的是当前所在主机的的OS和CPU ARCH。我的Go是采用安装包安装的，因此默认情况下，这两组环境变量的值都是来自当前主机的信息。</p>
<p>现在我们就来交叉编译一下：在linux/amd64平台下利用Go编译器编译一个可以运行在linux/amd64下的程序，样例程序如下：</p>
<p><font face="Courier New">//testport.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;os/exec&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;bytes&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cmd := exec.Command(&quot;uname&quot;, &quot;-a&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var out bytes.Buffer<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cmd.Stdout = &amp;out</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := cmd.Run()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Err when executing uname command&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;I am running on&quot;, out.String())<br />
	}</font></p>
<p>在Linux/amd64下编译运行：</p>
<p><font face="Courier New">$ go build -o testport_linux testport.go<br />
	$ testport_linux<br />
	I am running on Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux</font></p>
<p>接下来，我们来尝试在Linux/amd64上编译一个可以运行在darwin/amd64上的程序。我只需修改GOOS和GOARCH两个标识目标主机OS和ARCH的环境变量：</p>
<p><font face="Courier New">$ GOOS=darwin GOARCH=amd64 go build -o testport_darwin testport.go<br />
	go build runtime: darwin/amd64 must be bootstrapped using make.bash</font></p>
<p>编译器报错了！提示darwin/amd64必须通过make.bash重新装载。显然，通过安装包安装到linux/amd64下的Go编译器还无法直接交叉编译出darwin/amd64下可以运行的程序，我们需要做一些准备工作。我们找找make.bash在哪里！</p>
<p>我们到Go的$GOROOT路径下去找make.bash，Go的安装路径下的组织很简约，扫一眼便知make.sh大概在$GOROOT/src下，打开make.sh，我们在文件头处看到如下一些内容：</p>
<p><font face="Courier New"># Environment variables that control make.bash:<br />
	#<br />
	# GOROOT_FINAL: The expected final Go root, baked into binaries.<br />
	# The default is the location of the Go tree during the build.<br />
	#<br />
	# GOHOSTARCH: The architecture for host tools (compilers and<br />
	# binaries).&nbsp; Binaries of this type must be executable on the current<br />
	# system, so the only common reason to set this is to set<br />
	# GOHOSTARCH=386 on an amd64 machine.<br />
	#<br />
	# GOARCH: The target architecture for installed packages and tools.<br />
	#<br />
	# GOOS: The target operating system for installed packages and tools.</font><br />
	&#8230; &#8230;</p>
<p>make.bash头并未简要说明文件的用途，但名为make.xx的文件想必是用来构建Go编译工具的。这里提到几个环境变量可以控制 make.bash的行为，显然GOARCH和GOOS更能引起我们的兴趣。我们再回过头来输出testport.go编译过程的详细信息：</p>
<p><font face="Courier New">$ go build -x -o testport_linux testport.go<br />
	WORK=/tmp/go-build286732099<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>我们发现Go实际上用的是$GOROOT/pkg/tool/linux_amd64下的6g（编译器）和6l（链接器）来完成整个编译过程的，看到6g 和6l所在目录名为linux_amd64，我们可以大胆猜测编译darwin/amd64 go程序应该使用的是$GOROOT/pkg/tool/darwin_amd64下的工具。不过在我在$GOROOT/pkg/tool下没有发现 darwin_amd64目录，也就是说我们通过安装包安装的Go仅自带了for linux_amd64的编译工具，要想交叉编译出for darwin_amd64的程序，我们需要通过make.bash来手工编译出这些工具。</p>
<p><font face="Courier New">tonybai@ubuntu-Server-14:/usr/local/go/pkg$ ls<br />
	linux_amd64&nbsp; linux_amd64_race&nbsp; obj&nbsp; tool</font></p>
<p><font face="Courier New">tonybai@ubuntu-Server-14:/usr/local/go/pkg/tool$ ls<br />
	linux_amd64</font></p>
<p>根据前面make.bash的用法说明，我们来尝试构建一下：</p>
<p><font face="Courier New">cd $GOROOT/src<br />
	sudo GOOS=darwin GOARCH=amd64 ./make.bash</font></p>
<p><font face="Courier New"># Building C bootstrap tool.<br />
	cmd/dist</font></p>
<p><font face="Courier New"># Building compilers and Go bootstrap tool for host, linux/amd64.<br />
	&#8230; &#8230;<br />
	cmd/cc<br />
	cmd/gc<br />
	cmd/6l<br />
	cmd/6a<br />
	cmd/6c<br />
	cmd/6g<br />
	pkg/runtime<br />
	&#8230; &#8230;<br />
	cmd/go<br />
	pkg/runtime (darwin/amd64)</font></p>
<p><font face="Courier New"># Building packages and commands for host, linux/amd64.<br />
	runtime<br />
	&#8230; &#8230;<br />
	text/scanner</font></p>
<p><font face="Courier New"># Building packages and commands for darwin/amd64.<br />
	runtime<br />
	errors<br />
	&#8230; &#8230;<br />
	testing/quick<br />
	text/scanner</font></p>
<p><font face="Courier New">&#8212;<br />
	Installed Go for darwin/amd64 in /usr/local/go<br />
	Installed commands in /usr/local/go/bin</font></p>
<p>编译后，我们再来试试编译for darwin_amd64的程序：</p>
<p><font face="Courier New">$ GOOS=darwin GOARCH=amd64 go build -x -o testport_darwin testport.go<br />
	WORK=/tmp/go-build972764136<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_darwin -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>将文件copy到我的Mac Air下执行：</p>
<p><font face="Courier New">$chmod +x testport_darwin<br />
	$testport_darwin<br />
	I am running on Darwin TonydeMacBook-Air.local 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64</font></p>
<p>编译虽然成功了，但从-x输出的详细编译过程来看，Go编译连接使用的工具依旧是linux_amd64下的6g和6l，为什么没有使用darwin_amd64下的6g和6l呢？原来$GOROOT/pkg/tool/darwin_amd64下根本就没有6g和6l：</p>
<p><font face="Courier New">/usr/local/go/pkg/tool/darwin_amd64$ ls<br />
	addr2line&nbsp; cgo&nbsp; fix&nbsp; nm&nbsp; objdump&nbsp; pack&nbsp; yac</font>c</p>
<p>但查看一下<font face="Courier New">pkg/tool/linux_amd64/</font>下程序的更新时间：</p>
<p>/usr/local/go/pkg/tool/linux_amd64$ ls -l<br />
	<font face="Courier New">&#8230; &#8230;<br />
	-rwxr-xr-x 1 root root 2482877 10月 20 15:12 6g<br />
	-rwxr-xr-x 1 root root 1186445 10月 20 15:12 6l<br />
	&#8230; &#8230;</font></p>
<p>我们发现6g和6l都是被刚才的make.bash新编译出来的，我们可以得出结论：新6g和新6l目前既可以编译本地程序（linux/amd64)，也可以编译darwin/amd64下的程序了，例如重新编译testport_linux依旧ok：</p>
<p><font face="Courier New">$ go build -x -o testport_linux testport.go<br />
	WORK=/tmp/go-build636762567<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>如果我们还想给Go编译器加上交叉编译windows/amd64程序的功能，我们再执行一次make.bash：</p>
<p><font face="Courier New">sudo GOOS=windows GOARCH=amd64 ./make.bash</font></p>
<p>编译成功后，我们来编译一下Windows程序：</p>
<p><font face="Courier New">$ GOOS=windows GOARCH=amd64 go build -x -o testport_windows.exe testport.go<br />
	WORK=/tmp/go-build626615350<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_windows.exe -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>把testport_windows.exe扔到Windows上执行，结果：</p>
<p><font face="Courier New">Err when executing uname command</font></p>
<p>显然Windows下没有uname命令，提示执行出错。</p>
<p>至此，我的Go编译器具备了在Linux下编译windows/amd64和darwin/amd64的能力。如果你还想增加其他平台的能力，就像上面那样操作执行make.bash即可。</p>
<p>如果在go源文件中有与C语言的交互代码，那么交叉编译功能是否还能奏效呢？毕竟C在各个平台上的运行库、链接库等都是不同的。我们先来看看这个例子，我们使用之前在《<a href="http://tonybai.com/2014/10/12/discussion-on-shared-mem-support-in-docker/">探讨docker容器对共享内存的支持情况</a>》一文中的一个例子：</p>
<p><font face="Courier New">//testport_cgoenabled.go<br />
	package main</font></p>
<p><font face="Courier New">//#include &lt;stdio.h&gt;<br />
	//#include &lt;sys/types.h&gt;<br />
	//#include &lt;sys/mman.h&gt;<br />
	//#include &lt;fcntl.h&gt;<br />
	//<br />
	//#define SHMSZ&nbsp;&nbsp;&nbsp;&nbsp; 27<br />
	//<br />
	//int shm_rd()<br />
	//{<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char c;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *shm = NULL;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *s = NULL;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int fd;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ((fd = open(&quot;./shm.txt&quot;, O_RDONLY)) == -1)&nbsp; {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shm = (char*)mmap(shm, SHMSZ, PROT_READ, MAP_SHARED, fd, 0);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (!shm) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -2;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(fd);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; s = shm;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int i = 0;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (i = 0; i &lt; SHMSZ &#8211; 1; i++) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%c &quot;, *(s + i));<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;\n&quot;);<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 0;<br />
	//}<br />
	import &quot;C&quot;</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := C.shm_rd()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Mmap Share Memory Read Error:&quot;, i)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Mmap Share Memory Read Ok&quot;)<br />
	}</font></p>
<p>我们先编译出一个本地可运行的程序：</p>
<p><font face="Courier New">$ go build -x -o testport_cgoenabled_linux testport_cgoenabled.go<br />
	WORK=/tmp/go-build977176241<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	CGO_LDFLAGS=&quot;-g&quot; &quot;-O2&quot; /usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ &#8212; -I $WORK/command-line-arguments/_obj/ testport_cgoenabled.go<br />
	/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_defun.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_defun.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -print-libgcc-file-name<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_main.o -c $WORK/command-line-arguments/_obj/_cgo_main.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_export.o -c $WORK/command-line-arguments/_obj/_cgo_export.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -c $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_cgo_.o $WORK/command-line-arguments/_obj/_cgo_main.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2<br />
	/usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ -dynimport $WORK/command-line-arguments/_obj/_cgo_.o -dynout $WORK/command-line-arguments/_obj/_cgo_import.c<br />
	/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_import.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_import.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_all.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2 -Wl,-r -nostdlib /usr/lib/gcc/x86_64-linux-gnu/4.8/libgcc.a<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -D _/home/tonybai/Test/Go/porting -I $WORK -pack $WORK/command-line-arguments/_obj/_cgo_gotypes.go $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo1.go<br />
	pack r $WORK/command-line-arguments.a $WORK/command-line-arguments/_obj/_cgo_import.6 $WORK/command-line-arguments/_obj/_cgo_defun.6 $WORK/command-line-arguments/_obj/_all.o # internal<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_cgoenabled_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>输出了好多日志！不过可以看出Go编译器先调用CGO对Go源码中的C代码进行了编译，然后才是常规的Go编译，最后通过6l链接在一起。Cgo似乎直接使用了Gcc。我们再来试试跨平台编译：</p>
<p><font face="Courier New">$ GOOS=darwin GOARCH=amd64 go build -x -o testport_cgoenabled_darwin testport_cgoenabled.go<br />
	WORK=/tmp/go-build124869433<br />
	can&#39;t load package: no buildable Go source files in /home/tonybai/Test/Go/porting</font></p>
<p>当我们编译for Darwin/amd64平台的程序时，Go无法像之前那样的顺利完成编译，而是提示错误。从网上给出的资料来看，如果Go源码中包含C互操作代码，那么 目前依旧无法实现交叉编译，因为cgo会直接使用各个平台的本地c编译器去编译Go文件中的C代码。默认情况下，make.bash会置 CGO_ENABLED=0。</p>
<p>如果你非要将CGO_ENABLED设置为1去编译go的话，至少我得到了如下错误，导致无法编译通过：</p>
<p><font face="Courier New">$ sudo CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./make.bash &#8211;no-clean<br />
	&#8230; &#8230;<br />
	# Building packages and commands for darwin/amd64.<br />
	&#8230; &#8230;<br />
	37: error: &#39;AI_MASK&#39; undeclared (first use in this function)</font><br />
	&nbsp;</p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/10/20/cross-compilation-with-golang/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>一个Solaris x86平台64位编译的问题</title>
		<link>https://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/</link>
		<comments>https://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/#comments</comments>
		<pubDate>Thu, 05 Nov 2009 05:40:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[编译]]></category>

		<guid isPermaLink="false">http://tonybai.com/2009/11/05/%e4%b8%80%e4%b8%aasolaris-x86%e5%b9%b3%e5%8f%b064%e4%bd%8d%e7%bc%96%e8%af%91%e7%9a%84%e9%97%ae%e9%a2%98/</guid>
		<description><![CDATA[<p>上午在做一个Solaris 10 on x86代码移植测试过程中，发现一个Gcc编译问题，这里记录下来以作备忘。<br />
<br />
我们的代码在一台安装了Solaris 10 for x86平台的机器A上进行64位编译(gcc -m64)时报错，错误信息如下：<br />
"xx.c:1: sorry, unimplemented: 64-bit mode not compiled...</p>]]></description>
			<content:encoded><![CDATA[<div>上午在做一个Solaris 10 on x86代码移植测试过程中，发现一个Gcc编译问题，这里记录下来以作备忘。</div>
<div>&nbsp;</div>
<div>我们的代码在一台安装了Solaris 10 for x86平台的机器A上进行64位编译(gcc -m64)时报错，错误信息如下：</div>
<div>&quot;xx.c:1: sorry, unimplemented: 64-bit mode not compiled in&quot;。</div>
<div>&nbsp;</div>
<div>而奇怪的是在另外一台同为Solaris 10 for x86的机器B（与上面的机器A硬件配置相同）上则顺利编译通过。最初猜测可能是因为系统设置或环境变量设置不同导致的问题，经过对比检查后发现以上设置都一致，最后将问题定位在Gcc编译器版本上了。</div>
<div>&nbsp;</div>
<div>机器A上使用的是Gcc 3.4.6 for Solaris 10 on x86版本；而可以通过编译的那台机器B上使用的是Gcc 3.4.3 (csl-sol210-3_4-branch+sol_rpath) for Solaris 10 x86版本。尝试在机器A上使用Gcc 3.4.3进行编译，错误未再出现，看来的确是Gcc编译器版本问题。</div>
<div>&nbsp;</div>
<div>遂到Sunfreeware网站上一查究竟。在Gcc 3.4.6 for Solaris 10 on x86的软件说明中，有这样一段话：</div>
<div>&nbsp;</div>
<div>&ldquo;If you need to do 64-bit compiles, you should use the gcc-3.4.3 that comes with Solaris 10 in /usr/sfw/bin.&rdquo;</div>
<div>&nbsp;</div>
<div>而Gcc 3.4.6 for Solaris 10 on sparc的版本说明中，则明确表示：&ldquo;When needed and the source code supports it, this C compiler can create 64-bit executables via the -m64 flag as well as the usual 32-bit ones.&rdquo;</div>
<div>&nbsp;</div>
<div>注：以上提到的Solaris软件均来自于<a href="http://sunfreeware.com" target="_blank">Sunfreeware</a>站点。</div>
<p style='text-align:left'>&copy; 2009, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>分布式编译让你的工作更高效</title>
		<link>https://tonybai.com/2008/10/14/distributed-compiling-make-you-work-more-effectivly/</link>
		<comments>https://tonybai.com/2008/10/14/distributed-compiling-make-you-work-more-effectivly/#comments</comments>
		<pubDate>Tue, 14 Oct 2008 10:36:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Distcc]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[分布式]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[编译]]></category>

		<guid isPermaLink="false">http://tonybai.com/2008/10/14/%e5%88%86%e5%b8%83%e5%bc%8f%e7%bc%96%e8%af%91%e8%ae%a9%e4%bd%a0%e7%9a%84%e5%b7%a5%e4%bd%9c%e6%9b%b4%e9%ab%98%e6%95%88/</guid>
		<description><![CDATA[随着工程代码量的增加，往往完整的编译一次Proj消耗的时间可能足够你喝两杯咖啡了，我现在build一次我所在proj的代码需要5分多钟，这是很痛苦的，颇让人懊恼的。为了解决这个工作中的别扭事儿，我在网上搜寻了一番，找到了distcc这个分布式编译工具。]]></description>
			<content:encoded><![CDATA[<p>随着工程代码量的增加，往往完整的编译一次Proj消耗的时间可能足够你喝两杯咖啡了，我现在build一次我所在proj的代码需要5分多钟，这是很痛苦的，颇让人懊恼的。为了解决这个工作中的别扭事儿，我在网上搜寻了一番，找到了<a href="http://distcc.samba.org/" target="_blank">distcc</a>这个分布式编译工具。</p>
<p>先看看distcc能帮助我节省多少时间吧。我在公司的一台Sun SPARC Solaris9主机下对整个项目源代码按照以前的编译方式进行了一次build，这次build用了5分多钟；同样我使用distcc编译(安装了两个节点，都是Sun SPARC Solaris9主机)，居然只用了1分多钟，试想如何有更多的服务器作为distcc的守候进程主机节点的话，势必编译性能还会有提升。</p>
<p>有了&quot;惊人&quot;结果后，我们来看看distcc的原理，distcc本身是gcc的一个wrapper，也可用作本地编译，但是更多的是其分布式编译的强大功能，简单来说：就是将<a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/" target="_blank">gcc</a>的编译任务分布到各个其他主机上去，然后再传回来整合。它提高的是gcc -c阶段的速度，链接阶段的速度由于肯定要在本地实施，所以distcc无能为力。另外distcc推荐分布的不同主机上安装的编译器版本最好要一致，否则可能会有意想不到的错误。</p>
<p>distcc的安装和使用方法甚是简单，我安装的是<a href="ftp://ftp.sunfreeware.com/pub/freeware/sparc/9/distcc-2.13-sol9-sparc-local.gz" target="_blank">distcc-2.13-sol9-sparc-local</a>，直接在root下pkgadd即可。然后在各个distcc节点启动后台守候进程：distccd &#8211;daemon &#8211;allow x.x.x.x/16，以普通用户启动即可。</p>
<p>客户端使用方法：<br />
	1、在自己的用户下，添加环境变量(如果你用的是C shell)：setenv DISTCC_HOSTS &#039;localhost x.x.x.x&#039;，代表本机和x.x.x.x上安装并启动了distccd<br />
	2、将你的makefile 中的CC=gcc改为CC=distcc gcc<br />
	3、make即可 。同样你还可以在make的-j参数选项，如make -j 12，这样在单机上进行多任务并行编译，使速度更快。<br />
	4、如果你想观察各节点上distcc的工作状态，可使用distccmon-text 2 命令查看distcc在各台主机上的任务快照。参数2代表：每隔2秒刷新一次。</p>
<p>Distcc理论上是可以部署在不同平台上辅助进行分布式编译的，但是在异构平台上分布需要一段时间设置和<a href="http://tonybai.com/2006/01/08/debug-multiple-process-program-using-gdb/" target="_blank">调试</a>，我推荐还是尽量部署在同一类型平台上吧。有了distcc，我们的服务器的计算能力得到了充分的发挥，个人工作效率也会有所提高的，不知道长此下去喝咖啡的机会会不会被剥夺了:)</p>
<p style='text-align:left'>&copy; 2008, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2008/10/14/distributed-compiling-make-you-work-more-effectivly/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
