找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

6221

积分

0

好友

799

主题
发表于 昨天 20:08 | 查看: 1| 回复: 0

很多人第一次接触 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,也给出了自己的答案。

而云原生时代真正发生的变化,也许正是从这里开始。




上一篇:Solana 主网激活 v1 交易格式:最大交易尺寸提升至 4096 字节
下一篇:Jev 决策AI深度拆解:不聊天的AI,凭什么比通用大模型快200倍
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-2 23:45 , Processed in 0.511640 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表