每次有人问”写系统程序用什么语言”,回答都是立场先行的:C 程序员说 C,Rust 程序员说 Rust,Go 程序员说 Go。每个人都有自己的 benchmark 证明自己是对的。
这篇不站队。我用同一个任务——一个 TCP 代理服务器——分别用 Go、C、Rust 实现,然后在五个维度上正面对比。数据说话,不说教。
测试任务:TCP Proxy
功能:接受客户端连接,转发到后端服务器,双向透传数据。最小的 L4 代理。
选这个任务是因为它恰好覆盖了系统编程的核心操作:网络 I/O、并发管理、内存拷贝、错误处理。
三个实现的代码量:
| 语言 | 实现方式 | 代码行数 | 依赖 |
|---|---|---|---|
| C | epoll + non-blocking | 380 | 无 |
| Go | goroutine + io.Copy | 85 | 标准库 |
| Rust | tokio + async/await | 120 | tokio |
维度一:吞吐量
测试:1000 个并发连接,每个连接持续双向发送 1KB 数据块。测量每秒总数据传输量。
| 并发 | C (Gbps) | Go (Gbps) | Rust (Gbps) |
|---|---|---|---|
| 100 | 8.2 | 7.5 | 8.0 |
| 1,000 | 7.8 | 7.1 | 7.6 |
| 10,000 | 7.2 | 5.8 | 7.0 |
| 50,000 | 6.5 | 3.2 | 6.3 |
低并发下三者差距不大——瓶颈在网络栈和网卡,不在应用。
10,000 并发以上 Go 开始掉队。原因:goroutine
调度器在高并发下的开销。每次 goroutine
切换需要保存/恢复栈帧(Go
调度器那篇详细分析过这个),50,000 个 goroutine
的调度开销相当可观。Go 的 io.Copy 也不如 C/Rust
的 splice 或 zero-copy 路径高效。
C 和 Rust 差距始终在 5% 以内。Rust 的 tokio 底层也是 epoll(或 io_uring),抽象层的开销在 1-2% 左右。
维度二:延迟
测试:单个连接,发送 64 字节 -> 等待后端 echo -> 转发回客户端。测量端到端延迟。
| 分位数 | C | Go | Rust |
|---|---|---|---|
| P50 | 18 us | 35 us | 20 us |
| P99 | 42 us | 180 us | 55 us |
| P99.9 | 85 us | 2.1 ms | 120 us |
| P99.99 | 210 us | 8.5 ms | 350 us |
Go 的尾延迟比 C/Rust 高一个数量级。P99.99 差 40 倍。
原因一:GC。Go 的 GC 在 1.21+ 已经非常好了,P50 STW 在 100us 以内。但 P99.99 的 GC pause 可以到几毫秒,尤其是在堆上有大量小对象时。
原因二:goroutine 调度延迟。goroutine 不是被调度就能立刻运行——它需要等 P (处理器) 空闲。在有其他 goroutine 竞争时,调度延迟是不确定的。
C 的尾延迟最低且最可预测。没有 GC、没有 runtime、内存分配模式完全可控。
Rust 介于两者之间。tokio 的 work-stealing 调度器有少量延迟抖动,但没有 GC 停顿。
维度三:内存占用
测试:维持 10,000 个空闲连接,测量 RSS。
| 指标 | C | Go | Rust |
|---|---|---|---|
| 基础 RSS | 1.2 MB | 12 MB | 3.5 MB |
| 10K 连接 RSS | 18 MB | 85 MB | 22 MB |
| 每连接开销 | 1.7 KB | 7.3 KB | 1.9 KB |
Go 的每连接开销是 C/Rust 的 4 倍。原因:每个 goroutine 的初始栈是 8KB(会动态收缩,但空闲状态最小 2KB)。10,000 个连接 = 10,000 个读 goroutine + 10,000 个写 goroutine = 20,000 个 goroutine。
C 用 epoll 事件驱动,每个连接只是一个 fd + 一小块 state 结构体。Rust 用 tokio 的异步 task,每个 task 是一个 Future 状态机,占用通常 <200 字节。
在需要支持百万级连接的场景(如 IM 服务器、推送网关),内存占用是关键约束。Go 需要 ~7GB 支撑 100 万连接,C/Rust 只需要 ~2GB。
维度四:编译速度与开发体验
| 指标 | C | Go | Rust |
|---|---|---|---|
| 编译时间(全量) | 0.3s | 1.2s | 15s |
| 编译时间(增量) | 0.1s | 0.4s | 3s |
| 编译器错误可读性 | 差 | 好 | 极好 |
| Debug 工具 | gdb/valgrind | delve/pprof | gdb/miri |
| 包管理 | 无(手写 Makefile) | go mod | cargo |
Go
的开发速度明显领先。编译快、错误信息清晰、go mod
开箱即用、pprof 性能分析一行代码嵌入。
Rust 的编译慢是真痛点。15 秒全量编译对 380 行代码来说太慢了(主要是编译 tokio 依赖)。但增量编译 3 秒可以接受。编译器错误信息是三者中最好的——不只告诉你哪里错了,还告诉你怎么改。
C 的编译最快,但没有包管理是现代开发中的硬伤。手动管理 header 依赖、链接库版本,在大型项目中是噩梦。
维度五:并发模型
| 维度 | C | Go | Rust |
|---|---|---|---|
| 并发原语 | pthread / epoll | goroutine / channel | async/await / tokio |
| 数据竞争检测 | 运行时 (TSan) | 运行时 (race detector) | 编译时 (Send/Sync) |
| 死锁检测 | 无内建 | 无内建 | 无内建 |
| Channel | 无内建 | 内建 | crossbeam / tokio mpsc |
| 共享状态 | 手动 mutex | sync.Mutex | Mutex |
Go 的并发模型对开发者最友好。go func()
启动一个 goroutine,channel
传数据,不需要思考”这个数据要不要加锁”——因为 Go
鼓励”通过通信共享,而非通过共享通信”。
Rust 的并发模型对正确性最好。Send 和
Sync trait 在编译时阻止 data
race。Mutex<T>
把锁和数据绑在一起——你不可能忘记加锁就访问数据。但学习曲线陡峭。
C 的并发模型给你最大自由度和最大犯错空间。pthread + atomic 可以做任何事,包括用一种极其隐蔽的方式写出 data race。
选型建议
| 场景 | 推荐 | 理由 |
|---|---|---|
| 业务后端 / 微服务 | Go | 开发速度快,GC 开销可接受 |
| 延迟敏感系统 (P99.9 < 1ms) | Rust 或 C | 无 GC,延迟可预测 |
| 百万级连接 | C 或 Rust | 每连接内存开销低 |
| 嵌入式 / 内核模块 | C | 无 runtime |
| CLI 工具 | Rust 或 Go | 编译为单一二进制 |
| 原型开发 | Go | 编译快,标准库全 |
| 已有 C 代码库扩展 | C 或 Rust (FFI) | 避免重写 |
没有一个语言在所有维度上赢。 Go 赢开发速度和并发友好度,C 赢延迟和控制精度,Rust 赢安全性和内存效率。
选语言不是选信仰。是选你的项目在哪个维度上的约束最紧。
延伸阅读:
- Go 调度器深度拆解 – Go 的 goroutine 调度为什么在高并发下掉速
- Rust 所有权:C++ RAII 本来想成为的样子 – Rust 为什么能编译时防 data race
- unsafe Rust:当编译器不再替你扛枪 – Rust 并非全程安全
- 用 Rust 重写你的 C 网络服务器 – C->Rust 迁移的真实体验
参考资料:
- TechEmpower Framework Benchmarks – 多语言 Web 框架性能横评
- The Benchmarks Game – 多语言 micro-benchmark 集合