<?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; Makefile</title>
	<atom:link href="http://tonybai.com/tag/makefile/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sat, 11 Apr 2026 00:16:11 +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>2026 年了，写 Go + Protobuf 还在手敲 protoc 命令？是时候换用这种新姿势了！</title>
		<link>https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026/</link>
		<comments>https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026/#comments</comments>
		<pubDate>Wed, 04 Mar 2026 23:43:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[APIContract]]></category>
		<category><![CDATA[BackwardCompatibility]]></category>
		<category><![CDATA[BreakingChangeDetection]]></category>
		<category><![CDATA[BSR]]></category>
		<category><![CDATA[buf]]></category>
		<category><![CDATA[BufSchemaRegistry]]></category>
		<category><![CDATA[CLITools]]></category>
		<category><![CDATA[CLI工具]]></category>
		<category><![CDATA[CodeGeneration]]></category>
		<category><![CDATA[DependencyManagement]]></category>
		<category><![CDATA[Engineering]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoLanguage]]></category>
		<category><![CDATA[Go语言]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[IDL]]></category>
		<category><![CDATA[Linting]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Microservices]]></category>
		<category><![CDATA[protobuf]]></category>
		<category><![CDATA[protoc]]></category>
		<category><![CDATA[RemoteModules]]></category>
		<category><![CDATA[RemotePlugins]]></category>
		<category><![CDATA[YAMLConfiguration]]></category>
		<category><![CDATA[YAML配置]]></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=5983</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026 大家好，我是Tony Bai。 在现代后端开发领域，Go 语言与 Protocol Buffers（简称 Protobuf）加上 gRPC 的组合，早已成为构建高性能微服务架构的“行业标准”。这两者的结合在网络传输效率、强类型契约以及跨语言互操作性上展现出了无与伦比的优势。 然而，令人感到魔幻的是，随着 Go 语言本身的生态在过去几年里飞速进化（从 GOPATH 到 Go Modules，从混乱的依赖管理到极其统一且优雅的标准工具链），处理 Protobuf 文件的代码生成环节，却长期停留在一种“上古时代”的原始状态。 就在最近，技术社区 Reddit 的 r/golang 板块上出现了一则引发大家共鸣的帖子。一位开发者提出下面拷问： “I was wondering what is the preferred way to do golang + protobuf in 2026. Do I still have to download protoc or are there any natives I can [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/modern-go-protobuf-dev-in-2026-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026">本文永久链接</a> &#8211; https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026</p>
<p>大家好，我是Tony Bai。</p>
<p>在现代后端开发领域，Go 语言与 Protocol Buffers（简称 <a href="https://tonybai.com/2020/04/24/gogoprotobuf-vs-goprotobuf-v1-and-v2">Protobuf</a>）加上 <a href="https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server">gRPC</a> 的组合，早已成为构建高性能微服务架构的“行业标准”。这两者的结合在网络传输效率、强类型契约以及跨语言互操作性上展现出了无与伦比的优势。</p>
<p>然而，令人感到魔幻的是，随着 Go 语言本身的生态在过去几年里飞速进化（从 GOPATH 到 Go Modules，从混乱的依赖管理到极其统一且优雅的标准工具链），处理 Protobuf 文件的代码生成环节，却长期停留在一种“上古时代”的原始状态。</p>
<p>就在最近，技术社区 Reddit 的 r/golang 板块上出现了<a href="https://www.reddit.com/r/golang/comments/1rapxyq/golang_protobuf_in_2026/">一则引发大家共鸣的帖子</a>。一位开发者提出下面拷问：</p>
<blockquote>
<p><em>“I was wondering what is the preferred way to do golang + protobuf in 2026. Do I still have to download protoc or are there any natives I can use with the golang compiler.”</em><br />
  （我想知道 2026 年 Go + protobuf 的首选开发方式是什么？我是否仍然必须下载 protoc，或者 Go 编译器有没有内置原生的支持？）</p>
</blockquote>
<p>这不仅是这位开发者的困惑，更是无数长期忍受繁琐工具链的 Gopher 们的心声。在跟帖回复中，社区开发者们给出了一个相对主流的的答案：<strong>Go 编译器本身并没有，也不打算内置解析 .proto 的功能，但是，所有严肃的现代工程团队都开始在用 Buf (buf.build) 替代原生 protoc 工具链了。</strong></p>
<p>本文将深入剖析 2026 年的现代 Protobuf 工程化实践。我将带你领略为什么 buf CLI 是当之无愧的现代化首选，以及它是如何彻底终结“手敲 protoc 命令”这一痛苦历史的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/agentic-software-engineering-qr.png" alt="" /></p>
<h2>核心痛点：为什么原生 protoc 令人抓狂？</h2>
<p>在请出主角 Buf 之前，我们需要先深刻理解，传统的 protoc 工作流到底哪里出了问题，以至于整个社区都在寻求替代方案。</p>
<p>如果你在过去几年使用过原生的方式在 Go 中生成 Protobuf 代码，你的项目里极大概率会存在一个类似于下面这样“臭名昭著”的 Makefile 或 build.sh 脚本：</p>
<pre><code class="makefile"># 传统项目中常见的“野生” Makefile 节选
.PHONY: generate-proto

PROTO_FILES=$(shell find api -name "*.proto")

generate-proto:
    @echo "Generating Go code from Protobuf..."
    protoc \
        -I api \
        -I /usr/local/include \
        -I $(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \
        --go_out=gen/go \
        --go_opt=paths=source_relative \
        --go-grpc_out=gen/go \
        --go-grpc_opt=paths=source_relative \
        $(PROTO_FILES)
</code></pre>
<p>这段看似能跑的脚本背后，隐藏着令开发者抓狂的三大“原罪”：</p>
<ol>
<li>环境依赖的地狱</li>
</ol>
<p>要成功运行上述命令，你的机器（以及所有协作者的机器、甚至是 CI/CD 流水线的容器）上必须预先安装 C++ 编写的 protoc 编译器核心二进制文件。此外，你还需要通过 go install 将正确版本的 protoc-gen-go 和 protoc-gen-go-grpc 插件安装到系统的 $PATH 目录下。任何一个人机器上的版本不一致，都会导致生成的 Go 代码带有微小的差异，最终在 Git 提交中引发无意义的代码冲突。</p>
<ol>
<li>路径导入的迷宫 (-I 噩梦)</li>
</ol>
<p>protoc 是基于文件系统的。如果你的 .proto 文件中引入了第三方的定义（例如 import “google/api/annotations.proto”; 以支持 HTTP 网关），你必须在机器上找到这些第三方文件的物理存放路径，并通过极度冗长且极易出错的 -I（&#8211;proto_path）参数将它们一个个拼接起来。</p>
<ol>
<li>缺乏规范约束与破坏性变更保护</li>
</ol>
<p>protoc 仅仅是一个编译器，它完全不在乎你的字段命名是否符合团队规范（例如把字段命名为 camelCase 而不是官方推荐的 snake_case）。更致命的是，当你随意删改已经在线上运行的字段类型时，protoc 会毫无波澜地为你生成新的代码，直到代码发布导致客户端反序列化崩溃，你才会发现酿成了大祸。</p>
<p>开发者的精力应该集中在业务逻辑的设计上，而不是每天在终端里调试 protoc 的环境变量和路径参数。这就是 Buf CLI 诞生的核心驱动力。</p>
<h2>Buf CLI 闪亮登场：声明式的现代 Protobuf 工具链</h2>
<p>Buf（由 buf.build 公司开发）并不是另一个像 protoc-gen-go 一样的单点插件，而是一套完全由 Go 语言编写、开箱即用、向下兼容 Protobuf 语法的全链路现代编译器套件。</p>
<p>它的核心设计哲学非常清晰：</p>
<ul>
<li>声明式配置：用简洁的 YAML 文件取代面条式的 Shell 命令。</li>
<li>一致性保障：无论在本地开发机还是远程 CI 环境，保证 100% 的生成结果一致。</li>
<li>工程化内置：将代码规范检查（Linting）和向后兼容性检测（Breaking Change Detection）作为一等公民内置于 CLI 中。</li>
</ul>
<p>为了真正理解它的强大，接下来我们将基于一个干净的 Linux (Ubuntu/Debian 或类似发行版) 环境，从零开始构建一个微服务的 API 契约层，带你体验这套全新的开发范式。</p>
<h2>零基础环境搭建与项目初始化</h2>
<h3>步骤 1：安装 Go 与 Buf CLI</h3>
<p>首先，确保你的 Linux 环境中已经安装了 Go 语言（建议使用 Go 1.22 或更高版本）。</p>
<p>由于 <a href="https://github.com/bufbuild/buf">Buf CLI</a> 自身就是用 Go 编写的，因此在 Linux 下安装它最简单、最不易出错的方式就是直接下载预编译好的单体二进制文件，或者通过 go install。为了全局可用且版本可控，我们使用官方推荐的下载脚本：</p>
<pre><code class="bash"># 下载适用于 Linux x86_64 架构的 buf CLI v1.66.0 (请根据实际情况调整版本号)
# 以及protoc-gen-buf-breaking、protoc-gen-buf-lint工具

# Substitute PREFIX for your install prefix.
# Substitute VERSION for the current released version.
PREFIX="/usr/local" &amp;&amp; \
VERSION="1.66.0" &amp;&amp; \
curl -sSL \
"https://github.com/bufbuild/buf/releases/download/v${VERSION}/buf-$(uname -s)-$(uname -m).tar.gz" | \
tar -xvzf - -C "${PREFIX}" --strip-components 1

# 验证安装成功
$ buf --version
1.66.0
</code></pre>
<p><em>极其清爽的体验：仅仅这一个只有几十 MB 的二进制文件，就涵盖了后续我们需要的所有核心功能，你完全不需要再去单独使用 apt-get install protobuf-compiler 安装传统的 protoc！</em></p>
<h3>步骤 2：创建项目结构与编写 Protobuf IDL</h3>
<p>我们在当前用户的主目录下创建一个名为 acme-shop 的微服务项目，并初始化 Go Module：</p>
<pre><code class="bash">$ mkdir -p acme-shop &amp;&amp; cd acme-shop
$ go mod init github.com/acme/shop
</code></pre>
<p>接着，按照现代工程的最佳实践，我们将 Protobuf 文件与具体的 Go 业务代码隔离开来。我们创建一个 proto 目录专门存放接口定义（IDL）：</p>
<pre><code class="bash"># 创建目录层级
$ mkdir -p proto/acme/order/v1
</code></pre>
<p>使用你喜欢的编辑器（如 vim, nano 或 VSCode），在 proto/acme/order/v1/order.proto 中写入以下内容：</p>
<pre><code class="protobuf">// proto/acme/order/v1/order.proto
syntax = "proto3";

package acme.order.v1;

// go_package 是必须的，它告诉工具生成的 Go 代码最终属于哪个 import path
option go_package = "github.com/acme/shop/gen/go/acme/order/v1;orderv1";

import "google/protobuf/timestamp.proto";

// 订单服务接口定义
service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {}
}

message CreateOrderRequest {
  string customer_id = 1;
  double amount = 2;
}

message CreateOrderResponse {
  string order_id = 1;
  google.protobuf.Timestamp created_at = 2;
}
</code></pre>
<p>请注意，在这个文件中我们引入了标准的 google/protobuf/timestamp.proto。在传统方式下，你必须确保你的机器上存在这个标准库文件，而在接下来 Buf 的演示中，你会看到它是如何自动化处理这一切的。</p>
<h2>彻底告别命令行黑魔法：Buf 核心功能实战</h2>
<h3>步骤 3：初始化 Buf 模块 (The buf.yaml)</h3>
<p>传统的 protoc 需要你每次在命令行指定要编译哪些文件。Buf 引入了“工作区（Workspace）”和“模块（Module）”的概念。</p>
<p>在项目的 proto 目录下，我们通过 buf mod init 命令(最新版本的buf建议使用buf config init)来声明这是一个受 Buf 管理的 Protobuf 模块：</p>
<pre><code class="bash">$ cd proto
$ buf mod init
$ cd ..
</code></pre>
<p>这会在 proto/ 目录下生成一个非常简洁的 buf.yaml 文件，内容类似如下（基于当前默认的 v1 版本，若是更高版本可能是 v2）：</p>
<pre><code class="yaml"># For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE
</code></pre>
<p>这个看似简单的文件意义非凡。它告诉 Buf CLI：当前目录（proto）的根路径，就是所有 .proto 文件导入路径（Import Path）的起点。<strong>你从此再也不用在任何地方手写令人头疼的 -I /path/to/proto 参数了。</strong> 此外，它还激活了默认的代码规范规则（lint）和兼容性检测规则（breaking）。</p>
<h3>步骤 4：零配置的代码规范检查 (buf lint)</h3>
<p>在传统开发中，Protobuf 的风格往往是一笔糊涂账。现在，Buf 直接将静态代码分析带到了你的终端。</p>
<p>让我们故意在 order.proto 中犯一个小错。打开 proto/acme/order/v1/order.proto，将请求消息的字段名改成驼峰式命名：</p>
<pre><code class="protobuf">message CreateOrderRequest {
  // 故意违反 protobuf 推荐的 snake_case 命名规范
  string customerId = 1;
  double amount = 2;
}
</code></pre>
<p>回到终端，在项目根目录（acme-shop）下运行检查命令：</p>
<pre><code class="bash">$ buf lint proto
</code></pre>
<p>输出结果清晰得令人拍案叫绝：</p>
<pre><code class="text">proto/acme/order/v1/order.proto:18:10:Field name "customerId" should be lower_snake_case, such as "customer_id".
</code></pre>
<p>Buf 指出了具体的文件、行号、列号，甚至直接给出了修改建议。这使得将 Protobuf 规范集成到 Git Pre-commit Hook 或 CI/CD 流水线中变得易如反掌。将代码改回 customer_id 后，再次运行 buf lint proto，将没有任何输出，代表检查通过。</p>
<h3>步骤 5：声明式代码生成 (buf.gen.yaml)</h3>
<p>重头戏来了。我们要用一种极其优雅的方式，取代前面提到的长串 protoc 命令和冗长的 Makefile。</p>
<p>在项目根目录（acme-shop）下，新建一个文件名为 buf.gen.yaml 的生成配置文件：</p>
<pre><code class="yaml"># buf.gen.yaml
version: v1
plugins:
  # 插件 1：生成基础的 Go struct 代码
  - plugin: go
    out: gen/go
    opt: paths=source_relative
  # 插件 2：生成 gRPC 客户端/服务端接口代码
  - plugin: go-grpc
    out: gen/go
    opt: paths=source_relative
</code></pre>
<p>在这个配置文件中，我们声明了需要使用哪两个插件（go 和 go-grpc），生成的代码输出到哪里（out: gen/go），以及附加的选项（opt: paths=source_relative 确保生成的目录结构与 proto 文件结构保持一致）。</p>
<p><strong>【纯本地环境的准备工作】</strong></p>
<p>由于我们在配置中指定了具体的插件名称（go 和 go-grpc），当运行 Buf 时，它会在你的系统环境中寻找名为 protoc-gen-go 和 protoc-gen-go-grpc 的可执行文件。因此，仅仅是为了完成<strong>本地代码生成</strong>这一步，我们依然需要使用 Go 官方工具获取这两个插件：</p>
<pre><code class="bash">$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# 确保安装后的protoc-gen-go和protoc-gen-go-grpc在系统 $PATH 中
</code></pre>
<p><em>注意：虽然这里依然下载了本地插件，但这已经是你在本地唯一需要管理的外部依赖了。核心的编译器、路径解析、规范约束都已经被 Buf 接管。稍后我们会讲到如何通过 BSR 甚至连这一步都省略掉。</em></p>
<h3>步骤 6：一键执行，见证优雅 (buf generate)</h3>
<p>万事俱备，现在只需在项目根目录执行极其简单的一句命令：</p>
<pre><code class="bash">$ buf generate proto
</code></pre>
<p>就是如此朴实无华。没有任何屏幕乱码，没有任何报错。</p>
<p>我们可以查看目录结构，生成的代码已经按照包结构完美地放置在了预期位置：</p>
<pre><code class="bash">$ tree -F gen/go
gen/go/
└── acme/
    └── order/
        └── v1/
            ├── order.pb.go
            └── order_grpc.pb.go

</code></pre>
<p>这一句 buf generate 的执行是幂等且高度一致的。你可以放心地将 buf.gen.yaml 提交进版本控制库。任何新加入的同事，只要执行这一句命令，得到的永远是一模一样的结果。</p>
<h3>步骤 7：防范接口灾难的“保护伞” (buf breaking)</h3>
<p>企业级开发中，Protobuf 被用于构建微服务间强契约的 API。如果你随意删除了一个字段，或者修改了字段的类型（比如从 int32 改为 string），依赖于旧接口的客户端在解析新数据时将直接崩溃。</p>
<p>传统 protoc 对此无能为力，必须靠开发者人工审查。但 Buf CLI 提供了业界最强的 breaking change（破坏性变更）检测功能。</p>
<p>让我们模拟一次灾难。打开 proto/acme/order/v1/order.proto，我们将 amount 字段的标号从 2 改为 3（在 Protobuf 中，变更字段编号是非常严重的向后不兼容行为，会导致序列化错乱）：</p>
<pre><code class="protobuf">message CreateOrderRequest {
  string customer_id = 1;
  // 危险操作：修改了原有字段的标号
  double amount = 3;
}
</code></pre>
<p>为了检测出这个变更，我们需要将当前状态与过去的某个状态（例如我们上一次的稳定状态，或者 Git 的 main 分支）进行对比。由于我们的演示项目还没提交过 Git，Buf 提供了一个非常灵活的对比方法，可以直接对比文件系统的快照或者之前的目录。</p>
<p>假设我们在修改前，将原始正确的 proto 文件备份在了 proto_backup 目录中。我们可以这样运行检测：</p>
<pre><code class="bash">$ buf breaking proto --against proto_backup
</code></pre>
<p>Buf 会立刻阻止你，并在终端输出刺眼的错误提示：</p>
<pre><code class="text">$ buf breaking proto --against proto_backup
proto/acme/order/v1/order.proto:17:1:Previously present field "2" with name "amount" on message "CreateOrderRequest" was deleted.
</code></pre>
<p>它准确地指出你删除了编号为 2 的字段。如果在一个接入了 Git 仓库的真实项目中，你通常会运行：</p>
<pre><code class="bash"># 检测当前代码库中的 proto 相对 Git main 分支的最新提交是否发生向后兼容性破坏
$ buf breaking proto --against '.git#branch=main'
</code></pre>
<p>只需将这行简单的命令加入到你的 CI 流水线（如 GitHub Actions 或 GitLab CI）中，你的团队就彻底杜绝了因疏忽导致的 API 不兼容事故。</p>
<h2>深度解析：BSR (Buf Schema Registry) 究竟解决了什么问题？</h2>
<p>到目前为止，我们所有的演示都是在<strong>纯本地、完全离线</strong>的环境下进行的。</p>
<p>我们证明了：即便你完全不使用云端服务，仅仅是将原生的 protoc 替换为 buf CLI，依然能获得巨大无比的工程化收益（免配置导入路径、内置代码校验、极其简洁的生成配置、强大的向后兼容性保护）。</p>
<p>但是，如果你想了解 2026 年 Protobuf 生态演进的最前沿，就必须提到 Buf 公司推出的杀手级 SaaS 平台：<strong>Buf Schema Registry (BSR)</strong>。</p>
<p>BSR 可以被理解为 “Protobuf 界的 npm 或 Docker Hub”。如果没有 BSR，你的本地开发依然会面临两个难以根除的痛点：</p>
<h3>痛点一：第三方公共 API 文件的搬运工</h3>
<p>在纯本地模式下，如果你的业务需要使用 HTTP 网关网关（如 grpc-gateway），你的 order.proto 就必须写上 import “google/api/annotations.proto”;。</p>
<p><strong>没有 BSR 时，你需要手工管理：</strong> 你必须去 Google 的 GitHub 仓库里把 annotations.proto 及其级联依赖文件下载下来，在自己的项目里建一个 third_party/google/api/ 目录存放进去。这不仅污染了项目结构，还需要人工维护版本更新。</p>
<p><strong>BSR 解决之道：远程模块依赖 (Remote Modules)</strong></p>
<p>BSR 上托管了成千上万的知名开源 Protobuf 库。当你使用 BSR 时，你只需要在 proto/buf.yaml 中声明一句依赖：</p>
<pre><code class="yaml"># 开启 BSR 远程依赖后
version: v1
deps:
  # 直接声明依赖 Google API 的云端模块
  - buf.build/googleapis/googleapis
</code></pre>
<p>然后在终端运行一句 buf mod update，Buf CLI 就会像 go mod 拉取 Go 源码一样，自动将所需的 .proto 文件从云端缓存到你的本地（开发者甚至感知不到）。你的代码库瞬间变得干净纯粹，只需关注自身的业务 IDL。</p>
<h3>痛点二：本地生成插件的管理成本</h3>
<p>在上文的步骤 5 中，我们依然需要使用 go install 安装 protoc-gen-go 等二进制文件。如果团队有人使用的是 Windows，有人用 macOS，维护本地插件栈依然存在轻微的不便。</p>
<p><strong>BSR 解决之道：远程执行引擎与云端插件 (Remote Plugins)</strong></p>
<p>这是颠覆式的一项创新。如果你愿意借助 BSR 的云端基础设施，你可以<strong>彻底删除本地所有的 protoc-gen-xxx 二进制文件</strong>。</p>
<p>我们只需将 buf.gen.yaml 改造为指向云端的插件：</p>
<pre><code class="yaml"># 依托 BSR 远程插件生态的 buf.gen.yaml
version: v1
plugins:
  # 注意 plugin 前缀变成了云端地址
  - plugin: buf.build/protocolbuffers/go:v1.36.11
    out: gen/go
    opt: paths=source_relative
  - plugin: buf.build/grpc/go:v1.6.1
    out: gen/go
    opt: paths=source_relative
</code></pre>
<p>在这个配置下，当你运行 buf generate proto 时(为了见证奇迹，你可以将你本地安装的protoc-gen-go和protoc-gen-go-grpc都删除掉)，发生的事情堪称魔法：</p>
<ol>
<li>Buf CLI 将你的 .proto 文件作为有效负载（Payload）发送到 BSR 的云端编译集群。</li>
<li>BSR 服务器调用官方认证的插件环境为你生成对应的 Go 代码。</li>
<li>编译好的 .pb.go 文件通过网络流瞬间返回并精准投放到你本地的 gen/go 目录下。</li>
</ol>
<p>这不仅统一了所有成员的编译器环境版本，更将开发者的本地负担降到了绝对零度：<strong>只需安装一个 buf 二进制，就能编译世间万物。</strong> （当然，如果你的网络环境受限，依然可以随时回退到上文介绍的本地插件模式配置。）</p>
<h2>小结与展望</h2>
<p>在当前的 Go 开发生态中，“不要重复发明轮子，而应拥抱标准工具链”是大家共同的准则。过去几年，处理 Protobuf 犹如陷入一片充满陷阱的沼泽，开发者们花费了大量心智与那些毫无价值的 CLI 参数作斗争。</p>
<p>随着时间来到 2026 年，我们欣喜地看到，整个社区对于构建现代化 API 契约的认知已经彻底觉醒。通过本文详实的演练，我们可以得出一个极度确定的结论：</p>
<ol>
<li>停用手写的 protoc Shell 脚本：它在代码重用性、跨平台一致性和防范人为灾难方面毫无招架之力。</li>
<li>全面拥抱 Buf CLI：将 buf mod init、buf lint、buf breaking 纳入每一个微服务项目的初始化模板。它是现代 Protobuf 工程化当之无愧的选择，即使完全脱离 BSR 服务作为本地工具使用，其体验也是颠覆性的。</li>
<li>了解 BSR 的架构演进思路：依赖的包袱就该交给包管理器（如远程模块管理）去解决，这代表了系统级应用开发的未来趋势。</li>
</ol>
<p>还在维护祖传的 Makefile 吗？赶紧删掉那些脚本吧，在新项目里安装 buf，开启你的现代protobuf代码生成之旅吧！你的开发体验，值得这样的升级。</p>
<p>本文涉及的代码在<a href="https://github.com/bigwhite/experiments/tree/master/buf-examples">这里</a>可以下载。</p>
<p>资料链接：</p>
<ul>
<li>https://www.reddit.com/r/golang/comments/1rapxyq/golang_protobuf_in_2026/</li>
<li>https://buf.build/docs/cli/</li>
</ul>
<hr />
<p><strong>你的 protoc 脚本有多少行？</strong></p>
<p>传统的 protoc 确实让人爱恨交织。在你的项目中，为了维护一套跨平台的 Protobuf 生成环境，你踩过哪些最离谱的“坑”？你认为 Buf 这种云端插件模式（BSR）会在国内企业环境下大规模落地吗？</p>
<p>欢迎在评论区分享你的看法或吐槽！</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p><strong>想系统学习Go，构建扎实的知识体系？</strong></p>
<p>我的新书《<a href="https://book.douban.com/subject/37499496/">Go语言第一课</a>》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-4.png" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 服务自省指南：抛弃 ldflags，让你的二进制文件“开口说话”</title>
		<link>https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo/</link>
		<comments>https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo/#comments</comments>
		<pubDate>Wed, 31 Dec 2025 04:23:09 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BuildInfo]]></category>
		<category><![CDATA[BuildSettings]]></category>
		<category><![CDATA[BuildTime]]></category>
		<category><![CDATA[buildvcs]]></category>
		<category><![CDATA[CommitHash]]></category>
		<category><![CDATA[debugbuildinfo]]></category>
		<category><![CDATA[deps]]></category>
		<category><![CDATA[DirtyBuild]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoModules]]></category>
		<category><![CDATA[ldflags]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[ReadBuildInfo]]></category>
		<category><![CDATA[RuntimeDebug]]></category>
		<category><![CDATA[trimpath]]></category>
		<category><![CDATA[vcs]]></category>
		<category><![CDATA[VCSStamping]]></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=5636</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo 大家好，我是Tony Bai。 在微服务和云原生时代，当我们面对线上服务的报警时，第一个问题往往不是“哪里出错了？”，而是——“现在线上跑的到底是哪个版本？” 在 Go 的蛮荒时代，我们习惯在 Makefile 里写上一长串 -ldflags “-X main.version=$(git describe &#8230;) -X main.commit=$(git rev-parse &#8230;)”。这种方法虽然有效，但繁琐、易忘，且容易因为构建脚本的差异导致信息缺失。 其实，Go 语言早就为我们准备好了一套强大的“自省”机制。通过标准库 runtime/debug，二进制文件可以清晰地告诉我们它是由哪个 Commit 构建的、何时构建的、甚至它依赖了哪些库的哪个版本。 今天，我们就来深入挖掘 debug.BuildInfo，打造一个具有“自我意识”的 Go 服务。 重新认识 debug.BuildInfo Go 编译器在构建二进制文件时，会将构建时的元数据（Module Path、Go Version、Dependencies、Build Settings）写入到二进制文件的特定区域。在运行时，我们可以通过 runtime/debug.ReadBuildInfo() 读取这些信息。 让我们看一个最基础的例子： // buildinfo-examples/demo1/main.go package main import ( "fmt" "runtime/debug" ) func main() { info, ok := debug.ReadBuildInfo() [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-introspection-using-debug-buildinfo-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo">本文永久链接</a> &#8211; https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo</p>
<p>大家好，我是Tony Bai。</p>
<p>在微服务和云原生时代，当我们面对线上服务的报警时，第一个问题往往不是“哪里出错了？”，而是——<strong>“现在线上跑的到底是哪个版本？”</strong></p>
<p>在 Go 的蛮荒时代，我们习惯在 Makefile 里写上一长串 -ldflags “-X main.version=$(git describe &#8230;) -X main.commit=$(git rev-parse &#8230;)”。这种方法虽然有效，但繁琐、易忘，且容易因为构建脚本的差异导致信息缺失。</p>
<p>其实，Go 语言早就为我们准备好了一套强大的<strong>“自省”</strong>机制。通过标准库 runtime/debug，二进制文件可以清晰地告诉我们它是由哪个 Commit 构建的、何时构建的、甚至它依赖了哪些库的哪个版本。</p>
<p>今天，我们就来深入挖掘 debug.BuildInfo，打造一个具有“自我意识”的 Go 服务。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/the-ultimate-guide-to-go-module-qr.png" alt="" /></p>
<h2>重新认识 debug.BuildInfo</h2>
<p>Go 编译器在构建二进制文件时，会将构建时的元数据（Module Path、Go Version、Dependencies、Build Settings）写入到二进制文件的特定区域。在运行时，我们可以通过 runtime/debug.ReadBuildInfo() 读取这些信息。</p>
<p>让我们看一个最基础的例子：</p>
<pre><code class="go">// buildinfo-examples/demo1/main.go
package main

import (
    "fmt"
    "runtime/debug"
)

func main() {
    info, ok := debug.ReadBuildInfo()
    if !ok {
        fmt.Println("未获取到构建信息，请确保使用 Go Modules 构建")
        return
    }
    fmt.Printf("主模块: %s\n", info.Main.Path)
    fmt.Printf("Go版本: %s\n", info.GoVersion)
}
</code></pre>
<p>当你使用 go build 编译并运行上述代码时，你会发现它能准确输出模块名和 Go 版本。但这只是冰山一角。</p>
<pre><code>$go build
$./demo1
主模块: demo1
Go版本: go1.25.3
</code></pre>
<h2>告别 ldflags：VCS Stamping (版本控制盖章)</h2>
<p>从 Go 1.18 开始，Go 工具链引入了一项杀手级特性：<strong>VCS Stamping</strong>。默认情况下，go build 会自动检测当前的 Git（或 SVN 等）仓库状态，并将关键信息嵌入到 BuildInfo.Settings 中。</p>
<p>这意味着，你<strong>不再需要</strong>手动提取 Git Hash 并注入了。</p>
<p>我们可以编写一个辅助函数来提取这些信息：</p>
<pre><code class="go">// buildinfo-examples/demo2/main.go

package main

import (
    "fmt"
    "runtime/debug"
)

func printVCSInfo() {
    info, _ := debug.ReadBuildInfo()
    var revision string
    var time string
    var modified bool

    for _, setting := range info.Settings {
        switch setting.Key {
        case "vcs.revision":
            revision = setting.Value
        case "vcs.time":
            time = setting.Value
        case "vcs.modified":
            modified = (setting.Value == "true")
        }
    }

    fmt.Printf("Git Commit: %s\n", revision)
    fmt.Printf("Build Time: %s\n", time)
    fmt.Printf("Dirty Build: %v\n", modified) // 这一点至关重要！
}

func main() {
    printVCSInfo()
}
</code></pre>
<p>编译并运行示例：</p>
<pre><code>$go build
$./demo2
Git Commit: aa3539a9c4da76d89d25573917b2b37bb43f8a2a
Build Time: 2025-12-22T04:24:05Z
Dirty Build: true
</code></pre>
<p><strong>这里的 vcs.modified 非常关键</strong>。如果为 true，说明构建时的代码包含未提交的更改。对于线上生产环境，我们应当严厉禁止 Dirty Build，因为这意味着不仅代码不可追溯，甚至可能包含临时的调试逻辑。</p>
<blockquote>
<p><strong>注意</strong>：如果使用 -buildvcs=false 标志或者在非 Git 目录下构建，这些信息将不会存在。</p>
</blockquote>
<h2>依赖审计：你的服务里藏着什么？</h2>
<p>除了自身的版本，BuildInfo 还包含了完整的依赖树信息（info.Deps）。这在安全响应中价值连城。</p>
<p>想象一下，如果某个广泛使用的库（例如 github.com/gin-gonic/gin）爆出了高危漏洞，你需要确认线上几十个微服务中，哪些服务使用了受影响的版本。</p>
<p>传统的做法是去扫 go.mod 文件，但 go.mod 里的版本不一定是最终编译进二进制的版本（可能被 replace 或升级）。<strong>最准确的真相，藏在二进制文件里。</strong></p>
<p>我们可以暴露一个 /debug/deps 接口：</p>
<pre><code class="go">// buildinfo-examples/demo3/main.go

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "runtime/debug"

    _ "github.com/gin-gonic/gin" // &lt;---- 这里空导入一个依赖
)

// DepInfo 定义返回给前端的依赖信息结构
type DepInfo struct {
    Path    string json:"path"    // 依赖包路径
    Version string json:"version" // 依赖版本
    Sum     string json:"sum"     // 校验和
}

// BuildInfoResponse 完整的构建信息响应
type BuildInfoResponse struct {
    GoVersion string    json:"go_version"
    MainMod   string    json:"main_mod"
    Deps      []DepInfo json:"deps"
}

func depsHandler(w http.ResponseWriter, r *http.Request) {
    // 读取构建信息
    info, ok := debug.ReadBuildInfo()
    if !ok {
        http.Error(w, "无法获取构建信息，请确保使用 Go Modules 构建", http.StatusInternalServerError)
        return
    }

    resp := BuildInfoResponse{
        GoVersion: info.GoVersion,
        MainMod:   info.Main.Path,
        Deps:      make([]DepInfo, 0, len(info.Deps)),
    }

    // 遍历依赖树
    for _, d := range info.Deps {
        resp.Deps = append(resp.Deps, DepInfo{
            Path:    d.Path,
            Version: d.Version,
            Sum:     d.Sum,
        })
    }

    // 设置响应头并输出 JSON
    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(resp); err != nil {
        log.Printf("JSON编码失败: %v", err)
    }
}

func main() {
    http.HandleFunc("/debug/deps", depsHandler)

    fmt.Println("服务已启动，请访问: http://localhost:8080/debug/deps")
    // 为了演示依赖输出，你需要确保这个项目是一个 go mod 项目，并引入了一些第三方库
    // 例如：go get github.com/gin-gonic/gin
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>通过这个接口，运维平台可以瞬间扫描全网服务，精确定位漏洞影响范围。</p>
<p>以下是编译和运行示例代码的步骤：</p>
<pre><code>$go mod tidy
$go build
$./demo3
服务已启动，请访问: http://localhost:8080/debug/deps
</code></pre>
<p>使用浏览器打开http://localhost:8080/debug/deps，你会看到类似如下信息：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-introspection-using-debug-buildinfo-2.png" alt="" /></p>
<h2>进阶：不仅是“自省”，还能“他省”</h2>
<p>runtime/debug 用于读取当前运行程序的构建信息。但有时候，我们需要检查一个躺在磁盘上的二进制文件（比如在 CI/CD 流水线中检查构建产物，或者分析一个未知的程序）。</p>
<p>这时，我们需要用到标准库 debug/buildinfo。</p>
<p>下面这个示例代码是一个 CLI 工具，它可以读取磁盘上<strong>任意</strong> Go 编译的二进制文件，并分析其 Git 信息和依赖。</p>
<p><strong>文件：demo4/inspector.go</strong></p>
<pre><code class="go">package main

import (
    "debug/buildinfo"
    "flag"
    "fmt"
    "log"
    "os"
    "text/tabwriter"
)

func main() {
    // 解析命令行参数
    flag.Parse()
    if flag.NArg() &lt; 1 {
        fmt.Println("用法: inspector &lt;path-to-go-binary&gt;")
        os.Exit(1)
    }

    binPath := flag.Arg(0)

    // 核心：使用 debug/buildinfo 读取文件，而不是 runtime
    info, err := buildinfo.ReadFile(binPath)
    if err != nil {
        log.Fatalf("读取二进制文件失败: %v", err)
    }

    fmt.Printf("=== 二进制文件分析: %s ===\n", binPath)
    fmt.Printf("Go 版本: \t%s\n", info.GoVersion)
    fmt.Printf("主模块路径: \t%s\n", info.Main.Path)

    // 提取 VCS (Git) 信息
    fmt.Println("\n[版本控制信息]")
    vcsInfo := make(map[string]string)
    for _, setting := range info.Settings {
        vcsInfo[setting.Key] = setting.Value
    }

    // 使用 tabwriter 对齐输出
    w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
    if rev, ok := vcsInfo["vcs.revision"]; ok {
        fmt.Fprintf(w, "Commit Hash:\t%s\n", rev)
    }
    if time, ok := vcsInfo["vcs.time"]; ok {
        fmt.Fprintf(w, "Build Time:\t%s\n", time)
    }
    if mod, ok := vcsInfo["vcs.modified"]; ok {
        dirty := "否"
        if mod == "true" {
            dirty = "是 (包含未提交的更改!)"
        }
        fmt.Fprintf(w, "Dirty Build:\t%s\n", dirty)
    }
    w.Flush()

    // 打印部分依赖
    fmt.Printf("\n[依赖模块 (前5个)]\n")
    for i, dep := range info.Deps {
        if i &gt;= 5 {
            fmt.Printf("... 以及其他 %d 个依赖\n", len(info.Deps)-5)
            break
        }
        fmt.Printf("- %s %s\n", dep.Path, dep.Version)
    }
}
</code></pre>
<p><strong>运行指南：</strong></p>
<ol>
<li>编译这个工具：go build -o inspector</li>
<li>找一个其他的 Go 程序（或者就用它自己）：</li>
</ol>
<pre><code>$./inspector ./inspector
=== 二进制文件分析: ./inspector ===
Go 版本:  go1.25.3
主模块路径:  demo4

[版本控制信息]
Commit Hash:  aa3539a9c4da76d89d25573917b2b37bb43f8a2a
Build Time:   2025-12-22T04:24:05Z
Dirty Build:  是 (包含未提交的更改!)

[依赖模块 (前5个)]
</code></pre>
<p>这实际上就是 go version -m <binary> 命令的底层实现原理。用go version查看一下inspector程序的信息：</p>
<pre><code>$go version -m ./inspector
./inspector: go1.25.3
    path    demo4
    mod demo4   (devel)
    build   -buildmode=exe
    build   -compiler=gc
    build   CGO_ENABLED=1
    build   CGO_CFLAGS=
    build   CGO_CPPFLAGS=
    build   CGO_CXXFLAGS=
    build   CGO_LDFLAGS=
    build   GOARCH=amd64
    build   GOOS=darwin
    build   GOAMD64=v1
    build   vcs=git
    build   vcs.revision=aa3539a9c4da76d89d25573917b2b37bb43f8a2a
    build   vcs.time=2025-12-22T04:24:05Z
    build   vcs.modified=true
</code></pre>
<h2>最佳实践建议</h2>
<ol>
<li>
<p><strong>标准化 CLI 版本输出</strong>：<br />
在你的 CLI 工具中，利用 ReadBuildInfo 实现 &#8211;version 参数，输出 Commit Hash 和 Dirty 状态。这比手动维护一个 const Version = “v1.0.0&#8243; 要可靠得多。</p>
</li>
<li>
<p><strong>Prometheus 埋点</strong>：<br />
在服务启动时，读取构建信息，并将其作为 Prometheus Gauge 指标的一个固定的 Label 暴露出去（例如 build_info{branch=”main”, commit=”abc1234&#8243;, goversion=”1.25&#8243;}）。这样你就可以在 Grafana 上直观地看到版本发布的变更曲线。</p>
</li>
<li>
<p><strong>警惕 -trimpath</strong>：<br />
虽然 -trimpath 对构建可重现的二进制文件很有用，但它不会影响 VCS 信息的嵌入，大家可以放心使用。但是，如果你使用了 -buildvcs=false，那么本文提到的 Git 信息将全部丢失。</p>
</li>
</ol>
<h2>小结</h2>
<p>Go 语言通过 debug.BuildInfo 将构建元数据的一等公民身份赋予了二进制文件。作为开发者，我们不应浪费这一特性。</p>
<p>从今天起，停止在 Makefile 里拼接版本号的魔法吧，让你的 Go 程序拥有“自我意识”，让线上排查变得更加从容。</p>
<p>本文涉及的示例源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/buildinfo-examples">这里</a>下载。</p>
<hr />
<p><strong>聊聊你的版本管理</strong></p>
<p>告别了繁琐的 ldflags，Go 原生的自省能力确实让人眼前一亮。<strong>在你的项目中，目前是使用什么方式来管理和输出版本信息的？是否遇到过因为版本不清导致的线上“罗生门”？</strong></p>
<p><strong>欢迎在评论区分享你的踩坑经历或最佳实践！</strong> 让我们一起把服务的“户口本”管好。</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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Bash 虽好，但我选 Go：如何用 10 倍代码换来 100 倍的维护性？</title>
		<link>https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability/</link>
		<comments>https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability/#comments</comments>
		<pubDate>Wed, 24 Dec 2025 04:00:45 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI辅助编程]]></category>
		<category><![CDATA[AWSSSM]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[BoilerplateCode]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[Copilot]]></category>
		<category><![CDATA[CrossPlatform]]></category>
		<category><![CDATA[Cursor]]></category>
		<category><![CDATA[debugging]]></category>
		<category><![CDATA[EngineeringGovernance]]></category>
		<category><![CDATA[EnvMap]]></category>
		<category><![CDATA[ExplicitContract]]></category>
		<category><![CDATA[GlueCode]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[Maintainability]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[StaticBinary]]></category>
		<category><![CDATA[StaticTypeChecking]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[Testability]]></category>
		<category><![CDATA[ToolchainHell]]></category>
		<category><![CDATA[TypeSafety]]></category>
		<category><![CDATA[vault]]></category>
		<category><![CDATA[Verbosity]]></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=5591</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability 大家好，我是Tony Bai。 “Bash 是一种很棒的胶水语言，但 Go 是更好的胶水。” 在日常开发中，我们经常会写一些 Bash 脚本来处理本地环境配置、启动 Docker 容器、同步密钥等琐碎任务。起初，它们只是几行简单的命令；但随着时间推移，它们逐渐膨胀成包含数百行 jq、sed、awk 的怪物，充斥着针对 macOS 和 Linux 的条件分支，以及“千万别动这行代码”的注释。 近日，一位开发者分享了他用 Go 重写这些 Bash 脚本的经历，引发了一场Go社区的关于工程可维护性与“胶水代码”治理的深度探讨。 在本文中，我们将跟随这位开发者的视角，深入剖析这次从脚本到工程的“降熵”之旅，并探讨在 AI 辅助编程日益普及的今天，这一选择背后的新逻辑。 Bash 脚本的“熵增”之路 许多团队的本地开发环境脚本，往往始于一个简单的需求：从 AWS SSM 或 Vault 拉取密钥，生成 .env 文件，然后启动服务。 最初的 Bash 脚本可能只有 10 行。但随着需求增加，它变成了这样： 工具链依赖地狱：脚本依赖特定版本的 sed、grep 或 jq。一旦某个同事更新了系统工具，脚本就挂了。 跨平台噩梦：sed 在 macOS 和 Linux 上的行为不一致，导致脚本中充斥着 if [[ [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/bash-vs-go-10x-code-100x-maintainability-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability">本文永久链接</a> &#8211; https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability</p>
<p>大家好，我是Tony Bai。</p>
<blockquote>
<p>“Bash 是一种很棒的胶水语言，但 Go 是更好的胶水。”</p>
</blockquote>
<p>在日常开发中，我们经常会写一些 Bash 脚本来处理本地环境配置、启动 Docker 容器、同步密钥等琐碎任务。起初，它们只是几行简单的命令；但随着时间推移，它们逐渐膨胀成包含数百行 jq、sed、awk 的怪物，充斥着针对 macOS 和 Linux 的条件分支，以及“千万别动这行代码”的注释。</p>
<p>近日，一位开发者<a href="https://www.reddit.com/r/golang/comments/1pb7t1q/show_tell_bash_is_great_glue_go_is_better_glue/">分享了他用 Go 重写这些 Bash 脚本的经历</a>，引发了一场Go社区的关于<strong>工程可维护性</strong>与<strong>“胶水代码”治理</strong>的深度探讨。</p>
<p>在本文中，我们将跟随这位开发者的视角，深入剖析这次从脚本到工程的“降熵”之旅，并探讨在 AI 辅助编程日益普及的今天，这一选择背后的新逻辑。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/system-programming-in-go-pr.png" alt="" /></p>
<h2>Bash 脚本的“熵增”之路</h2>
<p>许多团队的本地开发环境脚本，往往始于一个简单的需求：从 AWS SSM 或 Vault 拉取密钥，生成 .env 文件，然后启动服务。</p>
<p>最初的 Bash 脚本可能只有 10 行。但随着需求增加，它变成了这样：</p>
<ul>
<li><strong>工具链依赖地狱</strong>：脚本依赖特定版本的 sed、grep 或 jq。一旦某个同事更新了系统工具，脚本就挂了。</li>
<li><strong>跨平台噩梦</strong>：sed 在 macOS 和 Linux 上的行为不一致，导致脚本中充斥着 if [[ "$OS" == "darwin" ]] 这样的分支。</li>
<li><strong>调试困难</strong>：当脚本出错时，你很难知道是哪一行管道（pipe）出了问题，也没有类型检查来帮你发现潜在错误。</li>
</ul>
<p>正如评论区一位开发者所言：“Bash 脚本就像是一堆没有明确所有权的‘杂物’。每个人都在上面打补丁，直到它变成一个没人敢碰的定时炸弹。”</p>
<h2>Go 作为“强力胶水”的优势</h2>
<p>原作者将这堆复杂的 Bash 逻辑重构为一个名为 envmap 的小型 Go CLI 工具。虽然代码行数可能增加了（Go 确实比 Bash 繁琐），但他收获了<strong>工程质量的质变</strong>：</p>
<h3>结构化配置与类型安全</h3>
<p>不再有脆弱的字符串解析。配置被定义为强类型的 struct，编译器会帮你检查拼写错误和类型不匹配。</p>
<pre><code class="go">// Bash: 祈祷这个字符串解析是对的...
// Go: 编译器保证它是对的
type Config struct {
    Env      string json:"env"
    Region   string json:"region"
    UseVault bool   json:"use_vault"
}
</code></pre>
<h3>接口抽象与可测试性</h3>
<p>原作者定义了一个 Provider 接口来抽象不同的密钥后端（AWS SSM, Vault, 本地文件）。这不仅让代码结构清晰，更重要的是，<strong>它变得可测试了</strong>。你可以轻松编写单元测试来验证逻辑，而无需真的连接到 AWS。</p>
<pre><code class="go">type Provider interface {
    Get(ctx context.Context, key string) (string, error)
    // ...
}
</code></pre>
<h3>跨平台的一致性</h3>
<p>Go 编译出的静态二进制文件，消除了“它在我的机器上能跑”的问题。无论同事使用 macOS、Linux 还是 Windows，他们运行的都是相同的逻辑，不再受系统自带 Shell 工具版本的影响。</p>
<h2>社区的思辨——“杀鸡用牛刀”吗？</h2>
<p>这场重构也引发了激烈的讨论。有开发者质疑：用 Go 写脚本是不是太重了？Python 或 TypeScript 岂不是更好的替代品？甚至，为什么不直接用 Makefile？</p>
<h3>反方观点：复杂度的转移</h3>
<ul>
<li><strong>“代码更多了”</strong>：Go 的 verbose（繁琐）是公认的。简单的 cp a b 在 Go 中需要写不少代码。</li>
<li><strong>“编译步骤”</strong>：虽然 go run很快，但毕竟多了一个编译环节。</li>
</ul>
<h3>正方观点：维护性的胜利</h3>
<ul>
<li><strong>“长期收益”</strong>：一位开发者分享了他将 40k 行 Bash/Perl 脚本重构为 10k 行 Go 代码的经历。虽然初期投入大，但获得了<strong>测试覆盖</strong>、<strong>文档化</strong>和<strong>零依赖部署</strong>的巨大收益。</li>
<li><strong>“显式契约”</strong>：Bash 脚本之间往往通过不稳定的文本流（stdout/stdin）通信，极其脆弱。而 Go 代码之间通过明确的接口和模块调用通信，更加稳健。</li>
</ul>
<p>正如一位评论者总结的：“如果你只是写一个 10 行的脚本，Bash 是完美的。但如果你的脚本开始需要处理复杂的逻辑、状态和错误，那么它就不再是一个脚本，而是一个<strong>程序</strong>。既然是程序，就应该用编写程序的语言（如 Go）来写。”</p>
<h2>AI 时代的变量——“繁琐”不再是借口</h2>
<p>在过去，阻碍开发者用 Go 替代 Bash 的最大阻力往往是<strong>编写效率</strong>。写一个几十行的 Go 程序来替换一行 sed 命令，听起来确实不仅“繁琐”，而且“低效”。</p>
<p>然而，在 AI 辅助编程（如 Copilot, Cursor, Claude Code等）普及的今天，这个天平正在发生倾斜。</p>
<h3>AI 为 Go 支付了“样板税”</h3>
<p>Go 语言的 verbose（繁琐）特性——显式的错误处理、结构体定义、库的引入——曾经是手写代码的负担。但在 AI 时代，这些标准化的样板代码恰恰是 <strong>LLM（大语言模型）最擅长生成的</strong>。</p>
<p>你只需要告诉 AI：“写一个 CLI，读取环境变量，请求 AWS SSM，如果有错误就打印红色日志。” AI 能瞬间生成 80% 的 Go 代码骨架。开发者只需专注于核心逻辑的微调。</p>
<h3>编译器是 AI 最好的“质检员”</h3>
<p>用 AI 生成 Bash 脚本是一场赌博。LLM 可能会编造出不存在的 awk 参数，或者写出在某些 Shell 下不兼容的语法，而这些错误往往要在运行时才能发现（甚至引发灾难性的 rm -rf）。</p>
<p>相比之下，用 AI 生成 Go 代码具有天然的<strong>安全屏障</strong>：</p>
<ul>
<li><strong>静态类型检查</strong>：如果 AI 幻觉了不存在的方法，编译器会立刻报错，而不是等到运行时崩溃。</li>
<li><strong>确定性</strong>：Go 的语法规范极其严格，减少了 AI 生成“虽然能跑但很奇怪”的代码的概率。</li>
</ul>
<p>正如原作者在回复中所承认的：“我使用了 Cursor 和 Codex，代码的复杂性主要来自业务逻辑，而非语言本身。” <strong>在 AI 的加持下，获得一个类型安全、跨平台、易维护的 Go 二进制文件，其生产效率已经并不输给编写和调试一个脆弱的 Bash 脚本。</strong></p>
<h2>小结：从脚本到工程，从手写到 AI 共生</h2>
<p>这个案例告诉我们，<strong>“胶水代码”也需要工程化治理</strong>。</p>
<p>当你的 Bash 脚本开始变得让你感到恐惧、难以维护时，不要犹豫，用 Go 重写它吧。虽然你会多写一些 if err != nil，但你换来的是<strong>确定性</strong>、<strong>可维护性</strong>和<strong>内心的宁静</strong>。</p>
<p>特别是在 AI 时代，Go 语言的“繁琐”已被智能助手和编码智能体消解，而它带来的“稳健”却愈发珍贵。Go 也许不是最简洁的胶水，但在 AI 的帮助下，它绝对是性价比最高、<strong>最牢固</strong>的胶水。</p>
<p>资料链接：https://www.reddit.com/r/golang/comments/1pb7t1q/show_tell_bash_is_great_glue_go_is_better_glue/</p>
<hr />
<p><strong>你的“胶水”选型</strong></p>
<p>“Bash 还是 Go/Python？”这可能是每个团队都会面临的选择题。<strong>在你的工作中，你会为多大规模的脚本选择改用 Go 或 Python 重写？你是否有过被复杂 Bash 脚本“坑”惨的经历？</strong></p>
<p><strong>欢迎在评论区分享你的“血泪史”或“重构心得”！</strong> 让我们一起探讨如何让工具代码更优雅。</p>
<p><strong>如果这篇文章给了你重构旧脚本的勇气，别忘了点个【赞】和【在看】，并分享给你的团队！</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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>还在当“上下文搬运工”？我写了一门课，帮你重塑AI开发工作流</title>
		<link>https://tonybai.com/2025/11/20/ai-native-dev-workflow/</link>
		<comments>https://tonybai.com/2025/11/20/ai-native-dev-workflow/#comments</comments>
		<pubDate>Thu, 20 Nov 2025 00:19:25 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[agents.md]]></category>
		<category><![CDATA[ai-native-dev-workflow]]></category>
		<category><![CDATA[AIDeveloperIntegrationMaturityModel]]></category>
		<category><![CDATA[AILargeModel]]></category>
		<category><![CDATA[AI伙伴协作]]></category>
		<category><![CDATA[AI公司]]></category>
		<category><![CDATA[AI原生]]></category>
		<category><![CDATA[AI原生世界观]]></category>
		<category><![CDATA[AI原生开发方法论]]></category>
		<category><![CDATA[AI工作流指挥家]]></category>
		<category><![CDATA[AI工具使用者]]></category>
		<category><![CDATA[AI开发工作流]]></category>
		<category><![CDATA[AI驾驶舱]]></category>
		<category><![CDATA[CI/CD]]></category>
		<category><![CDATA[Claude.md]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[CodingAgent]]></category>
		<category><![CDATA[CommandLineCodingAgent]]></category>
		<category><![CDATA[constitution.md]]></category>
		<category><![CDATA[Copilot]]></category>
		<category><![CDATA[F1RacingCar]]></category>
		<category><![CDATA[F1赛车驾驶手册]]></category>
		<category><![CDATA[GitCommit]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Goproject]]></category>
		<category><![CDATA[HeadlessMode]]></category>
		<category><![CDATA[Hooks]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[IDEPlugin]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[MCP]]></category>
		<category><![CDATA[plan.md]]></category>
		<category><![CDATA[SDD]]></category>
		<category><![CDATA[Skills]]></category>
		<category><![CDATA[SlashCommands]]></category>
		<category><![CDATA[spec-driven-development]]></category>
		<category><![CDATA[spec.md]]></category>
		<category><![CDATA[Subagents]]></category>
		<category><![CDATA[tasks.md]]></category>
		<category><![CDATA[TDD]]></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[命令行AI智能体]]></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[智普AI]]></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[通用CodingAgent驾驭技能]]></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=5410</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/11/20/ai-native-dev-workflow 大家好，我是Tony Bai。 最近半年，我发现我的开发日常，正被一种新的“工作流摩擦”所困扰。 我猜，你可能也感同身受。 我们在一块屏幕上沉浸于IDE中的Go代码，在另一块屏幕上，则像一个勤奋的“学生”，不断向AI大模型提问。我们从代码库中精心挑选上下文，复制，切换窗口，粘贴，然后带着AI给出的答案，再复制，切换，粘贴回来。 我们成了AI时代的“上下文搬运工”和“提示词调优师”。 IDE插件的出现，让AI离我们更近了一步，它像一个“副驾驶”，能为我们提供实时的建议。但它依然无法真正地“动手”——它不能为你运行一次测试，不能帮你执行一次git commit，更无法理解你那套复杂的Makefile里到底藏着什么玄机。 我们拥抱了AI，却发现自己陷入了一个新的“效率怪圈”。我们与AI的协作，始终是割裂的、被动的、充满摩擦的。 我一直在思考，这真的是AI时代软件开发的终极形态吗？一定有更好的方式。一定有一种方法，能让AI不再是一个外部的“辅助工具”，而是成为我们开发流程中一个原生的、可指挥的、能动手干活的“核心成员”。 正是为了系统性地解决这个问题，并把我过去大半年时间的思考、踩坑、实践与沉淀分享出来，我与极客时间合作，倾力打造了一门全新的专栏——《AI原生开发工作流实战：重塑新一代软件工程范式》。 为什么要写这个专栏？ 因为我相信，软件开发的范式，正在经历一场深刻的革命。 我们正从“人机协作”的1.0时代，迈向“AI原生”的2.0时代。在这场变革中，开发者的核心价值，将不再仅仅是“写出代码”，而是“设计出能让AI写出高质量代码的工作流”。 而承载这场革命的最佳载体，正是以Claude Code为代表的新一代命令行AI智能体（Command-line Coding Agent）。它们让AI的能力，以前所未有的深度，“活”进了我们最熟悉的开发环境——终端里。 但是，拥有强大的工具，和懂得如何驾驭它，是两回事。 下面是一个AI-开发者集成成熟度模型，你看看你处在哪一层？ 我看到的太多开发者，依然在用L1、L2的思维模式，去使用一个为L3、L4工作流设计的强大智能体。这就像开着一辆F1赛车去买菜，不仅没发挥出它的全部性能，还觉得它“不好开”。 这个专栏的目标，就是为你提供那本缺失的“F1赛车驾驶手册”。它不是一本简单的工具说明书，而是一套完整的AI原生开发方法论。我将带你一起，从“第一性原理”出发，重新思考和构建我们在AI时代的软件工程实践。 在这个专栏里，我为你设计了怎样的学习路径？ 为了让你能系统性地完成这次思维和技能的升维，我将专栏精心设计为四个层层递进的模块，它就像一张清晰的“升级打怪地图”： 模块一：概念篇 · 建立AI原生世界观 在这一模块，我们将首先统一认知。你将深入理解什么是“规范驱动开发（Spec-Driven Development）”，这一AI原生开发的核心引擎。我们还会一起扫描整个命令行AI Agent的生态，并最终明确，我们为什么选择Claude Code作为核心的实战载体，以及如何通过接入国产大模型（如智普AI）来解决国内开发者的成本与可用性问题。 模块二：基础篇 · 掌握与AI伙伴协作的通用语言 我们将从零开始，手把手带你掌握与AI Agent协作的核心交互模型。你将精通上下文的艺术（CLAUDE.md, agents.md, constitution.md），学会如何为AI注入“长期记忆”和项目“宪法”。你还将掌握强大的自定义指令（Slash Commands），开始将你自己的工作流封装为AI可以执行的命令。学完此模块，你将能为任何项目快速定制一套AI‘说明书’，让它秒懂你的代码库。 模块三：进阶篇 · 将Agent锻造成你的专属神器 这是专栏的“硬核”部分。我们将进入AI Agent的“引擎室”，为你揭示其所有高级特性的工作原理和实战技巧。从安全基石（权限、沙箱、快照回滚），到能力扩展矩阵（Hooks, Skills, Sub-agents, MCP），再到自动化接口（Headless模式），你将学会如何将一个通用AI，彻底“魔改”成一个懂你项目、听你指挥的“专属神器”。学完此模块，你将拥有‘魔改’AI Agent的能力，让它从‘通用模型’变成你的‘专属战友’。 模块四：实战篇 · 在真实项目中重塑工程实践 这是整个专栏的“毕业大戏”。我们将把前面所有学到的理论和技巧，全部应用到一个从零到一的Go项目构建中。在通过顶层设计建立好你的AI驾驶舱后，你将亲历一个功能，是如何在AI原生工作流的加持下，被一步步地设计（spec.md）、规划（plan.md, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/11/20/ai-native-dev-workflow">本文永久链接</a> &#8211; https://tonybai.com/2025/11/20/ai-native-dev-workflow</p>
