很多人第一次接触 Go,都会遇到一个非常有意思的问题:
Go 为什么会和 Docker、Kubernetes、containerd、Prometheus 这些项目绑定在一起?
甚至有人会倒过来理解:
因为 Docker 使用 Go,所以 Go 才成为云原生语言。
但真正值得研究的其实不是这个结论。
真正的问题是:
为什么一个需要操作 Linux 内核、网络、文件系统、进程、Namespace、Cgroup,并且需要长期运行在服务器上的基础设施软件,会选择 Go?
如果 Docker 当年选择的是 C++,今天的容器生态会是什么样?如果选择的是 Java,Docker 会不会同样成功?如果选择的是 Python,开发速度可能更快,它会不会反而更适合?
甚至还有一个更尖锐的问题:
Docker 需要极致性能,为什么没有直接使用 C?
理解这个问题,就会发现:Docker 选择 Go,并不是因为 Go「更现代」,也不是因为 Go「写起来简单」。
它真正看中的,是一种非常特殊的工程平衡:
接近操作系统,但又不想承担传统系统语言全部的工程复杂度。
而这,恰恰是 Go 最重要的价值之一。
一、Docker 真正要解决的问题,不是「容器」
2013 年,Docker 第一次公开亮相。
当时创始人 Solomon Hykes 在 PyCon 2013 上演示 Docker,并把一个非常现实的问题摆到了开发者面前:
「把代码交付到服务器很难。」
Docker 后来的巨大影响,本质上不是创造了 Namespace 或 Cgroup。这些 Linux 内核能力在 Docker 出现之前就已经存在。
真正发生变化的是:
Docker 把一堆普通开发者很难直接使用的底层能力,包装成了一个简单的开发模型。
2013 年 Docker 出现之后,开发者逐渐可以用统一的方式:
编写应用 → 打包依赖 → 构建镜像 → 上传镜像 → 在服务器启动
这意味着:
开发环境和生产环境之间的差异开始被压缩。
Docker 官方后来回顾这段历史时,也明确指出,Docker 的目标是降低开发者构建、共享和运行应用的摩擦。
这时候问题来了。
如果只是做一个 CLI 工具,其实很多语言都能完成。但 Docker 不是简单 CLI,它最终要成为一个长期运行的服务器系统。
它需要:
- 网络管理
- 进程管理
- 文件系统管理
- 镜像管理
- 容器生命周期管理
- 日志系统
- 插件
- API
- 认证
- 并发任务
- 状态维护
- 事件通知
- 资源限制
- 进程监控
- 存储驱动
甚至还要和 Linux Kernel、Windows Container、containerd、runc 等底层组件交互。
换句话说:
Docker 是一个操作系统级的软件基础设施。
这才是理解 Go 选择的起点。
二、在 Docker 之前,容器已经存在
很多初学者容易产生一个误解:
Docker 发明了容器。
严格来说并不是。
Linux 容器背后的很多关键机制,在 Docker 出现之前就已经存在。例如:
- Linux namespaces
- Control Groups
- chroot
- Union Filesystem
- Copy-on-Write
这些技术经过多年演化,逐渐形成了现代容器的基础。
问题不是:
「Linux 能不能隔离进程?」
而是:
「普通开发者能不能简单地使用这些能力?」
这是两个完全不同的问题。
Linux Kernel 给你的是原始能力,Docker 给你的则是开发体验。
可以把这个差异理解成:
Linux Kernel:「这里有 Namespace,这是系统调用。」
Docker:「给我一个镜像,我帮你启动。」
Linux Kernel:「这里有 Cgroup。」
Docker:「你可以限制这个容器使用多少 CPU 和内存。」
Linux Kernel:「这里有网络 namespace 和虚拟网卡。」
Docker:「给这个容器分配一个网络。」
所以 Docker 的本质并不是「重新发明一个操作系统」,而是:
把操作系统的复杂能力,重新组织成一个开发者能够理解的抽象。
而实现这个抽象,需要一种语言。
这就是 Go 开始发挥作用的地方。
三、传统方案为什么不那么理想
假设我们今天不用 Go。
第一个选择,很可能是 C。
因为 Docker 最终要做的事情,确实非常接近操作系统。C 可以:
- 直接调用系统调用
- 控制内存
- 操作 Socket
- 创建进程
- 使用 epoll
- 修改 namespace
- 访问文件系统
- 配置网络
- 调用内核能力
从性能角度看,C 几乎没有理由输。
但真正的问题不是性能,而是:
工程复杂度。
四、如果 Docker 使用 C,会发生什么
假设 Docker 使用 C 开发,一个最基本的服务端模块,就可能涉及:
- 线程
- 锁
- 内存管理
- 指针
- 错误码
- 资源释放
- Socket
- 文件描述符
- 信号
- 进程
- 异步 I/O
这样的问题是:
每一个业务需求,都会不断往底层复杂度上叠加。
例如 Docker Engine 需要同时管理几千个容器,那么程序内部可能同时存在大量网络连接、大量文件描述符、大量子进程、大量状态对象、大量异步事件、大量后台任务。
在 C 中,这些事情当然都能完成。但开发者必须持续解决:
- 谁申请内存?
- 谁释放?
- 什么时候释放?
- 哪个线程拥有这个对象?
- 锁在哪里?
- 哪个线程可以修改?
- 发生异常以后怎么办?
- 文件描述符是否泄漏?
- 子进程退出以后谁负责回收?
- 网络断开以后状态怎么恢复?
这些问题不会消失,它们最终会变成软件维护成本。
五、C++ 解决了部分问题,却带来了另一种复杂度
C++ 比 C 更适合大型工程。它拥有 RAII、智能指针、模板、异常、STL、更强的类型系统、更丰富的抽象能力。
理论上,Docker 完全可以使用 C++。
但是 Docker 面对的另一类问题非常特殊:它不是在开发一个「复杂算法库」,而是在开发一个:
长期运行、不断处理外部请求、管理大量状态和系统资源的基础设施。
这类软件最怕什么?
不是不会抽象,而是:
抽象本身开始变成系统复杂度。
一个大型 C++ 基础设施项目可能出现非常复杂的对象生命周期、模板层级、异常传播、ABI、构建系统、编译时间、跨平台问题、第三方依赖。
Docker 的工程目标恰恰是降低基础设施复杂度。因此,语言本身不能成为新的复杂度来源。
六、Python 看起来很好,但并不适合 Docker Engine
如果从开发效率看,Python 非常诱人:代码短、生态成熟、网络编程简单、开发速度快、动态类型、调试方便。
甚至 Docker 早期生态中的某些外围组件就使用过 Python;例如 Docker Registry 的早期实现就是 Python 应用。
但 Docker Engine 自己需要面对另一个问题:
大量并发 + 系统调用 + 常驻进程 + 多平台部署。
Python 的问题不是「慢到不能运行 Docker」,真正的问题是:它不是为这种系统级服务器控制面设计的。
当软件内部出现大量后台任务、大量网络连接、大量状态管理、大量锁、大量 I/O、大量进程生命周期、大量系统交互时,你希望语言本身已经替你解决一部分并发和资源管理问题。
Go 恰好提供了这个中间层。
七、Go 出现时,解决的就是这个「中间地带」
Go 的一个重要背景,是 Google 内部长期面对大型软件工程和基础设施系统的复杂度。
Go 并没有试图成为最快的语言、最强大的语言、最灵活的语言、最安全的语言。它选择的是另外一条路线:
让大型系统软件更容易被大量工程师持续开发和维护。
所以 Go 很特别。它一方面能够:
- 访问操作系统
- 操作网络
- 创建进程
- 使用文件描述符
- 处理系统信号
- 进行并发编程
另一方面,又提供:
- 自动内存管理
- 轻量级 Goroutine
- Channel
- 简单类型系统
- 标准库
- 统一工具链
- 快速编译
- 交叉编译能力
这形成了一个非常关键的技术位置:
比脚本语言更接近操作系统,比 C/C++ 更强调工程简单性。
这正好击中了 Docker 的需求。
八、Docker 真正需要的不是「最快语言」
这是理解整件事情最重要的一句话。
很多人看到 Docker 是基础设施,就会问:
为什么不用 C?C 不是更快吗?
问题在于:
Docker 的性能瓶颈并不主要来自 Go 本身。
例如执行启动容器、创建 Namespace、配置 Cgroup、创建网络、挂载文件系统、启动进程,这些工作真正涉及的是:
Docker Engine → containerd → shim → runc → Linux Kernel
当程序最终进入 Linux Kernel 后,真正执行底层资源隔离的并不是 Go 代码。
Docker 当前架构中,Docker Engine 使用 containerd 管理容器生命周期,而 containerd 默认使用 runc 作为容器运行时。而 runc 本身也是 Go 项目,它的入口代码直接使用 Go 的 context、os、runtime、filepath 等标准库,并通过 OCI runtime-spec 与 Linux 容器能力连接起来。
所以真正的性能路径更接近:
用户 → Docker CLI → Docker API → dockerd → containerd → shim → runc → Linux Kernel → CPU / Memory / Filesystem / Network
Docker Engine 负责协调,containerd 负责生命周期,runc 负责把 OCI 描述转换成实际的容器进程,Kernel 负责最终隔离。
这是一种非常典型的分层设计。
因此 Docker 不需要让 Go 本身达到 C 的全部极限性能。它真正需要的是:
足够快 + 足够简单 + 足够稳定 + 能够处理海量并发控制任务。
九、Docker 真正喜欢 Go 的地方:Goroutine
Docker 的服务器程序需要同时做很多事情。
例如:一个客户端正在执行 docker pull,另一个客户端正在启动容器,第三个客户端正在查询日志,第四个客户端正在读取容器状态,第五个客户端正在处理网络事件。
同时后台还可能发生镜像清理、容器恢复、日志处理、健康检查、事件分发、插件通信、监控。
这就是典型的并发服务器。
传统线程模型的问题是:
每个并发任务都可能对应一个 OS Thread。
而线程存在真实成本:栈空间、调度、上下文切换、内核资源、同步、锁、创建和销毁。
Go 的设计把这里做了一个非常关键的抽象——Goroutine。
它不是简单地把 Thread 改了个名字。Go Runtime 在用户态维护调度系统,让大量 Goroutine 可以复用较少的操作系统线程。
这意味着,Docker 可以非常自然地把很多独立任务写成并发函数。例如,一个典型的 Go 风格并发任务可以写成:
go func() {
handleContainer(ctx, id)
}()
这段代码真正重要的地方不是语法,而是它表达了一种工程思想:
一个任务就是一个独立的执行单元。
这与 Docker 的模型非常契合。因为 Docker 自己管理的,本来就是大量相互独立的实体:容器、镜像、网络、任务、连接、事件、插件、请求。
于是:
Go 的并发模型和 Docker 的对象模型天然匹配。
十、Channel 为什么也很适合 Docker
服务器系统中最大的麻烦之一,是共享状态。
例如:线程 A 修改容器状态,线程 B 读取状态,线程 C 删除容器,线程 D 处理退出事件,线程 E 更新日志。
如果所有线程都直接操作共享内存,很容易出现数据竞争、锁竞争、死锁、状态不一致。
Go 的 Channel 提供了一种不同的思路:
让并发任务通过通信交互。
例如:
events := make(chan string)
go func() {
events <- "container-exited"
}()
event := <-events
fmt.Println(event)
这看起来只是一个小语法,实际上,它代表了 Go 的核心设计哲学之一:
不要让大量并发任务互相直接修改状态。
而可以通过明确的消息流动:
任务 A → Channel → 任务 B
这种设计尤其适合事件系统、后台 Worker、网络处理、任务调度、容器状态变化、日志传递、插件通信。
这也是为什么 Go 非常适合写基础设施。
十一、Go 标准库,其实才是 Docker 真正重要的基础设施
很多人学习 Go,只会关注 Gin、GORM、Zap、Redis、Kafka 这些第三方库。
但是 Docker 这一类系统软件,大量能力来自 Go 标准库本身。例如:
net、net/http、os、os/exec、sync、context、io、syscall、runtime
这些东西恰好覆盖了基础设施软件最重要的几个方面:网络、进程、文件、并发、生命周期、I/O、系统调用、运行时。
例如 Go 的 os/exec 包直接封装外部进程执行,它本质上比传统 shell 调用更加接近系统层面的进程控制。Go 官方源码说明,该包封装了 os.StartProcess,用于执行外部命令以及连接输入输出。
这对容器运行时极其重要,因为:
容器最终就是一个进程。
而 Docker Engine 经常需要做的事情就是:创建进程、启动进程、监听进程、发送信号、等待进程、收集退出状态、处理 stdin/stdout/stderr。
因此,Go 的标准库和 Docker 的问题域之间存在非常高的匹配度。
十二、为什么 Go 特别适合写「服务器型基础设施」
Docker 不是一个一次性程序,它通常是一个长时间运行的后台服务。
这意味着它需要面对:网络请求、并发、超时、取消、错误恢复、后台任务、进程生命周期、资源回收、日志、监控。
这些事情正是 Go 标准库擅长的领域。
尤其是 context。
在现代 Go 服务中,一个请求通常对应一条生命周期:
请求开始 → 处理 → 调用后端 → 等待 → 超时或者完成 → 释放资源
Docker 这类系统则更加明显。因为:一个客户端关闭,不意味着整个容器停止;一个 goroutine 结束,不意味着整个 daemon 停止;一个网络请求失败,不意味着 Docker Engine 崩溃。
因此,生命周期管理本身就是基础设施软件的核心问题。
Go 的 Context 模型恰好非常适合这种结构。
十三、Go 的「错误处理」反而适合 Docker
很多人第一次学 Go,会抱怨为什么这么多:
if err != nil {
return err
}
看起来很啰嗦。
但是到了 Docker 这种基础设施项目,你会发现:
错误本身就是业务状态的一部分。
例如:容器不存在、镜像不存在、网络不存在、权限不足、系统调用失败、磁盘空间不足、Socket 关闭、context 超时、containerd 无响应、runc 启动失败。
这些错误不能简单地「抛异常然后结束」。Docker 必须知道:
- 发生了什么
- 应该返回给谁
- 是否重试
- 是否清理资源
- 是否恢复状态
- 是否记录日志
- 是否触发事件
Go 的显式错误处理虽然代码看起来更长,但它让控制流非常清晰。对于基础设施软件,这是一个重要优势。
十四、真正决定 Docker 成败的,是「可维护性」
假设有两个语言选项:
语言 A:代码更快 10%,但开发周期更长、调试困难、开发者数量受限、编译时间长、依赖复杂、跨平台困难。
语言 B:运行性能差 5%,但开发效率高、工具统一、新人容易加入、并发简单、测试简单、部署简单。
你会选择哪个?
对于 Docker 这样的基础设施软件,答案并不应该只看 Benchmark。因为:
Docker 最终是一个巨大的软件工程。
软件工程最稀缺的资源往往不是 CPU,而是工程师时间、理解成本、维护成本、调试成本、升级成本、故障成本。
所以 Go 最重要的优势之一,并不是「它比 C 快」,而是:
它让很多系统级程序能够被大量工程师以相对简单的方式长期维护。
Moby 项目当前公开的设计原则甚至直接强调:
- 「更少的代码更好」
- 「更少的组件更好」
- 「简单、可读的代码优于魔法式复杂代码」
这些理念与 Go 的语言哲学存在明显的契合。
十五、静态编译是 Go 进入基础设施世界的重要筹码
Docker 最终需要部署到很多不同机器:Ubuntu、Debian、RHEL、Fedora、云服务器、边缘设备、ARM、AMD64、甚至 Windows。
基础设施软件非常害怕:「这台机器上依赖的库版本不一样。」
尤其是 C/C++ 程序,动态链接会产生 glibc、libssl、libc、libstdc++、系统 ABI、各种共享库版本的问题。
而 Go 的一个巨大优势是:
可以把大量运行时依赖直接编译进一个二进制文件。
这不是说所有 Go 程序都天然完全静态,也不是说 Go 项目不存在 CGO。实际上,runc 当前的构建过程就可能启用 CGO,并使用 libseccomp 等系统组件;BuildKit 的构建脚本也明确区分了 Go/CGO 构建场景。
但是总体方向没有改变:
Go 非常适合构建可直接分发的基础设施二进制。
对于 Docker 这种软件,这一点非常重要。因为 Docker Engine 不是一个普通 Web 应用,它是一项系统基础设施。
部署时,用户希望的是:下载 → 安装 → 启动 → 然后工作。
而不是:安装语言运行时、安装十几个依赖、调整 ABI、解决动态库冲突、重新编译。
十六、跨平台能力为什么如此重要
早期 Docker 主要运行在 Linux。但是随着 Docker Desktop 等产品的发展,Docker 需要面对 Linux、macOS、Windows、AMD64、ARM64、其他架构。
Docker Engine 并不是说所有平台都使用完全相同的底层实现。实际上,不同操作系统拥有不同的容器实现方式。
但 Go 提供的工程优势是:
大量上层控制逻辑可以共享。
例如:API、配置、镜像处理、网络协议、RPC、调度、事件、序列化、日志、监控。
很多代码并不需要针对每一个 CPU 架构重新编写。这带来一个非常重要的工程收益:
把「平台差异」压缩在底层,把「产品逻辑」保持在上层。
这也是现代基础设施项目越来越重视的架构思想。
十七、源码告诉我们:Docker 并不是一个简单的 Go 程序
今天去看 Moby 源码,会发现一个非常有意思的事实。
Docker Engine 的 daemon 核心代码中,可以看到:
context、net、os、runtime、sync、sync/atomic
以及 containerd client、gRPC、BuildKit、OpenTelemetry 等大量组件。
这其实非常有代表性,因为它直接体现了 Docker Engine 的真实任务:
协调整个系统。
例如当前 Moby 的 Daemon 结构中,就能看到 container store、image service、network controller、volume service、registry service、stats collector、containerd client、plugin manager、event service 等等。
也就是说:
dockerd 真正做的事情,是「控制」。
它不是自己实现 Linux Kernel,也不是自己实现 CPU 调度,更不是自己实现文件系统,而是在管理很多底层组件。
这其实是非常典型的控制面软件,而 Go 非常适合写这种「控制面」。
十八、Docker 架构后来为什么越来越模块化
早期 Docker 更像一个巨大 Docker Engine,它负责很多事情。
随着容器生态发展,这种模式逐渐出现问题。一个程序承担镜像、构建、容器、运行时、网络、存储、生命周期,越来越复杂。
于是软件开始拆分,出现 containerd、runc、BuildKit、Compose、CLI、Moby、OCI。
这种变化非常重要,因为它说明了一件事情:
Go 带来的价值,不只是「写 Docker Engine 比较方便」。
它进一步降低了「把基础设施拆成多个独立项目」的成本。因为不同项目可以独立构建、独立测试、独立发布、独立升级,通过标准接口组合起来。
这也是现代云原生生态的重要特征。
十九、containerd 进一步证明了 Go 的选择
containerd 现在已经成为容器生态中非常重要的一层。Docker Engine 使用 containerd 管理容器生命周期。
而 containerd 本身就是 Go 项目,其当前模块直接定义为:
github.com/containerd/containerd/v2
它的开发和构建流程也是围绕 Go 展开。
这就出现了一个非常有意思的现象:Docker 最初选择 Go,后来容器运行时生态出现 containerd,containerd 继续使用 Go,再向下 runc 也是 Go,再往上 BuildKit 也是 Go。
结果形成了一个巨大的技术栈:
Go → Docker → containerd → runc → BuildKit → Kubernetes
这已经不是一个产品的技术选型,而是一种生态级的技术偏好。
二十、BuildKit 把这个趋势进一步放大
现代 Docker 的构建过程也发生了变化。
今天的 docker build 默认使用 Buildx 和 BuildKit。BuildKit 项目本身提供并发依赖解析、缓存、可扩展前端、多种输出格式以及可分布式 Worker 等能力。
而 BuildKit 同样是 Go 项目。
这意味着,Docker 不只是「用 Go 写了一个容器工具」,而是在:
使用 Go 构建整个容器控制平面生态。
甚至 BuildKit 的示例代码中,可以看到它直接编译 runc、containerd、buildkitd、buildctl。这几层基础设施之间已经形成了非常紧密的工具链。
这才是「Docker 为什么使用 Go」真正值得研究的地方。
二十一、runc 告诉我们:Go 并没有取代 C
这里必须纠正一个非常常见的误解。
很多文章会把「Docker 使用 Go」说成「容器底层使用 Go」。这是不准确的。
容器最终真正依赖的是 Linux Kernel、Namespace、Cgroup、Mount、Filesystem、Seccomp、Capabilities、Network。这些底层机制都属于操作系统能力。
runc 只是负责把 OCI Runtime Specification 转化成实际执行过程。runc 本身使用 Go,但它最终仍然需要进入操作系统底层。
所以:
Go 没有取代 C。
真正发生的是:Go 站在了 C 和 Linux Kernel 的上面。
于是整个系统形成:
硬件 → Linux Kernel → 系统调用 → runc → containerd → Docker Engine → Docker CLI
这种架构其实很值得 Go 工程师学习。因为现代基础设施不是「选一种语言打到底」,而是:
让不同层使用最适合自己的技术。
二十二、性能分析:Go 到底慢不慢?
现在来看最现实的问题:Docker 是不是因为 Go 而性能变差?
答案不能简单地说「快」,也不能简单地说「慢」。应该先问:
性能到底发生在哪里?
一个容器启动过程里,大量成本发生在镜像准备、文件系统层处理、Snapshotter、网络配置、Namespace 创建、Cgroup 设置、进程创建、动态加载、应用自身启动、Kernel 系统调用。
因此,如果你把 Docker Engine 重新写成 C,并不意味着容器启动时间立即大幅下降。因为大量时间根本不在 Go 的业务代码里。
这就是系统性能分析最重要的原则:
不要优化语言,先找到真实瓶颈。
二十三、Go 真正容易出现性能问题的地方
Go 当然不是没有代价。它的主要工程成本包括:
- GC
- 内存分配
- Goroutine 调度
- 接口装箱
- 锁竞争
- Channel 通信
- 反射
- 大量小对象
- 高频日志
- 过度并发
这些都会影响性能。
尤其是:
Docker Engine 属于长时间运行的服务。
如果一个 Daemon 内部不断创建对象、分配内存、产生临时数据、序列化 JSON、处理大量事件,那么 GC 就会参与其中。
因此 Go 的使用必须建立在工程纪律之上。不是「有 GC,所以不用管内存」,而是:
让 GC 管理内存,同时认真控制对象生命周期和分配模式。
二十四、Cache Friendly 同样重要
假设 Docker Engine 需要维护 100 万个对象。如果这些对象随机分布、频繁指针跳转、数据结构巨大、访问模式不可预测,那么 CPU Cache Miss、Memory Latency、Lock Contention 都会成为问题。
这时候即使语言速度再快,也无法完全解决问题。
所以真正高级的 Go 性能优化,不是「把 for 循环优化一下」,而是:
重新设计数据结构。
例如减少共享状态、降低锁粒度、批量处理、避免不必要分配、使用连续内存、减少对象数量、缩短关键路径。
这实际上已经进入系统设计层面。
二十五、一个很有意思的事实:Docker 并没有追求「语言纯度」
当前 Docker 的架构越来越强调模块化、标准化、可插拔、可替换。
例如 Docker Engine 可以使用不同的 runtime,通过 containerd shim 接入,包括 Kata、gVisor、Wasmtime 等方案。
这说明 Docker 最终选择的是:
Go 负责控制与工程组织,而底层 runtime 则可以替换。
这比「所有东西必须用 Go」更加重要。因为现代基础设施追求的不是技术同质化,而是:
接口标准化。
二十六、Go 和 Rust 的分界线在哪里
今天再看 Docker 当年的语言选择,还可以提出一个现代问题:
如果 Docker 今天重新开始,会不会选择 Rust?
Rust 在内存安全、零成本抽象、底层控制、高性能、并发安全这些方面非常强。对于底层 Runtime,Rust 确实非常有吸引力,也已经出现了很多基于 Rust 的容器运行时探索,例如 youki。
但 Docker Engine 这样的控制面软件,并不只有「内存安全」和「性能」两个指标。它还要考虑开发效率、工程规模、生态、招聘、调试、工具、构建、部署、跨平台、第三方库、团队认知成本。
所以:
Rust 更加适合「控制底层风险」的方向,Go 更加擅长「把基础设施工程化」。
二者并不是简单替代关系。更可能出现的是:Go 负责控制面,Rust/C 负责部分高要求底层,Kernel 负责最终资源管理。这才是未来系统软件更现实的结构。
二十七、Go 和 Java 的区别也变得清晰起来
Java 的问题不是性能差。现代 JVM 非常强,Java 在大型企业系统中依然具有非常强的生命力。
但是 Docker 这类基础设施软件需要非常直接的操作系统接口、极简部署、单一二进制分发、较低的运行时依赖、快速启动、大量系统级 API。
这时候 Go 的设计就显得更自然。
Java 更像:
运行在一个强大的虚拟机平台上。
Go 更像:
构建一个独立的系统服务。
二者解决的问题并不完全相同。
二十八、为什么 Go 最终成为云原生语言
于是我们可以重新理解一个现象。
Kubernetes 为什么使用 Go?Docker 为什么使用 Go?containerd 为什么使用 Go?runc 为什么使用 Go?BuildKit 为什么使用 Go?很多 CNCF 项目为什么大量使用 Go?
并不是因为某一个项目决定「Go 是未来」,而是因为这些项目面对的问题高度相似:网络、并发、RPC、系统调用、分布式控制、CLI、配置、日志、监控、跨平台、长期运行、大量工程协作。
这些问题共同构成了:
云原生基础设施的软件形态。
而 Go 恰好覆盖了这些需求。
所以,不是云原生选择了 Go,更准确地说:
Go 和云原生基础设施的工程问题发生了高度匹配。
二十九、Go 真正改变的,不只是 Docker
Docker 使用 Go 产生的一个更深远影响是:
它证明了系统软件不一定只有 C/C++ 这一条路。
过去很多人认为:
- 系统软件:C/C++
- Web:Java/Python/JavaScript
- 脚本:Python/Shell
但是 Docker 出现以后,一个新的区域逐渐形成:
Infrastructure Software。
它既不是传统 Web,也不是操作系统 Kernel,而是在 Kernel 和 Application 之间。
例如:Container Runtime、Container Engine、Proxy、API Gateway、Service Mesh、Cloud Controller、Kubernetes Operator、Observability Agent。
这些系统软件需要接近操作系统,又需要网络,还需要并发,同时还需要非常高的工程开发效率。
这就是 Go 的主战场。
三十、Docker 的成功实际上放大了 Go
假设 Docker 当年使用的是一种不适合大规模工程协作的语言,即便 Docker 最终成功,Go 的生态影响力也不会像今天这样强。
但现实恰恰相反:Docker 成功、Kubernetes 成功、containerd 成功、Prometheus 成功、Terraform 等大量基础设施工具发展。
于是越来越多工程师开始学习 Go,越来越多公司开始用 Go 开发网关、代理、基础设施、云平台、控制器、Agent、Operator。
结果形成了一个正反馈:
更多项目使用 Go → 更多工程师学习 Go → 更多库和工具出现 → Go 基础设施生态更成熟 → 更多项目采用 Go
这是一种非常典型的生态网络效应。
三十一、但不能因此得出「Go 适合一切」
Docker 选择 Go,并不意味着 Go 是万能语言。这是非常重要的边界。
如果目标是 Kernel、驱动、极限低延迟、硬件控制、安全敏感底层组件、特殊嵌入式系统,那么 C、C++、Rust 仍然可能更适合。
如果目标是复杂企业业务、庞大 Java 生态、传统企业系统,那么 Java 依然拥有非常强的优势。
如果目标是数据科学、机器学习研究、Notebook,Python 依然不可替代。
如果目标是浏览器前端,JavaScript / TypeScript 仍然占据自己的位置。
所以真正应该学习的是:
语言边界,而不是语言崇拜。
三十二、今天的 Docker,已经不是「一个 Go 项目」
这是理解这篇文章最重要的结论之一。
2013 年的 Docker 更接近一个统一的容器平台。今天的 Docker 已经变成一个复杂的软件生态系统,其中包含 Docker CLI、Docker Engine、Moby、containerd、runc、BuildKit、Buildx、Compose、OCI、各种插件与扩展。
这种架构变化,本身就说明:
软件真正的成长,不是代码越来越多,而是边界越来越清晰。
Docker 当前的 Engine 架构已经围绕 containerd、runtime、snapshotter、BuildKit 等模块形成更明显的分层。Docker Engine 29 及之后的新安装默认采用 containerd image store;同时 Docker 官方文档也已经提供将 containerd 嵌入 dockerd 的实验性方案。
这其实比「Docker 使用 Go」更值得关注。因为:
语言解决开发问题,架构解决规模问题。
三十三、Go 真正适合的是「控制复杂系统」
现在重新看 Docker。它不是最底层,也不是最上层,它处于一个极其特殊的位置:上面是开发者,下面是 Kernel,中间存在很多复杂系统。
Docker 的工作,就是管理这些复杂系统。
这要求它能够处理网络、能够管理并发、能够启动进程、能够管理生命周期、能够处理错误、能够操作文件、能够连接 RPC、能够跨平台构建、能够长期运行、能够让大量工程师共同维护。
而这些能力恰好组成了 Go 的价值曲线。
因此:
Docker 选择 Go,并不是因为 Go 特别适合「写容器」。
而是:
Docker 需要一种能够把操作系统能力、网络能力、并发能力和大规模工程协作结合起来的语言。
Go 刚好落在这个交叉点上。
三十四、再看 Go,已经完全不一样了
学 Go 的时候,我们经常看到:变量、函数、Struct、Interface、Goroutine、Channel、Context。
这些东西看起来像语言特性。但学完 Docker 以后,再回头看,会发现它们其实都可以对应真实的软件工程问题。
- Struct:管理系统实体
- Interface:抽象不同后端实现
- Goroutine:处理大量独立任务
- Channel:传递事件
- Context:管理生命周期
- Error:显式描述失败
- Package:组织大型系统
- Standard Library:降低系统软件开发成本
- Runtime:提供并发调度
这时候你才真正理解:
Go 不是为了让代码看起来漂亮,它是为了让复杂系统仍然能够被人类管理。
三十五、一个更值得思考的问题
今天很多人讨论:Go 和 Rust 谁更好?Go 和 C++ 谁更快?Go 和 Java 谁更强?
其实这些问题的价值并不高。真正值得问的是:
这个系统最复杂的问题是什么?
- 如果复杂问题是内存安全,选择 Rust。
- 如果复杂问题是极致底层控制,选择 C / Rust。
- 如果复杂问题是庞大企业生态,选择 Java。
- 如果复杂问题是 AI 和数据分析,选择 Python。
- 如果复杂问题是大规模基础设施控制,那么 Go 就经常成为非常自然的选择。
所以:
语言不是答案,问题才是答案。
三十六、今天的 Go 与 Docker,还有一个容易忽略的细节
既然这一系列正在学习 Go 1.27.1,需要特别说明一件事情:
「Docker 使用 Go」并不意味着 Docker 当前版本必须使用 Go 1.27.1。
Go 是 Docker/Moby 的实现语言,但具体项目会独立决定自己的 Go 工具链版本。
当前 Moby 主分支的 Dockerfile 就是通过构建参数管理 Go 版本,目前仓库页面显示的默认构建版本为 Go 1.26.8。
因此:
Go 1.27.1 是你当前学习和开发 Go 的版本,而 Docker 的具体版本,则使用它自己的经过测试和验证的 Go 工具链。
这两个概念不要混在一起。这也是工程开发和「版本崇拜」的一个重要区别。
三十七、最终把整个系统串起来
现在可以重新看一遍 Docker。
Docker 的成功不是因为发明了 Namespace,也不是因为发明了 Cgroup,更不是因为 Go 本身拥有某种神奇性能。
Docker 真正做对的是:
把操作系统能力重新包装成开发者能够理解的工程抽象。
而 Go 做对的是:
让这种抽象能够以较低的工程成本长期运行和演进。
再后来:Docker 拆分、containerd 出现、runc 出现、BuildKit 出现、OCI 标准化、Kubernetes 建立控制平面、云原生生态开始发展。
于是一个最初的语言选择,最终演变成一个巨大的基础设施软件生态。
三十八、Docker 为什么使用 Go?
现在终于可以回答最初的问题了。
不是因为 Go 比 C 快,不是因为 Go 比 Rust 强,不是因为 Go 比 Java 更先进,也不是因为 Google 创造了 Go。
真正的原因是:Docker 需要同时面对操作系统、网络、并发、进程、I/O、生命周期、跨平台、工程协作、部署、维护。
而 Go 在这些问题之间找到了一个非常罕见的平衡。
它没有试图成为最底层语言,也没有试图成为最抽象语言。它选择站在:
系统与应用之间。
这个位置,后来恰好成为了云原生基础设施最重要的位置之一。
三十九、今天真正值得学习的,不是「Docker 用 Go」
如果今天学完这篇文章,你只记住一句:
Docker 是用 Go 写的。
其实价值并不大。
真正应该记住的是:
一个系统选择语言,本质上是在选择一种工程复杂度。
C 给你极致控制,C++ 给你强大抽象,Rust 给你内存安全和底层控制,Java 给你成熟的平台和企业生态,Python 给你极高的开发效率。
Go 则试图回答另一个问题:
能不能让系统软件拥有接近底层的能力,同时又让大型团队能够快速、持续地构建它?
Docker 给出的答案是:可以。
而 Kubernetes、containerd、BuildKit、runc 等后来的项目,又不断验证了这条路线。
写在最后
Docker 最有意思的地方,也许从来都不是「容器」。而是它告诉了整个软件行业:
系统软件也可以重新设计。
过去,操作系统负责底层,应用负责上层,中间存在巨大的鸿沟。Docker 开始尝试填补这个鸿沟,而 Go,则成为填补这条鸿沟的一种语言。
它没有消灭 Linux,没有消灭 C,没有消灭操作系统。它只是站在这些技术之上,把它们重新组合成开发者可以理解的系统。
于是:一个 Go 程序,开始管理进程、管理网络、管理文件系统、管理镜像、管理容器、管理分布式系统,最终甚至开始管理整个数据中心。
这也许才是 Docker 使用 Go 最值得研究的地方。
Go 真正进入云原生世界的原因,不是它能写 Docker,而是它找到了一块非常特殊的软件疆域:在操作系统之上,在业务应用之下。
这里需要并发、网络、系统调用、可靠性、工程效率、长期维护。
而随着云计算从几十台服务器走向几十万、几百万台机器,真正困难的问题也开始发生变化。
人们越来越少问:「怎样写出一段最快的代码?」
而越来越多地问:
怎样构建一个复杂到足以支撑整个基础设施,却又简单到足以让数千名工程师长期维护的系统?
Docker 给出了自己的答案。Go,也给出了自己的答案。
而云原生时代真正发生的变化,也许正是从这里开始。