<p>大家好，我是Tony Bai。</p>
<p>最近半年，我发现我的开发日常，正被一种新的“工作流摩擦”所困扰。</p>
<p>我猜，你可能也感同身受。</p>
<p>我们在一块屏幕上沉浸于IDE中的Go代码，在另一块屏幕上，则像一个勤奋的“学生”，不断向AI大模型提问。我们从代码库中精心挑选上下文，复制，切换窗口，粘贴，然后带着AI给出的答案，再复制，切换，粘贴回来。</p>
<p>我们成了AI时代的“<strong>上下文搬运工</strong>”和“<strong>提示词调优师</strong>”。</p>
<p>IDE插件的出现，让AI离我们更近了一步，它像一个“副驾驶”，能为我们提供实时的建议。但它依然无法真正地“动手”——它不能为你运行一次测试，不能帮你执行一次git commit，更无法理解你那套复杂的Makefile里到底藏着什么玄机。</p>
<p>我们拥抱了AI，却发现自己陷入了一个新的“效率怪圈”。我们与AI的协作，始终是割裂的、被动的、充满摩擦的。</p>
<p>我一直在思考，这真的是AI时代软件开发的终极形态吗？一定有更好的方式。一定有一种方法，能让AI不再是一个外部的“辅助工具”，而是成为我们开发流程中一个<strong>原生的、可指挥的、能动手干活的“核心成员”</strong>。</p>
<p>正是为了系统性地解决这个问题，并把我过去大半年时间的思考、踩坑、实践与沉淀分享出来，我与极客时间合作，倾力打造了一门全新的专栏——<strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战：重塑新一代软件工程范式</a>》</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-3.jpg" alt="" /></p>
<h2>为什么要写这个专栏？</h2>
<p>因为我相信，软件开发的范式，正在经历一场深刻的革命。</p>
<p>我们正从“<strong>人机协作</strong>”的1.0时代，迈向“<strong>AI原生</strong>”的2.0时代。在这场变革中，开发者的核心价值，将不再仅仅是“写出代码”，而是“<strong>设计出能让AI写出高质量代码的工作流</strong>”。</p>
<p>而承载这场革命的最佳载体，正是以Claude Code为代表的新一代<strong>命令行AI智能体（Command-line Coding Agent）</strong>。它们让AI的能力，以前所未有的深度，“活”进了我们最熟悉的开发环境——终端里。</p>
<p>但是，拥有强大的工具，和懂得如何驾驭它，是两回事。</p>
<p>下面是一个AI-开发者集成成熟度模型，你看看你处在哪一层？</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-2.png" alt="" /></p>
<p>我看到的太多开发者，依然在用L1、L2的思维模式，去使用一个为L3、L4工作流设计的强大智能体。这就像开着一辆F1赛车去买菜，不仅没发挥出它的全部性能，还觉得它“不好开”。</p>
<p>这个专栏的目标，就是为你提供那本缺失的“<strong>F1赛车驾驶手册</strong>”。它不是一本简单的工具说明书，而是一套完整的<strong>AI原生开发方法论</strong>。我将带你一起，从“第一性原理”出发，重新思考和构建我们在AI时代的软件工程实践。</p>
<h2>在这个专栏里，我为你设计了怎样的学习路径？</h2>
<p>为了让你能系统性地完成这次思维和技能的升维，我将专栏精心设计为四个层层递进的模块，它就像一张清晰的“升级打怪地图”：</p>
<ul>
<li>
<p><strong>模块一：概念篇 · 建立AI原生世界观</strong><br />
在这一模块，我们将首先统一认知。你将深入理解什么是“<strong>规范驱动开发（Spec-Driven Development）</strong>”，这一AI原生开发的核心引擎。我们还会一起扫描整个命令行AI Agent的生态，并最终明确，我们为什么选择Claude Code作为核心的实战载体，以及如何通过<strong>接入国产大模型（如智普AI）</strong>来解决国内开发者的成本与可用性问题。</p>
</li>
<li>
<p><strong>模块二：基础篇 · 掌握与AI伙伴协作的通用语言</strong><br />
我们将从零开始，手把手带你掌握与AI Agent协作的核心交互模型。你将精通<strong>上下文的艺术</strong>（CLAUDE.md, agents.md, constitution.md），学会如何为AI注入“长期记忆”和项目“宪法”。你还将掌握强大的<strong>自定义指令（Slash Commands）</strong>，开始将你自己的工作流封装为AI可以执行的命令。学完此模块，你将能为任何项目快速定制一套AI‘说明书’，让它秒懂你的代码库。</p>
</li>
<li>
<p><strong>模块三：进阶篇 · 将Agent锻造成你的专属神器</strong><br />
这是专栏的“硬核”部分。我们将进入AI Agent的“引擎室”，为你揭示其所有高级特性的工作原理和实战技巧。从<strong>安全基石</strong>（权限、沙箱、快照回滚），到<strong>能力扩展矩阵</strong>（Hooks, Skills, Sub-agents, MCP），再到<strong>自动化接口</strong>（Headless模式），你将学会如何将一个通用AI，彻底“魔改”成一个懂你项目、听你指挥的“专属神器”。学完此模块，你将拥有‘魔改’AI Agent的能力，让它从‘通用模型’变成你的‘专属战友’。</p>
</li>
<li>
<p><strong>模块四：实战篇 · 在真实项目中重塑工程实践</strong><br />
这是整个专栏的“毕业大戏”。我们将把前面所有学到的理论和技巧，全部应用到一个<strong>从零到一的Go项目</strong>构建中。在通过顶层设计建立好你的AI驾驶舱后，你将亲历一个功能，是如何在AI原生工作流的加持下，被一步步地<strong>设计</strong>（spec.md）、<strong>规划</strong>（plan.md, tasks.md）、<strong>编码</strong>（TDD）、<strong>审查</strong>、<strong>交付</strong>（CI/CD），乃至最终<strong>维护与重构</strong>的。这将是你把知识转化为能力的最佳演练场。</p>
</li>
</ul>
<h2>学完这门课，你将获得什么？</h2>
<ul>
<li><strong>一套前沿的开发方法论：</strong> 真正掌握“AI原生开发”与“规范驱动开发”的核心思想，而不仅仅是工具的零散技巧。</li>
<li><strong>一套通用的Coding Agent驾驭技能：</strong> 精通上下文注入、自定义工具和技能、自动化编排等核心技巧，无论未来出现什么新的Coding Agent工具，你都能快速上手。</li>
<li><strong>一套可落地的工程实践：</strong> 获得AI在需求、设计、TDD、CI/CD、重构等软件工程全流程中的最佳实践和Go语言实战代码。</li>
<li><strong>一次思维模式的升级：</strong> 完成从“AI工具使用者”到“<strong>AI工作流指挥家</strong>”的角色转变，构筑在AI时代的个人核心竞争力。</li>
</ul>
<h2>写在最后：一份“抛砖引玉”的邀请</h2>
<p>在策划这门课时，我始终保持着一种敬畏之心。</p>
<p><strong>Claude Code是2025年2月才正式进入大众视野的</strong>，至今也不过大半年的时间。整个命令行Coding Agent领域，都还处在一个高速演进、日新月异的“黎明时代”。我们所有人，包括我在内，都还在“<strong>摸着石头过河</strong>”。</p>
<p>因此，这个专栏的内容会更偏向于<strong>基础和入门</strong>，我希望通过最详尽的示例，为你直观地展现AI原生工作流的巨大潜力。我为你呈现的，更多是我个人在当前阶段探索出的<strong>一种可行的工作流</strong>，它未必是放之四海而皆准的“最优解”，更谈不上是“终极银弹”。</p>
<p>我更希望这个专栏，能成为一个“<strong>抛砖引玉</strong>”的平台。</p>
<p>我把我这块“砖”抛出来，是希望能引出你——每一位身处一线的优秀开发者——那块更宝贵的“玉”。我非常期待你在课程的评论区，分享你的思考、你的工作流、你的“最佳实践”。</p>
<p>我相信，关于AI原生开发的未来，最终的答案，一定不是由我一个人，也不是由任何一个AI公司定义的。它将由我们所有拥抱变革、勇于实践的开发者，共同书写。</p>
<p>让我们一起，成为定义这个新时代开发范式的第一批人。</p>
<p>现在，这门凝结了我大半年心血的课程 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 已经在极客时间正式上线了！</p>
<p>专栏为图文形式，共22讲。我为你准备了<strong>早鸟优惠 ¥59</strong>（原价 ¥99），仅限首周。</p>
<p><strong>扫描下方二维码，立即订阅</strong></p>
<p><strong>用一两杯咖啡的钱，投资一次面向未来的思维和技能升级。</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<p>如果你想先了解更详细的课程内容，可以点击「<a href="http://gk.link/a/12EPd">这里</a>」查看专栏的详细目录。</p>
<p>期待在课程中，与你相遇，共同精进！</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/11/20/ai-native-dev-workflow/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go编译的几个细节，连专家也要停下来想想</title>
		<link>https://tonybai.com/2024/11/11/some-details-about-go-compilation/</link>
		<comments>https://tonybai.com/2024/11/11/some-details-about-go-compilation/#comments</comments>
		<pubDate>Sun, 10 Nov 2024 22:13:45 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[CFLAGS]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CGO_ENABLED]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[DWARF]]></category>
		<category><![CDATA[expvar]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[getaddrinfo]]></category>
		<category><![CDATA[getnameinfo]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[glibc-static]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-sqlite3]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golist]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[inittask]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[ldflags]]></category>
		<category><![CDATA[LD_LIBRARY_PATH]]></category>
		<category><![CDATA[libresolv.so]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[NameResolution]]></category>
		<category><![CDATA[nm]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[sqlite-devel]]></category>
		<category><![CDATA[sqlite3]]></category>
		<category><![CDATA[TinyGo]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[内联]]></category>
		<category><![CDATA[动态链接]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[死码消除]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[静态链接]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4383</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/11/11/some-details-about-go-compilation 在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。 注：本文示例使用的环境为Go 1.23.0、Linux Kernel 3.10.0和CentOS 7.9。 1. Go编译默认采用静态链接还是动态链接？ 我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？ 很多人脱口而出：动态链接，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？ 我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点： $go env&#124;grep CGO_ENABLED CGO_ENABLED='1' 验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可： // go-compilation/main.go package main import "fmt" func main() { fmt.Println("hello, world") } 构建该程序： $go build -o helloworld-default main.go 之后，我们查看一下生成的可执行文件helloworld-default的文件属性： $file helloworld-default helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ldd [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/some-details-about-go-compilation-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/11/11/some-details-about-go-compilation">本文永久链接</a> &#8211; https://tonybai.com/2024/11/11/some-details-about-go-compilation</p>
<p>在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。</p>
<blockquote>
<p>注：本文示例使用的环境为<a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23/">Go 1.23.0</a>、Linux Kernel 3.10.0和CentOS 7.9。</p>
</blockquote>
<h2>1. Go编译默认采用静态链接还是动态链接？</h2>
<p>我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？</p>
<p>很多人脱口而出：<a href="https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/">动态链接</a>，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？</p>
<p>我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点：</p>
<pre><code>$go env|grep CGO_ENABLED
CGO_ENABLED='1'
</code></pre>
<p>验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可：</p>
<pre><code>// go-compilation/main.go

package main

import "fmt"

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

package main

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

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

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

package main

import (
    "fmt"
    _ "net"
)

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

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

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

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

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

.PHONY:  all static

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

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

#ifndef MYLIB_H
#define MYLIB_H

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

#endif // MYLIB_H

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

#include &lt;stdio.h&gt;

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

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

package main

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

package main

import (
    _ "expvar"
    "fmt"
)

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

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
runtime/cgo: [cgo.go]
github.com/mattn/go-sqlite3: [backup.go callback.go error.go sqlite3.go sqlite3_context.go sqlite3_load_extension.go sqlite3_opt_serialize.go sqlite3_opt_userauth_omit.go sqlite3_other.go sqlite3_type.go]
</code></pre>
<p>用空导入os/user的示例再来看一下：</p>
<pre><code>$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}'  main-with-os-user.go | grep -v '\[\]'
runtime/cgo: [cgo.go]
os/user: [cgo_lookup_cgo.go getgrouplist_unix.go]
</code></pre>
<p>我们知道os/user有纯go和C版本两个实现，因此上述判断只能说“对了一半”，当我关闭CGO_ENABLED时，Go编译器不会使用基于cgo的C版实现。</p>
<p>那是否在禁用cgo的前提下对源码进行一次编译便能验证项目是否对cgo有依赖呢？这样做显然谈不上是一种“快速”的方法，那是否有效呢？我们来对上面的go-sqlite3项目做一个测试，我们在关闭CGO_ENABLED时，编译一下该示例：</p>
<pre><code>// cd go-compilation/go-sqlite3
$ CGO_ENABLED=0 go build demo
</code></pre>
<p>我们看到，Go编译器并未报错！似乎该项目不需要cgo!  但真的是这样吗？我们运行一下编译后的demo可执行文件：</p>
<pre><code>$ ./demo
2024/11/03 22:10:36 "Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub": CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);
</code></pre>
<p>我们看到成功编译出来的程序居然出现运行时错误，提示需要cgo！</p>
<p>到这里，没有一种方法可以快速、精确的给出项目是否依赖cgo的判断。也许判断Go项目是否依赖CGO并没有捷径，需要从源码分析、依赖检查和构建测试等多个维度进行。</p>
<h2>7. 小结</h2>
<p>在本文中，我们深入探讨了Go语言编译过程中的几个重要细节，尤其是在静态链接和动态链接的选择上。通过具体示例，我们了解到：</p>
<ul>
<li>
<p>默认链接方式：尽管CGO_ENABLED默认值为1，Go编译器在大多数情况下会采用静态链接，只有在依赖特定的C库或标准库包时，才会切换到动态链接。</p>
</li>
<li>
<p>动态链接的条件：我们讨论了几种情况下Go会默认生成动态链接的可执行文件，包括依赖使用C实现的标准库包、显式使用cgo调用外部C程序，以及使用依赖cgo的第三方包。</p>
</li>
<li>
<p>实现静态链接：对于需要动态链接的场景，我们也提供了将其转为静态链接的解决方案，包括关闭CGO、使用静态库，以及处理依赖cgo的外部包的静态链接问题。</p>
</li>
<li>
<p>二进制文件优化：我们还介绍了如何通过去除符号表和使用TinyGo等方法来优化生成的Go二进制文件的大小，以满足不同场景下的需求。</p>
</li>
<li>
<p>符号编译与死码消除：最后，我们探讨了未使用的符号是否会被编译到最终的二进制文件中，并解释了Go编译器的死码消除机制。</p>
</li>
</ul>
<p>通过这些细节探讨，我希望能够帮助大家更好地理解Go编译的复杂性，并在实际开发中做出更明智的选择，亦能在面对Go编译相关问题时，提供有效的解决方案。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go-compilation">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/11/11/some-details-about-go-compilation/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Gopher的Rust第一课：Rust代码组织</title>
		<link>https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code/</link>
		<comments>https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code/#comments</comments>
		<pubDate>Thu, 06 Jun 2024 15:13:51 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bazel]]></category>
		<category><![CDATA[binary]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[Cargo.lock]]></category>
		<category><![CDATA[Cargo.toml]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[crate]]></category>
		<category><![CDATA[DataFusion]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[lib.rs]]></category>
		<category><![CDATA[library]]></category>
		<category><![CDATA[main]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[repo]]></category>
		<category><![CDATA[rlib]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[rustc]]></category>
		<category><![CDATA[self]]></category>
		<category><![CDATA[Thread]]></category>
		<category><![CDATA[tokio]]></category>
		<category><![CDATA[workspace]]></category>
		<category><![CDATA[编译器]]></category>

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

go 1.22.1

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

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

use rand::Rng;

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

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

error: aborting due to 1 previous error

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

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

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

pub mod bar;
pub mod foo;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

use rand::Rng;

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

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

[dependencies]
rand = "0.8.5"

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

extern crate package2;
extern crate rand;

use rand::Rng;

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

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

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

[dependencies]
rand = "0.8.5"
package2 = { path = "../package2" }
</code></pre>
<p>我们看到：package1的main.rs依赖package2这个crate中的add函数，我们在package1的Cargo.toml中为package1添加了新依赖package2，由于package2仅仅存放在本地，所以这里我们使用了path方式指定package2的位置。</p>
<p>我们执行一下添加内部依赖后的package1：</p>
<pre><code>$cargo run -p package1
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/package1`
Random number: 2485645524
result: 4
</code></pre>
<h2>4.4 小结</h2>
<p>本文循序渐进地讨论了在Rust项目中如何组织代码的问题，这对于Rust初学者来说尤为有用。</p>
<p>我们首先回顾了Go语言中的代码组织方式，介绍了Go项目代码组织的两个层级：module和package。然后，我们将Rust项目可以分为两种类型：使用rustc编译器的项目和使用Cargo的项目。</p>
<p>对于rustc-only的项目，开发者需要编写自己的构建脚本来管理项目的构建过程。</p>
<p>文章从最简单的单文件rustc-only项目开始介绍，展示了如何使用rustc编译器来编译和运行这种项目，并逐步介绍了带有外部依赖的rustc-only项目以及多文件项目的情况，引出了rust module概念。</p>
<p>rustc-only项目很少用于生产环境，这种方式主要用于学习和了解Rustc编译器的功能机制以及Rust语言的代码组织抽象。</p>
<p>在实际开发中，使用Cargo来创建和管理Rust包是常见的做法。在本章的后半段，我们介绍了使用cargo管理的rust项目的代码组织情况，包括单package项目和多package项目以及如何为项目引入外部和内部依赖。</p>
<p>总体而言，本文旨在帮助初学者理解和掌握Rust项目的代码组织结构，以提高学习效率和学习效果。通过介绍rustc-only项目和cargo管理的项目，读者可以逐步了解Rust代码组织的基本概念和实践方法。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/rust-guide-for-gopher/organizing-rust-code">这里</a>下载。</p>
<h2>4.5 参考资料</h2>
<ul>
<li><a href="https://doc.rust-lang.org/book">The book</a> &#8211; https://doc.rust-lang.org/book</li>
<li><a href="https://doc.rust-lang.org/cargo/index.html">The cargo book</a> &#8211; https://doc.rust-lang.org/cargo/index.html</li>
<li><a href="https://doc.rust-lang.org/rustc/index.html">The rustc book</a> &#8211; https://doc.rust-lang.org/rustc/index.html</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>编译Go应用的黑盒挑战：无源码只有.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>使用go test框架驱动的自动化测试</title>
		<link>https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test/</link>
		<comments>https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test/#comments</comments>
		<pubDate>Thu, 30 Mar 2023 13:42:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[acceptance-test]]></category>
		<category><![CDATA[awk]]></category>
		<category><![CDATA[broker]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-test-report]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[html]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[mosquitto]]></category>
		<category><![CDATA[mqtt]]></category>
		<category><![CDATA[Parallel]]></category>
		<category><![CDATA[publish]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Setup]]></category>
		<category><![CDATA[smoke-test]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[subscribe]]></category>
		<category><![CDATA[subtest]]></category>
		<category><![CDATA[teardown]]></category>
		<category><![CDATA[testcase]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[TestMain]]></category>
		<category><![CDATA[TOML]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[yaml]]></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=3841</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test 一. 背景 团队的测试人员稀缺，无奈只能“自己动手，丰衣足食”，针对我们开发的系统进行自动化测试，这样既节省的人力，又提高了效率，还增强了对系统质量保证的信心。 我们的目标是让自动化测试覆盖三个环境，如下图所示： 我们看到这三个环境分别是： CI/CD流水线上的自动化测试 发版后在各个stage环境中的自动化冒烟/验收测试 发版后在生产环境的自动化冒烟/验收测试 我们会建立统一的用例库或针对不同环境建立不同用例库，但这些都不重要，重要的是我们用什么语言来编写这些用例、用什么工具来驱动这些用例。 下面看看方案的诞生过程。 二. 方案 最初组内童鞋使用了YAML文件来描述测试用例，并用Go编写了一个独立的工具读取这些用例并执行。这个工具运作起来也很正常。但这样的方案存在一些问题： 编写复杂 编写一个最简单的connect连接成功的用例，我们要配置近80行yaml。一个稍微复杂的测试场景，则要150行左右的配置。 难于扩展 由于最初的YAML结构设计不足，缺少了扩展性，使得扩展用例时，只能重新建立一个用例文件。 表达能力不足 我们的系统是消息网关，有些用例会依赖一定的时序，但基于YAML编写的用例无法清晰地表达出这种用例。 可维护性差 如果换一个人来编写新用例或维护用例，这个人不仅要看明白一个个百十来行的用例描述，还要翻看一下驱动执行用例的工具，看看其执行逻辑。很难快速cover这个工具。 为此我们想重新设计一个工具，测试开发人员可以利用该工具支持的外部DSL文法来编写用例，然后该工具读取这些用例并执行。 注：根据Martin Fowler的《领域特定语言》一书对DSL的分类，DSL有三种选型：通用配置文件(xml, json, yaml, toml)、自定义领域语言，这两个合起来称为外部DSL。如：正则表达式、awk, sql、xml等。利用通用编程语言片段/子集作为DSL则称为内部dsl，像ruby等。 后来基于待测试的场景数量和用例复杂度粗略评估了一下DSL文法(甚至借助ChatGPT生成过几版DSL文法)，发现这个“小语言”那也是“麻雀虽小五脏俱全”。如果用这样的DSL编写用例，和利用通用语言(比如Python)编写的用例在代码量级上估计也不相上下了。 既然如此，自己设计外部DSL意义也就不大了。还不如用Python来整。但转念一想，既然用通用语言的子集了，团队成员对Python又不甚熟悉，那为啥不回到Go呢^_^。 让我们进行一个大胆的设定：将Go testing框架作为“内部DSL”来编写用例，用go test命令作为执行这些用例的测试驱动工具。此外，有了GPT-4加持，生成TestXxx、补充用例啥的应该也不是大问题。 下面我们来看看如何组织和编写用例并使用go test驱动进行自动化测试。 三. 实现 1. 测试用例组织 我的《Go语言精进之路vol2》书中的第41条“有层次地组织测试代码”中对基于go test的测试用例组织做过系统的论述。结合Go test提供的TestMain、TestXxx与sub test，我们完全可以基于go test建立起一个层次清晰的测试用例结构。这里就以一个对开源mqtt broker的自动化测试为例来说明一下。 注：你可以在本地搭建一个单机版的开源mqtt broker服务作为被测对象，比如使用Eclipse的mosquitto。 在组织用例之前，我先问了一下ChatGPT对一个mqtt broker测试都应该包含哪些方面的用例，ChatGPT给了我一个简单的表： 如果你对MQTT协议有所了解，那么你应该觉得ChatGPT给出的答案还是很不错的。 这里我们就以connection、subscribe和publish三个场景(scenario)来组织用例： $tree [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test">本文永久链接</a> &#8211; https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test</p>
<h2>一. 背景</h2>
<p>团队的测试人员稀缺，无奈只能“自己动手，丰衣足食”，针对我们开发的系统进行自动化测试，这样<strong>既节省的人力，又提高了效率，还增强了对系统质量保证的信心</strong>。</p>
<p>我们的目标是让自动化测试覆盖三个环境，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-2.png" alt="" /></p>
<p>我们看到这三个环境分别是：</p>
<ul>
<li>CI/CD流水线上的自动化测试</li>
<li>发版后在各个stage环境中的自动化冒烟/<a href="http://en.wikipedia.org/wiki/Acceptance_testing">验收测试</a></li>
<li>发版后在生产环境的自动化冒烟/验收测试</li>
</ul>
<p>我们会建立统一的用例库或针对不同环境建立不同用例库，但这些都不重要，重要的是我们<strong>用什么语言来编写这些用例、用什么工具来驱动这些用例</strong>。</p>
<p>下面看看方案的诞生过程。</p>
<h2>二. 方案</h2>
<p>最初组内童鞋使用了<a href="https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/">YAML文件</a>来描述测试用例，并用Go编写了一个独立的工具读取这些用例并执行。这个工具运作起来也很正常。但这样的方案存在一些问题：</p>
<ul>
<li>编写复杂</li>
</ul>
<p>编写一个最简单的connect连接成功的用例，我们要配置近80行yaml。一个稍微复杂的测试场景，则要150行左右的配置。</p>
<ul>
<li>难于扩展</li>
</ul>
<p>由于最初的YAML结构设计不足，缺少了扩展性，使得扩展用例时，只能重新建立一个用例文件。</p>
<ul>
<li>表达能力不足</li>
</ul>
<p>我们的系统是消息网关，有些用例会依赖一定的时序，但基于YAML编写的用例无法清晰地表达出这种用例。</p>
<ul>
<li>可维护性差</li>
</ul>
<p>如果换一个人来编写新用例或维护用例，这个人不仅要看明白一个个百十来行的用例描述，还要翻看一下驱动执行用例的工具，看看其执行逻辑。很难快速cover这个工具。</p>
<p>为此我们想重新设计一个工具，测试开发人员可以利用该工具支持的<a href="https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go">外部DSL文法</a>来编写用例，然后该工具读取这些用例并执行。</p>
<blockquote>
<p>注：根据Martin Fowler的<a href="https://book.douban.com/subject/21964984/">《领域特定语言》</a>一书对DSL的分类，DSL有三种选型：通用配置文件(xml, json, yaml, toml)、自定义领域语言，这两个合起来称为外部DSL。如：正则表达式、awk, sql、xml等。利用通用编程语言片段/子集作为DSL则称为内部dsl，像ruby等。</p>
</blockquote>
<p>后来基于待测试的场景数量和用例复杂度粗略评估了一下DSL文法(甚至借助ChatGPT生成过几版DSL文法)，发现这个“小语言”那也是“麻雀虽小五脏俱全”。如果用这样的DSL编写用例，和利用通用语言(比如Python)编写的用例在代码量级上估计也不相上下了。</p>
<p>既然如此，自己设计外部DSL意义也就不大了。还不如用Python来整。但转念一想，既然用通用语言的子集了，团队成员对Python又不甚熟悉，那为啥不回到Go呢^_^。</p>
<p>让我们进行一个大胆的设定：将Go testing框架作为“内部DSL”来编写用例，用go test命令作为执行这些用例的测试驱动工具。此外，有了GPT-4加持，生成TestXxx、补充用例啥的应该也不是大问题。</p>
<p>下面我们来看看如何组织和编写用例并使用go test驱动进行自动化测试。</p>
<h2>三. 实现</h2>
<h3>1. 测试用例组织</h3>
<p>我的<a href="https://item.jd.com/13694000.html">《Go语言精进之路vol2》</a>书中的<a href="https://book.douban.com/subject/35720729/">第41条“有层次地组织测试代码”</a>中对基于go test的测试用例组织做过系统的论述。结合Go test提供的<a href="https://pkg.go.dev/testing#Main">TestMain</a>、TestXxx与<a href="https://tonybai.com/2023/03/15/an-intro-of-go-subtest/">sub test</a>，我们完全可以基于go test建立起一个层次清晰的测试用例结构。这里就以一个对开源mqtt broker的自动化测试为例来说明一下。</p>
<blockquote>
<p>注：你可以在本地搭建一个单机版的开源mqtt broker服务作为被测对象，比如使用<a href="https://github.com/eclipse/mosquitto">Eclipse的mosquitto</a>。</p>
</blockquote>
<p>在组织用例之前，我先问了一下ChatGPT对一个mqtt broker测试都应该包含哪些方面的用例，ChatGPT给了我一个简单的表：</p>
<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-3.png" alt="" /></p>
<p>如果你对<a href="https://mqtt.org/mqtt-specification/">MQTT协议</a>有所了解，那么你应该觉得ChatGPT给出的答案还是很不错的。</p>
<p>这里我们就以connection、subscribe和publish三个场景(scenario)来组织用例：</p>
<pre><code>$tree -F .
.
├── Makefile
├── go.mod
├── go.sum
├── scenarios/
│   ├── connection/              // 场景：connection
│   │   ├── connect_test.go      // test suites
│   │   └── scenario_test.go
│   ├── publish/                 // 场景：publish
│   │   ├── publish_test.go      // test suites
│   │   └── scenario_test.go
│   ├── scenarios.go             // 场景中测试所需的一些公共函数
│   └── subscribe/               // 场景：subscribe
│       ├── scenario_test.go
│       └── subscribe_test.go    // test suites
└── test_report.html             // 生成的默认测试报告
</code></pre>
<p>简单说明一下这个测试用例组织布局：</p>
<ul>
<li>我们将测试用例分为多个场景(scenario)，这里包括connection、subscribe和publish；</li>
<li>由于是由go test驱动，所以每个存放test源文件的目录中都要遵循Go对Test的要求，比如：源文件以&#95;test.go结尾等。</li>
<li>每个场景目录下存放着测试用例文件，一个场景可以有多个&#95;test.go文件。这里设定&#95;test.go文件中的每个TestXxx为一个test suite，而TestXxx中再基于subtest编写用例，这里每个subtest case为一个最小的test case；</li>
<li>每个场景目录下的scenario_test.go，都是这个目录下包的TestMain入口，主要是考虑为所有包传入统一的命令行标志与参数值，同时你也针对该场景设置在TestMain中设置setup和teardown。该文件的典型代码如下：</li>
</ul>
<pre><code>// github.com/bigwhite/experiments/automated-testing/scenarios/subscribe/scenario_test.go

package subscribe

import (
    "flag"
    "log"
    "os"
    "testing"

    mqtt "github.com/eclipse/paho.mqtt.golang"
)

var addr string

func init() {
    flag.StringVar(&amp;addr, "addr", "", "the broker address(ip:port)")
}

func TestMain(m *testing.M) {
    flag.Parse()

    // setup for this scenario
    mqtt.ERROR = log.New(os.Stdout, "[ERROR] ", 0)

    // run this scenario test
    r := m.Run()

    // teardown for this scenario
    // tbd if teardown is needed

    os.Exit(r)
}
</code></pre>
<p>接下来我们再来看看具体测试case的实现。</p>
<h3>2. 测试用例实现</h3>
<p>我们以稍复杂一些的subscribe场景的测试为例，我们看一下subscribe目录下的subscribe_test.go中的测试suite和cases：</p>
<pre><code>// github.com/bigwhite/experiments/automated-testing/scenarios/subscribe/subscribe_test.go

package subscribe

import (
    scenarios "bigwhite/autotester/scenarios"
    "testing"
)

func Test_Subscribe_S0001_SubscribeOK(t *testing.T) {
    t.Parallel() // indicate the case can be ran in parallel mode

    tests := []struct {
        name  string
        topic string
        qos   byte
    }{
        {
            name:  "Case_001: Subscribe with QoS 0",
            topic: "a/b/c",
            qos:   0,
        },
        {
            name:  "Case_002: Subscribe with QoS 1",
            topic: "a/b/c",
            qos:   1,
        },
        {
            name:  "Case_003: Subscribe with QoS 2",
            topic: "a/b/c",
            qos:   2,
        },
    }

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // indicate the case can be ran in parallel mode
            client, testCaseTeardown, err := scenarios.TestCaseSetup(addr, nil)
            if err != nil {
                t.Errorf("want ok, got %v", err)
                return
            }
            defer testCaseTeardown()

            token := client.Subscribe(tt.topic, tt.qos, nil)
            token.Wait()

            // Check if subscription was successful
            if token.Error() != nil {
                t.Errorf("want ok, got %v", token.Error())
            }

            token = client.Unsubscribe(tt.topic)
            token.Wait()
            if token.Error() != nil {
                t.Errorf("want ok, got %v", token.Error())
            }
        })
    }
}

func Test_Subscribe_S0002_SubscribeFail(t *testing.T) {
}
</code></pre>
<p>这个测试文件中的测试用例与我们日常编写单测并没有什么区别！有一些需要注意的地方是：</p>
<ul>
<li>Test函数命名</li>
</ul>
<p>这里使用了Test_Subscribe_S0001_SubscribeOK、Test_Subscribe_S0002_SubscribeFail命名两个Test suite。命名格式为：</p>
<pre><code>Test_场景_suite编号_测试内容缩略
</code></pre>
<p>之所以这么命名，一来是测试用例组织的需要，二来也是为了后续在生成的Test report中区分不同用例使用。</p>
<ul>
<li>testcase通过subtest呈现</li>
</ul>
<p>每个TestXxx是一个test suite，而基于表驱动的每个sub test则对应一个test case。</p>
<ul>
<li>test suite和test case都可单独标识为是否可并行执行</li>
</ul>
<p>通过testing.T的Parallel方法可以标识某个TestXxx或test case(subtest)是否是可以并行执行的。</p>
<ul>
<li>针对每个test case，我们都调用setup和teardown</li>
</ul>
<p>这样可以保证test case间都相互独立，互不影响。</p>
<h3>3. 测试执行与报告生成</h3>
<p>设计完布局，编写完用例后，接下来就是执行这些用例。那么怎么执行这些用例呢？</p>
<p>前面说过，我们的方案是基于go test驱动的，我们的执行也要使用go test。</p>
<p>在顶层目录automated-testing下，执行如下命令：</p>
<pre><code>$go test ./... -addr localhost:30083
</code></pre>
<p>go test会遍历执行automated-testing下面每个包的测试，在执行每个包的测试时会将-addr这个flag传入。如果localhost:30083端口并没有mqtt broker服务监听，那么上面的命令将输出如下信息：</p>
<pre><code>$go test ./... -addr localhost:30083
?       bigwhite/autotester/scenarios   [no test files]
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   Failed to connect to a broker
--- FAIL: Test_Connection_S0001_ConnectOKWithoutAuth (0.00s)
    connect_test.go:20: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL    bigwhite/autotester/scenarios/connection    0.015s
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   Failed to connect to a broker
--- FAIL: Test_Publish_S0001_PublishOK (0.00s)
    publish_test.go:11: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL    bigwhite/autotester/scenarios/publish   0.016s
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   Failed to connect to a broker
[ERROR] [client]   Failed to connect to a broker
[ERROR] [client]   dial tcp [::1]:30083: connect: connection refused
[ERROR] [client]   Failed to connect to a broker
--- FAIL: Test_Subscribe_S0001_SubscribeOK (0.00s)
    --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_002:_Subscribe_with_QoS_1 (0.00s)
        subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
    --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_003:_Subscribe_with_QoS_2 (0.00s)
        subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
    --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_001:_Subscribe_with_QoS_0 (0.00s)
        subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL    bigwhite/autotester/scenarios/subscribe 0.016s
FAIL
</code></pre>
<p>这也是一种测试失败的情况。</p>
<p>在自动化测试时，我们一般会把错误或成功的信息保存到一个测试报告文件(多是html)中，那么我们如何基于上面的测试结果内容生成我们的测试报告文件呢？</p>
<p>首先go test支持将输出结果以结构化的形式展现，即传入-json这个flag。这样我们仅需基于这些json输出将各个字段读出并写入html中即可。好在有现成的开源工具可以做到这点，那就是<a href="https://github.com/vakenbolt/go-test-report">go-test-report</a>。下面是通过命令行管道让go test与go-test-report配合工作生成测试报告的命令行：</p>
<blockquote>
<p>注：go-test-report工具的安装方法：go install github.com/vakenbolt/go-test-report@latest</p>
</blockquote>
<pre><code>$go test ./... -addr localhost:30083 -json|go-test-report
[go-test-report] finished in 1.375540542s
</code></pre>
<p>执行结束后，就会在当前目录下生成一个test_report.html文件，使用浏览器打开该文件就能看到测试执行结果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-4.png" alt="" /></p>
<p>通过测试报告的输出，我们可以很清楚看到哪些用例通过，哪些用例失败了。并且通过Test suite的名字或Test case的名字可以快速定位是哪个scenario下的哪个suite的哪个case报的错误！我们也可以点击某个test suite的名字，比如：Test_Connection_S0001_ConnectOKWithoutAuth，打开错误详情查看错误对应的源文件与具体的行号：</p>
<p><img src="https://tonybai.com/wp-content/uploads/automated-testing-driven-by-go-test-5.png" alt="" /></p>
<p>为了方便快速敲入上述命令，我们可以将其放入Makefile中方便输入执行，即在顶层目录下，执行make即可执行测试：</p>
<pre><code>$make
go test ./... -addr localhost:30083 -json|go-test-report
[go-test-report] finished in 2.011443636s
</code></pre>
<p>如果要传入自定义的mqtt broker的服务地址，可以用：</p>
<pre><code>$make broker_addr=192.168.10.10:10083
</code></pre>
<h2>四. 小结</h2>
<p>在这篇文章中，我们介绍了如何实现基于go test驱动的自动化测试，介绍了这样的测试的结构布局、用例编写方法、执行与报告生成等。</p>
<p>这个方案的不足是<strong>要求测试用例所在环境需要部署go与go-test-report</strong>。</p>
<p>go test支持将test编译为一个可执行文件，不过不支持将多个包的测试编译为一个可执行文件：</p>
<pre><code>$go test -c ./...
cannot use -c flag with multiple packages
</code></pre>
<p>此外由于go test编译出的可执行文件<a href="https://github.com/golang/go/issues/22996">不支持将输出内容转换为JSON格式</a>，因此也无法对接go-test-report将测试结果保存在文件中供后续查看。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/automated-testing">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/tree/master/automated-testing</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/03/30/automated-testing-driven-by-go-test/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.20中值得关注的几个变化</title>
		<link>https://tonybai.com/2023/02/08/some-changes-in-go-1-20/</link>
		<comments>https://tonybai.com/2023/02/08/some-changes-in-go-1-20/#comments</comments>
		<pubDate>Wed, 08 Feb 2023 15:21:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[arena]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[benchstat]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[comparable]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[cover]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[ecdh]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.17]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[go1.19]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[GOCOVERDIR]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[loccount]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PGO]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[port]]></category>
		<category><![CDATA[riscv64]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[stdlib]]></category>
		<category><![CDATA[unit-test]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[vet]]></category>
		<category><![CDATA[wasi]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[WebAssembly]]></category>
		<category><![CDATA[wrap]]></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=3794</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/02/08/some-changes-in-go-1-20 美国时间2023年2月1日，唯一尚未退休的Go语言之父Robert Griesemer代表Go核心开发团队在Go官博撰文正式发布了Go 1.20版本。就像Russ Cox在2022 GopherCon大会所说的那样：Go2永不会到来，Go 1.x.y将无限延续！ 注：似乎新兴编程语言都喜欢停留在1.x.y上无限延续，譬如已经演化到1.67版本的Rust^_^。 在《Go，13周年》之后，Go 1.20新特性在开发主干冻结(2022.11)之前，我曾写过一篇《Go 1.20新特性前瞻》，对照着Go 1.20 milestone中内容，把我认为的主要特性和大家简单过了一遍，不过那时Go 1.20毕竟没有正式发布，前瞻肯定不够全面，某些具体的点与正式版本可能也有差异！现在Go 1.20版本正式发布了，其Release Notes也补充完整了，在这一篇中，我再来系统说说Go 1.20版本中值得关注的那些变化。对于在前瞻一文中详细介绍过的特性，这里不会再重复讲解了，大家参考前瞻一文中的内容即可。而对于其他一些特性，或是前瞻一文中着墨不多的特性，这里会挑重点展开说说。 按照惯例，我们依旧首先来看看Go语法层面都有哪些变化，这可能也是多数Gopher们最为关注的变化点。 一. 语法变化 Go秉持“大道至简”的理念，对Go语法特性向来是“不与时俱进”的。自从Go 1.18大刀阔斧的加入了泛型特性后，Go语法特性就又恢复到了之前的“新三年旧三年，缝缝补补又三年”的节奏。Go 1.20亦是如此啊！Release Notes说Go 1.20版本在语言方面包含了四点变化，但看了变化的内容后，我觉得真正的变化只有一个，其他的都是修修补补。 1. 切片到数组的转换 唯一算是真语法变化的特性是支持切片类型到数组类型(或数组类型的指针)的类型转换，这个特性在前瞻一文中系统讲过，这里就不赘述了，放个例子大家直观认知一下就可以了： // https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/slice2arr.go func slice2arrOK() { var sl = []int{1, 2, 3, 4, 5, 6, 7} var arr = [7]int(sl) var parr = (*[7]int)(sl) fmt.Println(sl) // [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-20-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20">本文永久链接</a> &#8211; https://tonybai.com/2023/02/08/some-changes-in-go-1-20</p>
<p>美国时间2023年2月1日，唯一尚未退休的Go语言之父<a href="https://github.com/griesemer">Robert Griesemer</a>代表Go核心开发团队在<a href="https://go.dev/blog/go1.20">Go官博撰文正式发布了Go 1.20版本</a>。就像<a href="https://www.youtube.com/watch?v=v24wrd3RwGo">Russ Cox在2022 GopherCon大会</a>所说的那样：<strong><a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">Go2永不会到来，Go 1.x.y将无限延续</a></strong>！</p>
<blockquote>
<p>注：似乎新兴编程语言都喜欢停留在1.x.y上无限延续，譬如已经<a href="https://www.rust-lang.org">演化到1.67版本的Rust</a>^_^。</p>
</blockquote>
<p>在<a href="https://tonybai.com/2022/11/11/go-opensource-13-years/">《Go，13周年》</a>之后，Go 1.20新特性在开发主干冻结(2022.11)之前，我曾写过一篇《<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">Go 1.20新特性前瞻</a>》，对照着<a href="https://github.com/golang/go/milestone/250">Go 1.20 milestone</a>中内容，把我认为的主要特性和大家简单过了一遍，不过那时Go 1.20毕竟没有正式发布，前瞻肯定不够全面，某些具体的点与正式版本可能也有差异！现在Go 1.20版本正式发布了，其<a href="https://go.dev/blog/go1.20">Release Notes</a>也补充完整了，在这一篇中，我再来系统说说Go 1.20版本中值得关注的那些变化。对于在<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">前瞻一文</a>中详细介绍过的特性，这里不会再重复讲解了，大家参考前瞻一文中的内容即可。而对于其他一些特性，或是前瞻一文中着墨不多的特性，这里会挑重点展开说说。</p>
<p>按照惯例，我们依旧首先来看看Go语法层面都有哪些变化，这可能也是多数Gopher们最为关注的变化点。</p>
<h2>一. 语法变化</h2>
<p>Go秉持“大道至简”的理念，对Go语法特性向来是“不与时俱进”的。自从<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18大刀阔斧的加入了泛型特性</a>后，Go语法特性就又恢复到了之前的“新三年旧三年，缝缝补补又三年”的节奏。Go 1.20亦是如此啊！Release Notes说Go 1.20版本在语言方面包含了四点变化，但看了变化的内容后，我觉得真正的变化只有一个，其他的都是修修补补。</p>
<h3>1. 切片到数组的转换</h3>
<p>唯一算是真语法变化的特性是支持<strong>切片类型到数组类型(或数组类型的指针)的类型转换</strong>，这个特性在<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">前瞻一文</a>中系统讲过，这里就不赘述了，放个例子大家直观认知一下就可以了：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/slice2arr.go

func slice2arrOK() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var arr = [7]int(sl)
    var parr = (*[7]int)(sl)
    fmt.Println(sl)  // [1 2 3 4 5 6 7]
    fmt.Println(arr) // [1 2 3 4 5 6 7]
    sl[0] = 11
    fmt.Println(arr)  // [1 2 3 4 5 6 7]
    fmt.Println(parr) // &amp;[11 2 3 4 5 6 7]
}

func slice2arrPanic() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    fmt.Println(sl)
    var arr = [8]int(sl) // panic: runtime error: cannot convert slice with length 7 to array or pointer to array with leng  th 8
    fmt.Println(arr)     // &amp;[11 2 3 4 5 6 7]

}

func main() {
    slice2arrOK()
    slice2arrPanic()
}
</code></pre>
<p>有两点注意一下就好：</p>
<ul>
<li>切片转换为数组类型的指针，那么该指针将指向切片的底层数组，就如同上面例子中slice2arrOK的parr变量那样；</li>
<li>转换的数组类型的长度不能大于原切片的长度(注意是长度而不是切片的容量哦)，否则在运行时会抛出panic。</li>
</ul>
<h3>2. 其他的修修补补</h3>
<ul>
<li>comparable“放宽”了对泛型实参的限制</li>
</ul>
<p>下面代码在Go 1.20版本之前，比如Go 1.19版本中会无法通过编译：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable.go

func doSth[T comparable](t T) {
}

func main() {
    n := 2
    var i interface{} = n // 编译错误：interface{} does not implement comparable
    doSth(i)
}
</code></pre>
<p>之前，comparable约束下的泛型形参需要支持严格可比较(strictly comparable)的类型作为泛型实参，哪些是严格可比较的类型呢？Go 1.20的语法规范做出了进一步澄清：如果一个类型是可比较的，且不是接口类型或由接口类型组成的类型，那么这个类型就是<strong>严格可比较的类型</strong>，包括：</p>
<pre><code>- 布尔型、数值类型、字符串类型、指针类型和channel是严格可比较的。
- 如果结构体类型的所有字段的类型都是严格可比较的，那么该结构体类型就是严格可比较的。
- 如果数组元素的类型是严格可比较的，那么该数组类型就是严格可比较的。
- 如果类型形参的类型集合中的所有类型都是严格可比较的，那么该类型形参就是严格可比较的。
</code></pre>
<p>我们看到：例外的就是接口类型了。接口类型不是“严格可比较的(strictly comparable)”，但未作为类型形参的接口类型是可比较的(comparable)，如果两个接口类型的动态类型相同且值相等，那么这两个接口类型就相等，或两个接口类型的值均为nil，它们也相等，否则不等。</p>
<p>Go 1.19版本及之前，作为非严格比较类型的接口类型是不能作为comparable约束的类型形参的类型实参的，就像上面comparable.go中示例代码那样，但Go 1.20版本开始，这一要求被防控，接口类型被允许作为类型实参赋值给comparable约束的类型形参了！不过这么做之前，你也要明确一点，如果像下面这样两个接口类型底层类型相同且是不可比较的类型（比如切片)，那么代码将在运行时抛panic：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable1.go

func doSth[T comparable](t1, t2 T) {
    if t1 != t2 {
        println("unequal")
        return
    }
    println("equal")
}

func main() {
    n1 := []byte{2}
    n2 := []byte{3}
    var i interface{} = n1
    var j interface{} = n2
    doSth(i, j) // panic: runtime error: comparing uncomparable type []uint8
}
</code></pre>
<p>Go 1.20语言规范借此机会还进一步澄清了结构体和数组两种类型比较实现的规范：对于结构体类型，Go会按照结构体字段的声明顺序，逐一字段进行比较，直到遇到第一个不相等的字段为止。如果没有不相等字段，则两个结构体字段相等；对于数组类型，Go会按数组元素的顺序，逐一元素进行比较，直到遇到第一个不相等的元素为止。如果没有不相等的元素，则两个数组相等。</p>
<ul>
<li>unsafe包继续添加“语法糖”</li>
</ul>
<p>继<a href="https://tonybai.com/2021/08/17/some-changes-in-go-1-17">Go 1.17版本</a>在unsafe包增加Slice函数后，Go 1.20版本又增加三个语法糖函数：SliceData、String和StringData：</p>
<pre><code>// $GOROOT/src/unsafe/unsafe.go
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte
</code></pre>
<p>值得注意的是由于string的不可更改性，String函数的参数ptr指向的内容以及StringData返回的指针指向的内容在String调用和StringData调用后不允许修改，但实际情况是怎么样的呢？</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/unsafe.go

func main() {
    var arr = [6]byte{'h', 'e', 'l', 'l', 'o', '!'}
    s := unsafe.String(&amp;arr[0], 6)
    fmt.Println(s) // hello!
    arr[0] = 'j'
    fmt.Println(s) // jello!

    b := unsafe.StringData(s)
    *b = 'k'
    fmt.Println(s) // kello!

    s1 := "golang"
    fmt.Println(s1) // golang
    b = unsafe.StringData(s1)
    *b = 'h' // fatal error: fault, unexpected fault address 0x10a67e5
    fmt.Println(s1)
}
</code></pre>
<p>我们看到：unsafe.String函数调用后，如果我们修改了传入的指针指向的内容，那么该改动会影响到后续返回的string内容！而StringData返回<br />
的指针所指向的内容一旦被修改，其结果要根据字符串的来源而定了。对于由可修改的底层数组“创建”的字符串(如s)，通过StringData返回的指<br />
针可以“修改”字符串的内容；而对于由字符串字面值初始化的字符串变量(如s1)，其内容是不可修改的(编译器将字符串底层存储分配在了只读数据区)，尝试通过指针修改指向内容，会导致运行时的段错误。</p>
<h2>二. 工具链</h2>
<h3>1. Go安装包“瘦身”</h3>
<p>这些年，Go发布版的安装包“体格”是越来越壮了，动辄100多MB的压缩包，以go.dev/dl页面上的go1.xy.linux-amd64.tar.gz为例，我们看看从Go 1.15版本到Go 1.19版本的“体格”变化趋势：</p>
<pre><code>Go 1.15 - 116MB
Go 1.16 - 123MB
Go 1.17 - 129MB
Go 1.18 - 135MB
Go 1.19 - 142MB
</code></pre>
<p>如果按此趋势，Go 1.20势必要上到150MB以上。但Go团队找到了“瘦身”方法，那就是：<a href="https://github.com/golang/go/issues/47257">从Go 1.20开始发行版的安装包不再为GOROOT中的软件包提供预编译的.a文件了</a>，这样我们得到的瘦身后的Go 1.20版本的size为<strong>95MB</strong>！相较于Go 1.19，Go 1.20的安装包“瘦”了三分之一。安装包解压后这种体现更为明显：</p>
<pre><code>➜  /Users/tonybai/.bin/go1.19 git:(master) ✗ $du -sh
495M    .
➜  /Users/tonybai/.bin/go1.20 git:(master) ✗ $du -sh
265M    .
</code></pre>
<p>我们看到：Go 1.20占用的磁盘空间仅为Go 1.19版本的一半多一点而已。 并且，Go 1.20版本中，GOROOT下的源码将像其他用户包那样在构建后被缓存到本机cache中。此外，go install也不会为GOROOT下的软件包安装.a文件。</p>
<h3>2. 编译器</h3>
<h4>1) PGO(profile-guided optimization)</h4>
<p>Go 1.20编译器的一个最大的变更点是引入了PGO优化技术预览版，这个在前瞻一文中也有<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">对PGO技术的简单介绍</a>。说白了点，PGO技术就是在原有compiler优化技术的基础上，针对程序在生产环境运行中的热点关键路径再进行一轮优化，并且针对热点代码执行路径，编译器会放开一些限制，比如<a href="https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example">Go决定是否对函数进行内联优化的复杂度上限默认值是80</a>，但对于PGO指示的关键热点路径，即便函数复杂性超过80很多，也可能会被inline优化掉。</p>
<p>之前持续性能剖析工具开发商Polar Signals曾发布一篇文章<a href="https://www.polarsignals.com/blog/posts/2022/09/exploring-go-profile-guided-optimizations/">《Exploring Go&#8217;s Profile-Guided Optimizations》</a>，专门探讨了PGO技术可能带来的优化效果，文章中借助了Go项目中自带的测试示例，这里也基于这个示例带大家重现一下。</p>
<p>我们使用的例子在Go 1.20源码/安装包的\$GOROOT/src/cmd/compile/internal/test/testdata/pgo/inline路径下：</p>
<pre><code>$ls -l
total 3156
-rw-r--r-- 1 tonybai tonybai    1698 Jan 31 05:46 inline_hot.go
-rw-r--r-- 1 tonybai tonybai     843 Jan 31 05:46 inline_hot_test.go
</code></pre>
<p>我们首先执行一下inline目录下的测试，并生成用于测试的可执行文件以及对应的cpu profile文件供后续PGO优化使用：</p>
<pre><code>$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof
goos: linux
goarch: amd64
pkg: cmd/compile/internal/test/testdata/pgo/inline
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkA-8        1348        870005 ns/op
PASS
ok      cmd/compile/internal/test/testdata/pgo/inline   1.413s
</code></pre>
<p>接下来，我们对比一下不使用PGO和使用PGO优化，Go编译器在内联优化上的区别：</p>
<pre><code>$diff &lt;(go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m" 2&gt;&amp;1 | grep "can inline") &lt;(go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m -pgoprofile inline_hot.pprof" 2&gt;&amp;1 | grep "can inline")
4a5,6
&gt; ./inline_hot.go:53:6: can inline (*BS).NS with cost 106 as: method(*BS) func(uint) (uint, bool) { x := int(i &gt;&gt; lWSize); if x &gt;= len(b.s) { return 0, false }; w := b.s[x]; w = w &gt;&gt; (i &amp; (wSize - 1)); if w != 0 { return i + T(w), true }; x = x + 1; for loop; return 0, false }
&gt; ./inline_hot.go:74:6: can inline A with cost 312 as: func() { s := N(100000); for loop; for loop }
</code></pre>
<blockquote>
<p>上面diff命令中为Go test命令传入-run=none -tags=”" -gcflags=”-m -m”是为了仅编译源文件，而不执行任何测试。</p>
</blockquote>
<p>我们看到，相较于未使用PGO优化的结果，PGO优化后的结果多了两个inline函数，这两个可以被inline的函数，一个的复杂度开销为106，一个是312，都超出了默认的80，但仍然可以被inline。</p>
<p>我们来看看PGO的实际优化效果，我们分为在无PGO优化与有PGO优化下执行100次benchmark，再用benchstat工具对比两次的结果：</p>
<pre><code>$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof -count=100 &gt; without_pgo.txt
$go test -o inline_hot.test -bench=. -gcflags="-pgoprofile inline_hot.pprof" -count=100 &gt; with_pgo.txt

$benchstat without_pgo.txt with_pgo.txt
goos: linux
goarch: amd64
pkg: cmd/compile/internal/test/testdata/pgo/inline
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
    │ without_pgo.txt │            with_pgo.txt             │
    │     sec/op      │   sec/op     vs base                │
A-8       874.7µ ± 0%   872.6µ ± 0%  -0.24% (p=0.024 n=100)
</code></pre>
<blockquote>
<p>注：benchstat的安装方法：\$go install golang.org/x/perf/cmd/benchstat@latest</p>
</blockquote>
<p>我们看到，在我的机器上(ubuntu 20.04 linux kerenel 5.4.0-132)，PGO针对这个测试的优化效果并不明显(仅仅有0.24%的提升)，Polar Signals原文中的提升幅度也不大，仅为1.05%。</p>
<p>Go官方Release Notes中提到benchmark提升效果为3%~4%，同时官方也提到了，这个仅仅是PGO初始技术预览版，后续会加强对PGO优化的投入，直至对多数程序产生较为明显的优化效果。个人觉得目前PGO尚处于早期，不建议在生产中使用。</p>
<p>Go官方也增加针对<a href="https://go.dev/doc/pgo">PGO的ref页面</a>，大家重点看看其中的FAQ，你会有更多收获！</p>
<h4>2) 构建速度</h4>
<p>Go 1.18泛型落地后，Go编译器的编译速度出现了回退(幅度15%)，Go 1.19编译速度也没有提升。虽然编译速度回退后依然可以“秒杀”竞争对手，但对于以编译速度快著称的Go来说，这个问题必须修复。Go 1.20做到了这一点，让Go编译器的编译速度重新回归到了Go 1.17的水准！相对Go 1.19提升10%左右。</p>
<p>我使用github.com/reviewdog/reviewdog这个库实测了一下，分别使用go 1.17.1、go 1.18.6、go 1.19.1和Go 1.20对这个module进行go build -a构建(之前将依赖包都下载本地，排除掉go get环节的影响)，结果如下：</p>
<pre><code>go 1.20：
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  48.01s user 7.96s system 536% cpu 10.433 total

go 1.19.1：
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  54.40s user 10.20s system 506% cpu 12.757 total

go 1.18.6：
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  53.78s user 9.85s system 545% cpu 11.654 total

go 1.17.1：
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  50.30s user 9.76s system 580% cpu 10.338 total
</code></pre>
<p>虽然不能十分精确，但总体上反映出各个版本的编译速度水准以及Go 1.20相对于Go 1.18和Go 1.19版本的提升。我们看到Go 1.20与Go 1.17版本在一个水平线上，甚至要超过Go 1.17(但可能仅限于我这个个例)。</p>
<h4>3) 允许在泛型函数/方法中进行类型声明</h4>
<p>Go 1.20版本之前下面代码是无法通过Go编译器的编译的：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/tools/compiler/local_type_decl.go
package main

func F[T1 any]() {
    type x struct{} // 编译错误：type declarations inside generic functions are not currently supported
    type y = x      // 编译错误：type declarations inside generic functions are not currently supported
}

func main() {
    F[int]()
}
</code></pre>
<p><a href="https://github.com/golang/go/issues/47631">Go 1.20改进了语言前端的实现</a>，通过unified IR实现了对在泛型函数/方法中进行类型声明(包括定义type alias)的支持。</p>
<p>同时，Go 1.20在<a href="https://go.dev/ref/spec#Type_parameter_declarations">spec</a>中还明确了<a href="https://github.com/golang/go/issues/40882">哪些使用了递归方式声明的类型形参列表是不合法的</a>：</p>
<pre><code>type T1[P T1[P]] …                    // 不合法: 形参列表中作为约束的T1引用了自己
type T2[P interface{ T2[int] }] …     // 不合法: 形参列表中作为约束的T2引用了自己
type T3[P interface{ m(T3[int])}] …   // 不合法: 形参列表中作为约束的T3引用了自己

type T4[P T5[P]] …                    // 不合法: 形参列表中，T4引用了T5 并且
type T5[P T4[P]] …                    //          T5引用了T4

type T6[P int] struct{ f *T6[P] }     // 正确: 虽然引用了T6，但这个引用发生在结构体定义中而不是形参列表中
</code></pre>
<h4>4) 构建自举源码的Go编译器的版本选择</h4>
<p>Go从Go 1.5版本开始实现自举，即使用Go实现Go，那么自举后的Go项目是谁来编译的呢？最初对应编译Go 1.5版本的Go编译器版本为Go 1.4。</p>
<p>以前从源码构建Go发行版，当未设置GOROOT_BOOTSTRAP时，编译脚本会默认使用Go 1.4，但如果有更高版本的Go编译器存在，会使用更高版本的编译器。</p>
<p>Go 1.18和Go 1.19会首先寻找是否有go 1.17版本，如果没有再使用go 1.4。</p>
<p>Go 1.20会寻找当前Go 1.17的最后一个版本Go 1.17.13，如果没有，则使用Go 1.4。</p>
<p>将来，Go核心团队计划一年升级一次构建自举源码的Go编译器的版本，例如：Go 1.22版本将使用Go 1.20版本的编译器。</p>
<h4>5) cgo</h4>
<p>Go命令现在在没有C工具链的系统上会默认禁用了cgo。更具体来说，当CGO_ENABLED环境变量未设置，CC环境变量未设置以及PATH环境变量中没有找到默认的C编译器（通常是clang或gcc）时，CGO_ENABLED会被默认设置为0。</p>
<h3>3. 其他工具</h3>
<h4>1) 支持采集应用执行的代码盖率</h4>
<p>在前瞻一文中，我提到过Go 1.20将对代码覆盖率的支持扩展到了应用整体层面，而不再仅仅是unit test。这里使用一个例子来看一下，究竟如何采集应用代码的执行覆盖率。我们以gitlab.com/esr/loccount这个代码统计工具为例，先修改一下Makefile，在go build后面加上-cover选项，然后编译loccount，并对其自身进行代码统计：</p>
<pre><code>// /home/tonybai/go/src/gitlab.com/loccount
$make
$mkdir mycovdata
$GOCOVERDIR=./mycovdata loccount .
all          SLOC=4279    (100.00%) LLOC=1213    in 110 files
Go           SLOC=1724    (40.29%)  LLOC=835     in 3 files
asciidoc     SLOC=752     (17.57%)  LLOC=0       in 5 files
C            SLOC=278     (6.50%)   LLOC=8       in 2 files
Python       SLOC=156     (3.65%)   LLOC=0       in 2 files
... ...
</code></pre>
<p>上面执行loccount之前，我们建立了一个mycovdata目录，并设置GOCOVERDIR的值为mycovdata目录的路径。在这样的上下文下，执行loccount后，mycovdata目录下会生成一些覆盖率统计数据文件：</p>
<pre><code>$ls mycovdata
covcounters.4ec45ce64f965e77563ecf011e110d4f.926594.1675678144659536943  covmeta.4ec45ce64f965e77563ecf011e110d4f
</code></pre>
<p>怎么查看loccount的执行覆盖率呢？我们使用go tool covdata来查看：</p>
<pre><code>$go tool covdata percent -i=mycovdata
    loccount    coverage: 69.6% of statements
</code></pre>
<p>当然, covdata子命令还支持其他一些功能，大家可以自行查看manual挖掘。</p>
<h4>2) vet</h4>
<p>Go 1.20版本中，go工具链的vet子命令增加了两个十分实用的检测：</p>
<ul>
<li>对loopclosure这一检测策略进行了增强</li>
</ul>
<p>具体可参见https://github.com/golang/tools/tree/master/go/analysis/passes/loopclosure代码</p>
<ul>
<li>增加对2006-02-01的时间格式的检查</li>
</ul>
<p>注意我们使用time.Format或Parse时，最常使用的是2006-01-02这样的格式，即ISO 8601标准的时间格式，但一些代码中总是出现2006-02-01，十分容易导致错误。这个版本中，go vet将会对此种情况进行检查。</p>
<h2>三. 运行时与标准库</h2>
<h3>1. 运行时(runtime)</h3>
<p>Go 1.20运行时的调整并不大，仅对GC的内部数据结构进行了微调，这个调整可以获得最多2%的内存开销下降以及cpu性能提升。</p>
<h3>2. 标准库</h3>
<p>标准库肯定是变化最多的那部分。前瞻一文中对下面变化也做了详细介绍，这里不赘述了，大家可以翻看那篇文章细读：</p>
<ul>
<li>支持wrap multiple errors</li>
<li>time包新增DateTime、DateOnly和TimeOnly三个layout格式常量</li>
<li>新增arena包<br />
&#8230; &#8230;</li>
</ul>
<p>标准库变化很多，这里不能一一罗列，再补充一些我认为重要的，其他的变化大家可以到<a href="https://go.dev/doc/go1.20">Go 1.20 Release Notes</a>去看：</p>
<h4>1) arena包</h4>
<p>前瞻一文已经对arena包做了简要描述，对于arena包的使用以及最佳适用场合的探索还在进行中。著名持续性能剖析工具<a href="https://pyroscope.io/">pyroscope</a>的官方博客文章<a href="https://pyroscope.io/blog/go-1-20-memory-arenas/">《Go 1.20 arenas实践：arena vs. 传统内存管理》</a>对于arena实验特性的使用给出了几点好的建议，比如：</p>
<ul>
<li>只在关键的代码路径中使用arena，不要到处使用它们</li>
<li>在使用arena之前和之后对你的代码进行profiling，以确保你在能提供最大好处的地方添加arena。</li>
<li>密切关注arena上创建的对象的生命周期。确保你不会把它们泄露给你程序中的其他组件，因为那里的对象可能会超过arena的寿命。</li>
<li>使用defer a.Free()来确保你不会忘记释放内存。</li>
<li>如果你想在arena被释放后使用对象，使用arena.Clone()将其克隆回heap中。</li>
</ul>
<p>pyroscope的开发人员认为arena是一个强大的工具，也支持标准库中保留arena这个特性，但也建议将arena和reflect、unsafe、cgo等一样纳入“不推荐”使用的包行列。这点我也是赞同的。我也在考虑如何基于arena改进我们产品的协议解析器的性能，有成果后，我也会将实践过程分享出来的。</p>
<h4>2) 新增crypto/ecdh包</h4>
<p>密码学包(crypto)的主要maintainer <a href="https://filippo.io/">Filippo Valsorda</a>从google离职后，<a href="https://words.filippo.io/full-time-maintainer/">成为了一名专职开源项目维护者</a>。这似乎让其更有精力和动力对crypto包进行更好的规划、设计和实现了。<a href="https://github.com/golang/go/issues/52221">crypto/ecdh包就是在他的提议下加入到Go标准库中的</a>。</p>
<p>相对于标准库之前存在的crypto/elliptic等包，crypto/ecdh包的API更为高级，Go官方推荐使用ecdh的高级API，这样大家以后可以不必再与低级的密码学函数斗争了。</p>
<h4>3) HTTP ResponseController</h4>
<p>以前HTTP handler的超时都是http服务器全局指定一个的：包括ReadTimeout和WriteTimeout。但有些时候，如果能在某个请求范围内支持这些超时（以及可能的其他选项）将非常有用。Damien Neil就创建了这个<a href="https://github.com/golang/go/issues/54136">增加ResponseController的提案</a>，下面是一个在HandlerFunc中使用ResponseController的例子：</p>
<pre><code>http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
  ctl := http.NewResponseController(w, r)
  ctl.SetWriteDeadline(time.Now().Add(1 * time.Minute)) // 仅为这个请求设置deadline
  fmt.Fprintln(w, "Hello, world.") // 这个写入的timeout为1-minute
})
</code></pre>
<h4>4) context包增加WithCancelCause函数</h4>
<p>context包新增了一个WithCancelCause函数，与WithCancel不同，通过WithCancelCause返回的Context，我们可以得到cancel的原因，比如下面示例：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/library/context.go

func main() {
    myError := fmt.Errorf("%s", "myError")
    ctx, cancel := context.WithCancelCause(context.Background())
    cancel(myError)
    fmt.Println(ctx.Err())          // context.Canceled
    fmt.Println(context.Cause(ctx)) // myError
}
</code></pre>
<p>我们看到通过context.Cause可以得到Context在cancel时传入的错误原因。</p>
<h2>四. 移植性</h2>
<p>Go对新cpu体系结构和OS的支持向来是走在前面的。Go 1.20还新增了对freebsd在risc-v上的实验性支持，其环境变量为GOOS=freebsd, GOARCH=riscv64。但Go 1.20也将成为对下面平台提供支持的最后一个Go版本：</p>
<ul>
<li>Windows 7, 8, Server 2008和Server 2012</li>
<li>MacOS 10.13 High Sierra和10.14 (我的安装了10.14的mac os又要在go 1.21不被支持了^_^) </li>
</ul>
<p>近期Go团队又有了新提案：<a href="https://github.com/golang/go/issues/58141">支持WASI(GOOS=wasi GOARCH=wasm)</a>，WASI是啥，它是WebAssembly一套与引擎无关(engine-indepent)的、面向非Web系统的WASM API标准，是WebAssembly脱离浏览器的必经之路！一旦生成满足WASI的WASM程序，该程序就可以在任何支持WASI或兼容的runtime上运行。不出意外，该提案将在Go 1.21或Go 1.22版本落地。</p>
<p>本文中的示例代码可以在<a href="https://github.com/bigwhite/experiments/blob/master/go1.20-examples">这里</a>下载。</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/08/some-changes-in-go-1-20/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Go开发Kubernetes Operator：基本结构</title>
		<link>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/</link>
		<comments>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/#comments</comments>
		<pubDate>Mon, 15 Aug 2022 14:47:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[controller]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[CR]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[CustomResourceDefinition]]></category>
		<category><![CDATA[DaemonSet]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[framework]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubebuilder]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[operator]]></category>
		<category><![CDATA[operator-framework]]></category>
		<category><![CDATA[operator-sdk]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[reconcile]]></category>
		<category><![CDATA[reconciliation]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[replicas]]></category>
		<category><![CDATA[ReplicaSet]]></category>
		<category><![CDATA[resource]]></category>
		<category><![CDATA[role]]></category>
		<category><![CDATA[role-binding]]></category>
		<category><![CDATA[scale]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[service-account]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[TPR]]></category>
		<category><![CDATA[webserver]]></category>
		<category><![CDATA[伸缩]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3638</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1 注：文章首图基于《Kubernetes Operators Explained》修改 几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像Operator这样的应用模式和扩展方式日益受到开发者与运维者的欢迎。 我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的拿手好戏，是时候来研究一下operator了。 一. Operator的优点 kubernetes operator的概念最初来自CoreOS &#8211; 一家被红帽(redhat)收购的容器技术公司。 CoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：etcd operator和prometheus operator。 注：etcd于2013年由CoreOS以开源形式发布；prometheus作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。 下面是CoreOS对Operator这一概念的诠释：Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序。 图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档) Operator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。 那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比： 通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。 我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。 在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。 我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。 不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面： 对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入； 从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：kubebuilder、operator framework sdk等； operator的能力也有高低之分，operator framework就提出了一个包含五个等级的operator能力模型(CAPABILITY MODEL)，见下图。使用Go开发高能力等级的operator需要对client-go这个kubernetes官方go client库中的API有深入的了解。 图：operator能力模型(截图来自operator framework官网) 当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。 二. Kubernetes resource、resource type、API和controller介绍 Kubernetes发展到今天，其本质已经显现： Kubernetes就是一个“数据库”(数据实际持久存储在etcd中)； 其API就是“sql语句”； API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)； 每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)； 每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)； [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1">本文永久链接</a> &#8211; https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1</p>
<blockquote>
<p>注：文章首图基于《Kubernetes Operators Explained》修改</p>
</blockquote>
<p><a href="https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/">几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准</a>，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/">Operator这样的应用模式和扩展方式</a>日益受到开发者与运维者的欢迎。</p>
<p>我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的<strong>拿手好戏</strong>，是时候来研究一下operator了。</p>
<h3>一. Operator的优点</h3>
<p><a href="https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html">kubernetes operator的概念最初来自CoreOS</a> &#8211; 一家被红帽(redhat)收购的容器技术公司。</p>
<p>CoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：<a href="https://web.archive.org/web/20170224100544/https://coreos.com/blog/introducing-the-etcd-operator.html">etcd operator</a>和<a href="https://web.archive.org/web/20170224101137/https://coreos.com/blog/the-prometheus-operator.html">prometheus operator</a>。</p>
<blockquote>
<p>注：<a href="https://etcd.io">etcd</a>于2013年由CoreOS以开源形式发布；<a href="https://prometheus.io">prometheus</a>作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。</p>
</blockquote>
<p>下面是CoreOS对Operator这一概念的诠释：<strong>Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-4.png" alt="" /><br />
<center>图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档)</center></p>
<p>Operator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。</p>
<p>那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-2.png" alt="" /></p>
<p>通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。</p>
<p>我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。</p>
<p>在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。</p>
<p>我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。</p>
<p>不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面：</p>
<ul>
<li>对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入；</li>
<li>从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：<a href="https://github.com/kubernetes-sigs/kubebuilder">kubebuilder</a>、<a href="https://sdk.operatorframework.io">operator framework sdk</a>等；</li>
<li>operator的能力也有高低之分，operator framework就提出了一个包含<strong>五个等级的operator能力模型(CAPABILITY MODEL)</strong>，见下图。使用Go开发高能力等级的operator需要对<a href="https://github.com/kubernetes/client-go">client-go</a>这个kubernetes官方go client库中的API有深入的了解。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-3.png" alt="" /><br />
<center>图：operator能力模型(截图来自operator framework官网)</center></p>
<p>当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。</p>
<h3>二. Kubernetes resource、resource type、API和controller介绍</h3>
<p>Kubernetes发展到今天，其本质已经显现：</p>
<ul>
<li>Kubernetes就是一个“数据库”(数据实际持久存储在etcd中)；</li>
<li>其API就是“sql语句”；</li>
<li>API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)；</li>
<li>每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)；</li>
<li>每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)；</li>
<li>Kubernetes这个“数据库”内置了很多“表”，比如Pod、Deployment、DaemonSet、ReplicaSet等；</li>
</ul>
<p>下面是一个Kubernetes API与resource关系的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-5.png" alt="" /></p>
<p>我们看到resource type有两类，一类的namespace相关的(namespace-scoped)，我们通过下面形式的API操作这类resource type的实例：</p>
<pre><code>VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE - 操作某特定namespace下面的resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME - 操作某特定namespace下面的resource type中的某个具体的resource实例
</code></pre>
<p>另外一类则是namespace无关，即cluster范围(cluster-scoped)的，我们通过下面形式的API对这类resource type的实例进行操作：</p>
<pre><code>VERB /apis/GROUP/VERSION/RESOURCETYPE - 操作resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/RESOURCETYPE/NAME - 操作resource type中的某个具体的resource实例
</code></pre>
<p>我们知道Kubernetes并非真的只是一个“数据库”，它是服务编排和容器调度的平台标准，它的基本调度单元是Pod(也是一个resource type)，即一组容器的集合。那么Pod又是如何被创建、更新和删除的呢？这就离不开控制器(controller)了。<strong>每一类resource type都有自己对应的控制器(controller)</strong>。以pod这个resource type为例，它的controller为ReplicasSet的实例。</p>
<p>控制器的运行逻辑如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-6.png" alt="" /><br />
<center>图：控制器运行逻辑(引自《Kubernetes Operators Explained》一文)</center></p>
<p>控制器一旦启动，将尝试获得resource的当前状态(current state)，并与存储在k8s中的resource的期望状态（desired state，即spec)做比对，如果不一致，controller就会调用相应API进行调整，尽力使得current state与期望状态达成一致。这个达成一致的过程被称为<strong>协调(reconciliation)</strong>，协调过程的伪代码逻辑如下：</p>
<pre><code>for {
    desired := getDesiredState()
    current := getCurrentState()
    makeChanges(desired, current)
}
</code></pre>
<blockquote>
<p>注：k8s中有一个object的概念？那么object是什么呢？它类似于Java Object基类或Ruby中的Object超类。不仅resource type的实例resource是一个(is-a)object，resource type本身也是一个object，它是kubernetes concept的实例。</p>
</blockquote>
<p>有了上面对k8s这些概念的初步理解，我们下面就来理解一下Operator究竟是什么！</p>
<h3>三. Operator模式 = 操作对象(CRD) + 控制逻辑(controller)</h3>
<p>如果让运维人员直面这些内置的resource type(如deployment、pod等)，也就是前面“使用operator vs. 不使用operator”对比图中的第二种情况, 运维人员面临的情况将会很复杂，且操作易错。</p>
<p>那么如果不直面内置的resource type，那么我们如何自定义resource type呢, Kubernetes提供了Custom Resource Definition，CRD(在coreos刚提出operator概念的时候，crd的前身是Third Party Resource, TPR)可以用于自定义resource type。</p>
<p>根据前面我们对resource type理解，定义CRD相当于建立新“表”(resource type)，一旦CRD建立，k8s会为我们自动生成对应CRD的API endpoint，我们就可以通过yaml或API来操作这个“表”。我们可以向“表”中“插入”数据，即基于CRD创建Custom Resource(CR)，这就好比我们创建Deployment实例，向Deployment“表”中插入数据一样。</p>
<p>和原生内置的resource type一样，光有存储对象状态的CR还不够，原生resource type有对应controller负责协调(reconciliation)实例的创建、伸缩与删除，CR也需要这样的“协调者”，即我们也需要定义一个controller来负责监听CR状态并管理CR创建、伸缩、删除以及保持期望状态(spec)与当前状态(current state)的一致。这个controller不再是面向原生Resource type的实例，而是<strong>面向CRD的实例CR的controller</strong>。</p>
<p>有了自定义的操作对象类型(CRD)，有了面向操作对象类型实例的controller，我们将其打包为一个概念：“Operator模式”，operator模式中的controller也被称为operator，它是在集群中对CR进行维护操作的主体。</p>
<h3>四. 使用kubebuilder开发webserver operator</h3>
<blockquote>
<p>假设：此时你的本地开发环境已经具备访问实验用k8s环境的一切配置，通过kubectl工具可以任意操作k8s。</p>
</blockquote>
<p><strong>再深入浅出的概念讲解都不如一次实战对理解概念更有帮助</strong>，下面我们就来开发一个简单的Operator。</p>
<p>前面提过operator开发非常verbose，因此社区提供了开发工具和框架来帮助开发人员简化开发过程，目前主流的包括operator framework sdk和kubebuilder，前者是redhat开源并维护的一套工具，支持使用go、ansible、helm进行operator开发(其中只有go可以开发到能力级别5的operator，其他两种则不行)；而kubebuilder则是kubernetes官方的一个sig(特别兴趣小组)维护的operator开发工具。目前基于operator framework sdk和go进行operator开发时，operator sdk底层使用的也是kubebuilder，所以这里我们就直接使用kubebuilder来开发operator。</p>
<p>按照operator能力模型，我们这个operator差不多处于2级这个层次，我们定义一个Webserver的resource type，它代表的是一个基于nginx的webserver集群，我们的operator支持创建webserver示例(一个nginx集群)，支持nginx集群伸缩，支持集群中nginx的版本升级。</p>
<p>下面我们就用kubebuilder来实现这个operator！</p>
<h4>1. 安装kubebuilder</h4>
<p>这里我们采用源码构建方式安装，步骤如下：</p>
<pre><code>$git clone git@github.com:kubernetes-sigs/kubebuilder.git
$cd kubebuilder
$make
$cd bin
$./kubebuilder version
Version: main.version{KubeBuilderVersion:"v3.5.0-101-g5c949c2e",
KubernetesVendor:"unknown",
GitCommit:"5c949c2e50ca8eec80d64878b88e1b2ee30bf0bc",
BuildDate:"2022-08-06T09:12:50Z", GoOs:"linux", GoArch:"amd64"}
</code></pre>
<p>然后将bin/kubebuilder拷贝到你的PATH环境变量中的某个路径下即可。</p>
<h4>2. 创建webserver-operator工程</h4>
<p>接下来，我们就可以使用kubebuilder创建webserver-operator工程了：</p>
<pre><code>$mkdir webserver-operator
$cd webserver-operator
$kubebuilder init  --repo github.com/bigwhite/webserver-operator --project-name webserver-operator

Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.12.2
go: downloading k8s.io/client-go v0.24.2
go: downloading k8s.io/component-base v0.24.2
Update dependencies:
$ go mod tidy
Next: define a resource with:
kubebuilder create api
</code></pre>
<blockquote>
<p>注：&#8211;repo指定go.mod中的module root path，你可以定义你自己的module root path。</p>
</blockquote>
<h4>3. 创建API，生成初始CRD</h4>
<p>Operator包括CRD和controller，这里我们就来建立自己的CRD，即自定义的resource type，也就是API的endpoint，我们使用下面kubebuilder create命令来完成这个步骤：</p>
<pre><code>$kubebuilder create api --version v1 --kind WebServer
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/webserver_types.go
controllers/webserver_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
</code></pre>
<p>之后，我们执行make manifests来生成最终CRD对应的yaml文件：</p>
<pre><code>$make manifests
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
</code></pre>
<p>此刻，整个工程的目录文件布局如下：</p>
<pre><code>$tree -F .
.
├── api/
│   └── v1/
│       ├── groupversion_info.go
│       ├── webserver_types.go
│       └── zz_generated.deepcopy.go
├── bin/
│   └── controller-gen*
├── config/
│   ├── crd/
│   │   ├── bases/
│   │   │   └── my.domain_webservers.yaml
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches/
│   │       ├── cainjection_in_webservers.yaml
│   │       └── webhook_in_webservers.yaml
│   ├── default/
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager/
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus/
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac/
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── role_binding.yaml
│   │   ├── role.yaml
│   │   ├── service_account.yaml
│   │   ├── webserver_editor_role.yaml
│   │   └── webserver_viewer_role.yaml
│   └── samples/
│       └── _v1_webserver.yaml
├── controllers/
│   ├── suite_test.go
│   └── webserver_controller.go
├── Dockerfile
├── go.mod
├── go.sum
├── hack/
│   └── boilerplate.go.txt
├── main.go
├── Makefile
├── PROJECT
└── README.md

14 directories, 40 files
</code></pre>
<h4>4. webserver-operator的基本结构</h4>
<p>忽略我们此次不关心的诸如leader election、auth_proxy等，我将这个operator例子的主要部分整理到下面这张图中：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-7.png" alt="" /></p>
<p>图中的各个部分就是使用kubebuilder生成的<strong>operator的基本结构</strong>。</p>
<p>webserver operator主要由CRD和controller组成：</p>
<ul>
<li>CRD</li>
</ul>
<p>图中的左下角的框框就是上面生成的CRD yaml文件：config/crd/bases/my.domain_webservers.yaml。CRD与api/v1/webserver_types.go密切相关。我们在api/v1/webserver_types.go中为CRD定义spec相关字段，之后make manifests命令可以解析webserver_types.go中的变化并更新CRD的yaml文件。</p>
<ul>
<li>controller</li>
</ul>
<p>从图的右侧部分可以看出，controller自身就是作为一个deployment部署在k8s集群中运行的，它监视CRD的实例CR的运行状态，并在Reconcile方法中检查预期状态与当前状态是否一致，如果不一致，则执行相关操作。</p>
<ul>
<li>其它</li>
</ul>
<p>图中左上角是有关controller的权限的设置，controller通过serviceaccount访问k8s API server，通过role.yaml和role_binding.yaml设置controller的角色和权限。</p>
<h4>5. 为CRD spec添加字段(field)</h4>
<p>为了实现Webserver operator的功能目标，我们需要为CRD spec添加一些状态字段。前面说过，CRD与api中的webserver_types.go文件是同步的，我们只需修改webserver_types.go文件即可。我们在WebServerSpec结构体中增加Replicas和Image两个字段，它们分别用于表示webserver实例的副本数量以及使用的容器镜像：</p>
<pre><code>// api/v1/webserver_types.go

// WebServerSpec defines the desired state of WebServer
type WebServerSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // The number of replicas that the webserver should have
    Replicas int `json:"replicas,omitempty"`

    // The container image of the webserver
    Image string `json:"image,omitempty"`

    // Foo is an example field of WebServer. Edit webserver_types.go to remove/update
    Foo string `json:"foo,omitempty"`
}
</code></pre>
<p>保存修改后，<strong>执行make manifests</strong>重新生成config/crd/bases/my.domain_webservers.yaml</p>
<pre><code>$cat my.domain_webservers.yaml
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.9.2
  creationTimestamp: null
  name: webservers.my.domain
spec:
  group: my.domain
  names:
    kind: WebServer
    listKind: WebServerList
    plural: webservers
    singular: webserver
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: WebServer is the Schema for the webservers API
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            description: WebServerSpec defines the desired state of WebServer
            properties:
              foo:
                description: Foo is an example field of WebServer. Edit webserver_types.go
                  to remove/update
                type: string
              image:
                description: The container image of the webserver
                type: string
              replicas:
                description: The number of replicas that the webserver should have
                type: integer
            type: object
          status:
            description: WebServerStatus defines the observed state of WebServer
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}
</code></pre>
<p>一旦定义完CRD，我们就可以将其安装到k8s中：</p>
<pre><code>$make install
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
{Version:kustomize/v3.8.7 GitCommit:ad092cc7a91c07fdf63a2e4b7f13fa588a39af4f BuildDate:2020-11-11T23:14:14Z GoOs:linux GoArch:amd64}
kustomize installed to /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain created
</code></pre>
<p>检查安装情况：</p>
<pre><code>$kubectl get crd|grep webservers
webservers.my.domain                                             2022-08-06T21:55:45Z
</code></pre>
<h4>6. 修改role.yaml</h4>
<p>在开始controller开发之前，我们先来为controller后续的运行“铺平道路”，即设置好相应权限。</p>
<p>我们在controller中会为CRD实例创建对应deployment和service，这样就要求controller有操作deployments和services的权限，这样就需要我们修改role.yaml，增加service account:  controller-manager 操作deployments和services的权限：</p>
<pre><code>// config/rbac/role.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  creationTimestamp: null
  name: manager-role
rules:
- apiGroups:
  - my.domain
  resources:
  - webservers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - my.domain
  resources:
  - webservers/finalizers
  verbs:
  - update
- apiGroups:
  - my.domain
  resources:
  - webservers/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - apps
  - ""
  resources:
  - services
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
</code></pre>
<p>修改后的role.yaml先放在这里，后续与controller一并部署到k8s上。</p>
<h4>7. 实现controller的Reconcile(协调)逻辑</h4>
<p>kubebuilder为我们搭好了controller的代码架子，我们只需要在controllers/webserver_controller.go中实现WebServerReconciler的Reconcile方法即可。下面是Reconcile的一个简易流程图，结合这幅图理解代码就容易的多了：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-8.png" alt="" /></p>
<p>下面是对应的Reconcile方法的代码：</p>
<pre><code>// controllers/webserver_controller.go

func (r *WebServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := r.Log.WithValues("Webserver", req.NamespacedName)

    instance := &amp;mydomainv1.WebServer{}
    err := r.Get(ctx, req.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Return and don't requeue
            log.Info("Webserver resource not found. Ignoring since object must be deleted")
            return ctrl.Result{}, nil
        }

        // Error reading the object - requeue the request.
        log.Error(err, "Failed to get Webserver")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Check if the webserver deployment already exists, if not, create a new one
    found := &amp;appsv1.Deployment{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, found)
    if err != nil &amp;&amp; errors.IsNotFound(err) {
        // Define a new deployment
        dep := r.deploymentForWebserver(instance)
        log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
        err = r.Create(ctx, dep)
        if err != nil {
            log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Deployment created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Deployment")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Ensure the deployment replicas and image are the same as the spec
    var replicas int32 = int32(instance.Spec.Replicas)
    image := instance.Spec.Image

    var needUpd bool
    if *found.Spec.Replicas != replicas {
        log.Info("Deployment spec.replicas change", "from", *found.Spec.Replicas, "to", replicas)
        found.Spec.Replicas = &amp;replicas
        needUpd = true
    }

    if (*found).Spec.Template.Spec.Containers[0].Image != image {
        log.Info("Deployment spec.template.spec.container[0].image change", "from", (*found).Spec.Template.Spec.Containers[0].Image, "to", image)
        found.Spec.Template.Spec.Containers[0].Image = image
        needUpd = true
    }

    if needUpd {
        err = r.Update(ctx, found)
        if err != nil {
            log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Spec updated - return and requeue
        return ctrl.Result{Requeue: true}, nil
    }

    // Check if the webserver service already exists, if not, create a new one
    foundService := &amp;corev1.Service{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name + "-service", Namespace: instance.Namespace}, foundService)
    if err != nil &amp;&amp; errors.IsNotFound(err) {
        // Define a new service
        srv := r.serviceForWebserver(instance)
        log.Info("Creating a new Service", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
        err = r.Create(ctx, srv)
        if err != nil {
            log.Error(err, "Failed to create new Servie", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Service created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Service")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Tbd: Ensure the service state is the same as the spec, your homework

    // reconcile webserver operator in again 10 seconds
    return ctrl.Result{RequeueAfter: time.Second * 10}, nil
}
</code></pre>
<p>这里大家可能发现了：<strong>原来CRD的controller最终还是将CR翻译为k8s原生Resource，比如service、deployment等。CR的状态变化(比如这里的replicas、image等)最终都转换成了deployment等原生resource的update操作</strong>，这就是operator的精髓！理解到这一层，operator对大家来说就不再是什么密不可及的概念了。</p>
<p>有些朋友可能也会发现，上面流程图中似乎没有考虑CR实例被删除时对deployment、service的操作，的确如此。不过对于一个7&#215;24小时运行于后台的服务来说，我们更多关注的是其变更、伸缩、升级等操作，删除是优先级最低的需求。</p>
<h4>8. 构建controller image</h4>
<p>controller代码写完后，我们就来构建controller的image。通过前文我们知道，这个controller其实就是运行在k8s中的一个deployment下的pod。我们需要构建其image并通过deployment部署到k8s中。</p>
<p>kubebuilder创建的operator工程中包含了Makefile，通过make docker-build即可构建controller image。docker-build使用golang builder image来构建controller源码，不过如果不对Dockerfile稍作修改，你很难编译过去，因为默认GOPROXY在国内无法访问。这里最简单的改造方式是使用vendor构建，下面是改造后的Dockerfile：</p>
<pre><code># Build the manager binary
FROM golang:1.18 as builder

ENV GOPROXY https://goproxy.cn
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
COPY vendor/ vendor/
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
#RUN go mod download

# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
#FROM gcr.io/distroless/static:nonroot
FROM katanomi/distroless-static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]
</code></pre>
<p>下面是构建的步骤：</p>
<pre><code>$go mod vendor
$make docker-build

test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
KUBEBUILDER_ASSETS="/home/tonybai/.local/share/kubebuilder-envtest/k8s/1.24.2-linux-amd64" go test ./... -coverprofile cover.out
?       github.com/bigwhite/webserver-operator    [no test files]
?       github.com/bigwhite/webserver-operator/api/v1    [no test files]
ok      github.com/bigwhite/webserver-operator/controllers    4.530s    coverage: 0.0% of statements
docker build -t bigwhite/webserver-controller:latest .
Sending build context to Docker daemon  47.51MB
Step 1/15 : FROM golang:1.18 as builder
 ---&gt; 2d952adaec1e
Step 2/15 : ENV GOPROXY https://goproxy.cn
 ---&gt; Using cache
 ---&gt; db2b06a078e3
Step 3/15 : WORKDIR /workspace
 ---&gt; Using cache
 ---&gt; cc3c613c19c6
Step 4/15 : COPY go.mod go.mod
 ---&gt; Using cache
 ---&gt; 5fa5c0d89350
Step 5/15 : COPY go.sum go.sum
 ---&gt; Using cache
 ---&gt; 71669cd0fe8e
Step 6/15 : COPY vendor/ vendor/
 ---&gt; Using cache
 ---&gt; 502b280a0e67
Step 7/15 : COPY main.go main.go
 ---&gt; Using cache
 ---&gt; 0c59a69091bb
Step 8/15 : COPY api/ api/
 ---&gt; Using cache
 ---&gt; 2b81131c681f
Step 9/15 : COPY controllers/ controllers/
 ---&gt; Using cache
 ---&gt; e3fd48c88ccb
Step 10/15 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go
 ---&gt; Using cache
 ---&gt; 548ac10321a2
Step 11/15 : FROM katanomi/distroless-static:nonroot
 ---&gt; 421f180b71d8
Step 12/15 : WORKDIR /
 ---&gt; Running in ea7cb03027c0
Removing intermediate container ea7cb03027c0
 ---&gt; 9d3c0ea19c3b
Step 13/15 : COPY --from=builder /workspace/manager .
 ---&gt; a4387fe33ab7
Step 14/15 : USER 65532:65532
 ---&gt; Running in 739a32d251b6
Removing intermediate container 739a32d251b6
 ---&gt; 52ae8742f9c5
Step 15/15 : ENTRYPOINT ["/manager"]
 ---&gt; Running in 897893b0c9df
Removing intermediate container 897893b0c9df
 ---&gt; e375cc2adb08
Successfully built e375cc2adb08
Successfully tagged bigwhite/webserver-controller:latest
</code></pre>
<blockquote>
<p>注：执行make命令之前，先将Makefile中的IMG变量初值改为IMG ?= bigwhite/webserver-controller:latest</p>
</blockquote>
<p>构建成功后，执行make docker-push将image推送到镜像仓库中(这里使用了docker公司提供的公共仓库)。</p>
<h4>9. 部署controller</h4>
<p>之前我们已经通过make install将CRD安装到k8s中了，接下来再把controller部署到k8s上，我们的operator就算部署完毕了。执行make deploy即可实现部署：</p>
<pre><code>$make deploy
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
cd config/manager &amp;&amp; /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize edit set image controller=bigwhite/webserver-controller:latest
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/default | kubectl apply -f -
namespace/webserver-operator-system created
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain unchanged
serviceaccount/webserver-operator-controller-manager created
role.rbac.authorization.k8s.io/webserver-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/webserver-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/webserver-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-proxy-rolebinding created
configmap/webserver-operator-manager-config created
service/webserver-operator-controller-manager-metrics-service created
deployment.apps/webserver-operator-controller-manager created
</code></pre>
<p>我们看到deploy不仅会安装controller、serviceaccount、role、rolebinding，它还会创建namespace，也会将crd安装一遍。也就是说deploy是一个完整的operator安装命令。</p>
<blockquote>
<p>注：使用make undeploy可以完整卸载operator相关resource。</p>
</blockquote>
<p>我们用kubectl logs查看一下controller的运行日志：</p>
<pre><code>$kubectl logs -f deployment.apps/webserver-operator-controller-manager -n webserver-operator-system
1.6600280818476188e+09    INFO    controller-runtime.metrics    Metrics server is starting to listen    {"addr": "127.0.0.1:8080"}
1.6600280818478029e+09    INFO    setup    starting manager
1.6600280818480284e+09    INFO    Starting server    {"path": "/metrics", "kind": "metrics", "addr": "127.0.0.1:8080"}
1.660028081848097e+09    INFO    Starting server    {"kind": "health probe", "addr": "[::]:8081"}
I0809 06:54:41.848093       1 leaderelection.go:248] attempting to acquire leader lease webserver-operator-system/63e5a746.my.domain...
I0809 06:54:57.072336       1 leaderelection.go:258] successfully acquired lease webserver-operator-system/63e5a746.my.domain
1.6600280970724037e+09    DEBUG    events    Normal    {"object": {"kind":"Lease","namespace":"webserver-operator-system","name":"63e5a746.my.domain","uid":"e05aaeb5-4a3a-4272-b036-80d61f0b6788","apiVersion":"coordination.k8s.io/v1","resourceVersion":"5238800"}, "reason": "LeaderElection", "message": "webserver-operator-controller-manager-6f45bc88f7-ptxlc_0e960015-9fbe-466d-a6b1-ff31af63a797 became leader"}
1.6600280970724993e+09    INFO    Starting EventSource    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "source": "kind source: *v1.WebServer"}
1.6600280970725305e+09    INFO    Starting Controller    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer"}
1.660028097173026e+09    INFO    Starting workers    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "worker count": 1}
</code></pre>
<p>可以看到，controller已经成功启动，正在等待一个WebServer CR的相关事件(比如创建)！下面我们就来创建一个WebServer CR!</p>
<h4>10. 创建WebServer CR</h4>
<p>webserver-operator项目中有一个CR sample，位于config/samples下面，我们对其进行改造，添加我们在spec中加入的字段：</p>
<pre><code>// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 3
</code></pre>
<p>我们通过kubectl创建该WebServer CR：</p>
<pre><code>$cd config/samples
$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample created
</code></pre>
<p>观察controller的日志：</p>
<pre><code>1.6602084232243123e+09  INFO    controllers.WebServer   Creating a new Deployment   {"Webserver": "default/webserver-sample", "Deployment.Namespace": "default", "Deployment.Name": "webserver-sample"}
1.6602084233446114e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}
</code></pre>
<p>我们看到当CR被创建后，controller监听到相关事件，创建了对应的Deployment和service，我们查看一下为CR创建的Deployment、三个Pod以及service：</p>
<pre><code>$kubectl get service
NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes                 ClusterIP   172.26.0.1     &lt;none&gt;        443/TCP        22d
webserver-sample-service   NodePort    172.26.173.0   &lt;none&gt;        80:30010/TCP   2m58s

$kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
webserver-sample   3/3     3            3           4m44s

$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          4m52s
</code></pre>
<p>我们访问一下该服务：</p>
<pre><code>$curl http://192.168.10.182:30010
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>服务如预期返回响应！</p>
<h4>11. 伸缩、变更版本和Service自愈</h4>
<p>接下来我们来对CR做一些常见的运维操作。</p>
<ul>
<li>副本数由3变为4</li>
</ul>
<p>我们将CR的replicas由3改为4，对容器实例做一次扩展操作：</p>
<pre><code>// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 4
</code></pre>
<p>然后通过kubectl apply使之生效：</p>
<pre><code>$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample configured
</code></pre>
<p>上述命令执行后，我们观察到operator的controller日志如下：</p>
<pre><code>1.660208962767797e+09   INFO    controllers.WebServer   Deployment spec.replicas change {"Webserver": "default/webserver-sample", "from": 3, "to": 4}
</code></pre>
<p>稍后，查看pod数量：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-v9gvg   1/1     Running   0          42s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          9m41s
</code></pre>
<p>webserver pod副本数量成功从3扩为4。</p>
<ul>
<li>变更webserver image版本</li>
</ul>
<p>我们将CR的image的版本从nginx:1.23.1改为nginx:1.23.0，然后执行kubectl apply使之生效。</p>
<p>我们查看controller的响应日志如下：</p>
<pre><code>1.6602090494113188e+09  INFO    controllers.WebServer   Deployment spec.template.spec.container[0].image change {"Webserver": "default/webserver-sample", "from": "nginx:1.23.1", "to": "nginx:1.23.0"}
</code></pre>
<p>controller会更新deployment，导致所辖pod进行滚动升级：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS              RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running             0          10m
webserver-sample-bc698b9fb-vk6gw   1/1     Running             0          10m
webserver-sample-bc698b9fb-xgrgb   1/1     Running             0          10m
webserver-sample-ffcf549ff-g6whk   0/1     ContainerCreating   0          12s
webserver-sample-ffcf549ff-ngjz6   0/1     ContainerCreating   0          12s
</code></pre>
<p>耐心等一小会儿，最终的pod列表为：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-ffcf549ff-g6whk   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-m6z24   1/1     Running   0          3m12s
webserver-sample-ffcf549ff-ngjz6   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-t7gvc   1/1     Running   0          4m16s
</code></pre>
<ul>
<li>service自愈：恢复被无删除的Service</li>
</ul>
<p>我们来一次“误操作”，将webserver-sample-service删除，看看controller能否帮助service自愈：</p>
<pre><code>$kubectl delete service/webserver-sample-service
service "webserver-sample-service" deleted
</code></pre>
<p>查看controller日志：</p>
<pre><code>1.6602096994710526e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}
</code></pre>
<p>我们看到controller检测到了service被删除的状态，并重建了一个新service！</p>
<p>访问新建的service：</p>
<pre><code>$curl http://192.168.10.182:30010
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>可以看到service在controller的帮助下完成了自愈！</p>
<h3>五. 小结</h3>
<p>本文对Kubernetes Operator的概念以及优点做了初步的介绍，并基于kubebuilder这个工具开发了一个具有2级能力的operator。当然这个operator离完善还有很远的距离，其主要目的还是帮助大家理解operator的概念以及实现套路。</p>
<p>相信你阅读完本文后，对operator，尤其是其基本结构会有一个较为清晰的了解，并具备开发简单operator的能力！</p>
<p>文中涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/webserver-operator">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/tree/master/webserver-operator。</p>
<h3>六. 参考资料</h3>
<ul>
<li>kubernetes operator 101, Part 1: Overview and key features &#8211; https://developers.redhat.com/articles/2021/06/11/kubernetes-operators-101-part-1-overview-and-key-features</li>
<li>Kubernetes Operators 101, Part 2: How operators work &#8211; https://developers.redhat.com/articles/2021/06/22/kubernetes-operators-101-part-2-how-operators-work</li>
<li>Operator SDK: Build Kubernetes Operators &#8211; https://developers.redhat.com/blog/2020/04/28/operator-sdk-build-kubernetes-operators-and-deploy-them-on-openshift</li>
<li>kubernetes doc: Custom Resources &#8211; https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/</li>
<li>kubernetes doc: Operator pattern &#8211; https://kubernetes.io/docs/concepts/extend-kubernetes/operator/</li>
<li>kubernetes doc: API concepts &#8211; https://kubernetes.io/docs/reference/using-api/api-concepts/</li>
<li>Introducing Operators: Putting Operational Knowledge into Software 第一篇有关operator的文章 by coreos &#8211; https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html</li>
<li>CNCF Operator白皮书v1.0 &#8211; https://github.com/cncf/tag-app-delivery/blob/main/operator-whitepaper/v1/Operator-WhitePaper_v1-0.md</li>
<li>Best practices for building Kubernetes Operators and stateful apps &#8211; https://cloud.google.com/blog/products/containers-kubernetes/best-practices-for-building-kubernetes-operators-and-stateful-apps</li>
<li>A deep dive into Kubernetes controllers &#8211;  https://docs.bitnami.com/tutorials/a-deep-dive-into-kubernetes-controllers</li>
<li>Kubernetes Operators Explained &#8211; https://blog.container-solutions.com/kubernetes-operators-explained</li>
<li>书籍《Kubernetes Operator》 &#8211; https://book.douban.com/subject/34796009/</li>
<li>书籍《Programming Kubernetes》 &#8211; https://book.douban.com/subject/35498478/</li>
<li>Operator SDK Reaches v1.0 &#8211; https://cloud.redhat.com/blog/operator-sdk-reaches-v1.0</li>
<li>What is the difference between kubebuilder and operator-sdk &#8211; https://github.com/operator-framework/operator-sdk/issues/1758</li>
<li>Kubernetes Operators in Depth &#8211; https://www.infoq.com/articles/kubernetes-operators-in-depth/</li>
<li>Get started using Kubernetes Operators &#8211; https://developer.ibm.com/learningpaths/kubernetes-operators/ </li>
<li>Use Kubernetes operators to extend Kubernetes’ functionality &#8211; https://developer.ibm.com/learningpaths/kubernetes-operators/operators-extend-kubernetes/</li>
<li>memcached operator &#8211; https://github.com/operator-framework/operator-sdk-samples/tree/master/go/memcached-operator</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：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; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/feed/</wfw:commentRss>
		<slash:comments>12</slash:comments>
		</item>
	</channel>
</rss>
