1. 为什么我会同时折腾 Rust、Go 和 Zig我手头有一台 8G 内存的旧笔记本某周末想写一个网络代理做本地流量转发。第一反应是用 Go因为 goroutine 和 net 包实在太顺手但一想到要精确控制每个连接的内存缓冲Go 的 GC 和 slice 逃逸又让我心里没底。换 Rust 吧借用检查器会逼着我认真设计每个生命周期可这个项目只是周末玩具我不想把时间全花在跟编译器斗智斗勇上。恰好当时 Zig 发布了新版本于是我做了一个后来被朋友吐槽自找麻烦的决定同一个代理用三种语言各写一遍。这个经历让我对三者的定位有了完全不同的理解。网上关于Rust 与 Go 之争的讨论已经够多了但很少有人认真问一个问题为什么在 2024 年还有人想造一个新的系统级语言如果你也有过这种选型摇摆或者在学 Rust 时被生命周期搞到自闭、写 Go 时又觉得对底层失控那么这篇从一个普通开发者视角出发的对比笔记应该能提供一点参考。先说结论Zig 不会取代 Rust也不会取代 Go但它很可能改写系统级编程的边界。它没有 Rust 那样严格的所有权模型没有 Go 那种托管运行时却提供了非常接近 C 的性能和更好的安全体验。它不是最终答案而是很多人都没意识到的一块拼图。2. Rust 的复杂到底消耗在哪里不是曲线陡峭而是日常心智税2.1 所有权不是规则而是设计约束前置Rust 学习者最常说的一句话是借我检查器教我写代码。这句话看似正面实际上隐藏了一个关键问题所有权系统把本该在运行时暴露的问题提前到了编译期这当然是好事但代价是你必须在动手写业务逻辑之前就把数据结构之间的关系、谁是所有者的边界问题想清楚。我在写代理项目时体会极深。一个简单的HashMapSocketAddr, Connection结构如果不提前想好 Connection 里放的是引用还是所有权、是否需要ArcMutex...等代码写到 100 行编译器会用一串红色错误告诉你重新设计吧。Rust 的 borrow checker 不是在检查你的代码而是在强制你采用一种特定的思维组织方式。这套思维方式长期看收益巨大但短期看就是实打实的心智税。更让人头疼的是static和生命周期标注。写业务代码时各种库里动不动就要求T: static新手可能只是照抄Box::new或tokio::spawn的写法完全不懂为什么这里非得加一个生命周期约束。这不是曲线陡峭的问题而是每次编码都要付出额外的认知成本。Rust 的文档和生态非常优秀但它的优秀恰恰建立在一套庞大而严谨的类型系统之上这意味着你不可能跳过底层机制去写高端代码。2.2 async 生态的 Send 边界问题Rust 的 async 生态在系统级编程里独树一帜但真正用过的人都知道这里暗坑很多。tokio::spawn要求 Future 是Send static于是你不得不在每个结构体里塞ArcMutexT或者设计很多无锁数据结构来绕过锁的负担。更别提早期async_trait宏是标配到如今标准库逐渐支持 async fn in trait但各种兼容性问题仍然在不断消耗开发者的精力。我见过太多人把上半年的时间花在为什么我的 Future 不满足 Send这个问题上最后发现是一个库的底层结构没有实现 Send。这个问题在 Go 里根本不存在在 Zig 里你甚至不需要考虑 async因为标准库直接把 async 移除了你可以用非阻塞 I/O 自己控制。Rust 的 async 确实是高性能高并发的利器但它的复杂性不像 Go 的 goroutine 那样透明而是隐藏在 trait 约束和生命周期幽灵里。2.3 宏复杂度被藏起来但没有减少Rust 的宏系统特别是过程宏确实强大serde的#[derive(Serialize)]写起来优雅得让人上瘾。但你有没有想过当宏展开出错时的报错信息会让你怀疑人生当你需要调试一个宏展开的中间结果时cargo expand几乎是唯一手段而面对几百行展开代码的体验并不愉快。宏本质上是在延迟复杂性它把重复代码的生成交给编译器但生成之后的代码仍然要遵循所有权规则仍然要处理生命周期仍然要满足 trait 约束。换句话说宏简化了书写但没有简化理解。你在 Rust 生态里可以用很短的代码调用别人封装的复杂抽象可一旦抽象不满足你的场景拆开底层时面对的就是成倍的信息量。3. Go 的极简带来的天花板当不用想变成没法想3.1 GC 是系统级编程绕不过去的坎Go 的垃圾回收经过这么多年的优化做云服务、API 网关、网络中间件已经完全没问题。我写过的 Go 服务日常应用场景下 GC 停顿几乎可以忽略。但系统级编程的边界不止是日常应用。当你写的是实时音频处理、高频交易、嵌入式网关、游戏引擎时GC 暂停哪怕只有几毫秒也可能让整个产品的体验崩盘。Go 的哲学是让开发者不用考虑内存这本身很伟大。但系统级编程的很多场景恰恰需要你考虑内存缓冲区复用、内存池、缓存的命中率。Go 提供了sync.Pool和unsafe指针但这些手段要么有使用限制要么需要绕过类型系统写起来很不Go。当你想真正控制内存布局时GC 的存在就像一个大管家你没法完全绕开他去安排家具摆放。3.2 泛型虽然来了但迟到与不彻底Go 在 1.18 加入泛型解决了interface{}装箱和类型断言的痛点也引入了constraints.Ordered等一堆新概念。但用过 Rust 或 Zig 的泛型再回来看 Go 的泛型你会发现它仍然很克制没有 trait 概念只能用接口约束方法集无法做类型的关联常量或编译期计算。这导致一些需要高性能数据结构的场景Go 泛型的表现还是不如 C/Rust 那样接近零成本抽象。Go 的极简风格让初学者上手极快但深入下去这种极简会变成一种抽象能力的上限。你无法用 Go 写出真正的泛型线性代数库也不太可能在编译期进行数值计算来替换运行时常量。这些功能在系统级编程中经常被忽略可一旦你需要Go 就给不出来了。3.3 底层控制力不足内存布局、无 GC 的现实问题在写代理的时候我需要为每个连接维护一个可回收的读写缓冲区。在 Zig 里我可以直接做一个内存池把空闲块放在链表里严格掌控分配策略。在 Go 里也有sync.Pool但它的回收时机是堆 GC 来决定的不保证立刻释放。结果就是Go 版本的高负载长长尾延迟比 Rust/Zig 明显而且无法通过调优代码消除。另一个痛点是与 C 库的交互。CGo 虽然能调用 C 代码但每次跨越 Go/C 边界都有固定的上下文切换开销而且 C 指针在 Go 里受 GC 扫描规则限制不能方便地持有 Go 堆对象。这意味着很多现成的 C 生态库在 Go 里并不能做到零成本封装。相比之下Zig 和 Rust 都能更自然地与 C ABI 对接Zig 甚至可以直接导入 C 头文件。我并不是说 Go 不好。Go 在工程效率、团队协作、部署运维方面依然是当前最好的选择之一。但如果你要触碰底层它确实提供了屏障而不是帮助。4. Zig 的破局方式不是说更简单而是把复杂放对位置4.1 comptime用编译期执行替代宏与泛型Zig 最大的创新很多人会说是「没有隐式内存分配」但我认为真正让 Zig 与众不同的是comptime。简单说comptime让你在编译期执行 Zig 代码生成类型、常量、代码分支而这一切都有完整的类型检查和语法支持。比如你写一个支持任意整数类型的Pointfn Point(comptime T: type) type { return struct { x: T, y: T, fn distanceSquared(self: This()) T { return self.x * self.x self.y * self.y; } }; } pub fn main() void { const PointF64 Point(f64); const p PointF64{ .x 3.0, .y 4.0 }; std.debug.print({d}\n, .{p.distanceSquared()}); }这段代码在编译期就会生成一个具体的结构体没有运行时开销也没有额外的类型装箱。比起 C 的宏comptime的类型正确性有保证比起 Rust 的泛型与 trait 组合comptime更像直接在语言里写编译期脚本限制更少表达更直观。写多了你会发现它足以替代绝大多数宏和模板元编程的需求。4.2 错误联合与 defer错误处理该有的样子Zig 的错误处理是我个人最喜欢的设计之一。它用一个!操作符表示这个函数返回结果或错误fn readUserFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 { const file try std.fs.cwd().openFile(path, .{}); defer file.close(); return try file.readToEndAlloc(allocator, 1024 * 1024); } pub fn main() !void { const contents try readUserFile(std.heap.page_allocator, user.txt); defer std.heap.page_allocator.free(contents); std.debug.print({s}, .{contents}); }关键点在于try和defer。try遇到错误就提前返回defer保证无论返回值还是返回错误都会执行清理动作。这比 Go 的if err ! nil简洁比 Rust 的? RAII 更容易读懂资源何时释放。Zig 没有 RAII你不必为每个类型实现 Drop而是显式地在作用域内安排defer释放心智模型非常直观。调试模式下 Zig 还会保留完整的错误追踪栈线上遇到错误打印出来的堆栈信息能精确到每个函数的返回值。这种错误即返回路径的设计我实际用下来觉得比异常机制和 Go 的错误检查都更容易写出可靠的代码。4.3 Allocator 显式传递内存责任的回归Zig 标准库里几乎所有需要分配内存的函数第一个参数都必须传入一个Allocator。这个设计初看很繁琐但它的好处是巨大的谁分配、谁释放、用什么策略分配全都在函数签名里暴露出来。于是你可以轻松做到用std.heap.GeneralPurposeAllocator做全局分配器开启安全检查用ArenaAllocator做一次性批量分配结束时整块释放用自己实现的GPA或专用内存池来精确控制性能在编译期就禁用某些库的分配功能因为这取决于传入的 Allocator 实例var arena std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator arena.allocator(); const list try std.ArrayList(u8).initCapacity(allocator, 16);这段代码里ArrayList的所有内存都来自arena函数结束时一次性释放不会再有漏内存的隐患。对比 Go你无法让某个 goroutine 用一块独立的堆内存运行对比 RustAllocator需要做额外的抽象设计目前 nightly 还在推进。Zig 把内存策略的选择权和责任彻底交还给开发者这种设计哲学显然更贴近系统级编程的本质。5. 同一个代理项目三种写法我在旧笔记本上的实测记录5.1 错误处理路径对比我截取代理中的一个核心函数读取配置文件并解析键值对如果文件不存在或内容格式错误返回错误信息。三种语言的写法差异非常能说明问题。Rust 版本使用?运算符和thiserror的典型风格use std::fs::File; use std::io::Read; fn load_config(path: str) - ResultString, std::io::Error { let mut file File::open(path)?; let mut contents String::new(); file.read_to_string(mut contents)?; Ok(contents) }Go 版本func loadConfig(path string) (string, error) { contents, err : os.ReadFile(path) if err ! nil { return , err } return string(contents), nil }Zig 版本fn loadConfig(path: []const u8) ![]const u8 { const file try std.fs.cwd().openFile(path, .{}); defer file.close(); return try file.readToEndAlloc(std.heap.page_allocator, 1024 * 1024); }三种代码都非常清晰但注意几个细节Rust 需要把错误类型写进签名这里的std::io::Error如果底层有不同的错误类型还需要做转换Go 手动返回error会导致每个调用点多一行if err ! nilZig 的try会自动传播错误且错误类型是匿名的调用方可以用catch或switch精细处理具体错误码。提示Zig 的错误联合类型和 Rust 的Result其实非常相似差别在于 Zig 不需要写错误类型的泛型参数。错误在 Zig 里本质上是一组全局错误表的索引处理开销极小。5.2 编译速度、二进制体积与内存表现我在同样的硬件上用三种语言编译了相同功能的最小代理程序结果如下维度Go 1.22Rust 1.77 (tokio)Zig 0.13冷编译时间含依赖拉取约 15 秒约 1 分 20 秒约 40 秒最终二进制体积约 8 MBstrip 后约 5 MBstrip 后约 200 KB空载时 RSS 内存约 2.5 MB约 1.2 MB约 0.4 MB高并发连接时内存峰值较高受 GC 影响较低整体可控最低完全手动错误排查体验日志 堆栈Debug 断言 可读错误链Debug 模式完整错误轨迹这里要说明Go 的编译速度和部署便利优势是真实的这也是它被大量云原生项目选中的原因。Rust 的二进制虽然也很小但因为依赖 tokio 等异步运行时最终体积和编译时间都明显上升。Zig 没有自带运行时二进制几乎可以和 C 程序比肩这在嵌入式、无容器环境、追求冷启动速度的场景下非常实用。当然我不建议把上面这组数字当作严格基准。它只是我在旧笔记本上的个人实测硬件、依赖版本、编译选项不同结果会有波动。但Zig 的二进制非常小、内存非常可控这点在各类公开对比中同样是普遍结论。5.3 修改迭代时的体验差异写代理过程中我经历了几次结构改动最能体现三者差异的是添加一个超时字段这种小事在 Go 里加字段初始化时赋值读写逻辑里取值编译运行一气呵成甚至不需要测试覆盖率特别高。代码确实不用想但我心里也清楚任何问题都不会在编译期被拦截全靠运行测试。在 Rust 里加字段如果这个类型派生了很多 traitDebug、Clone、Serialize等会触发大量引用点的类型错误。改完后你会获得极强的编译期保障但过程中的红色错误消息和多次cargo check等待时间确实会让迭代变慢。在 Zig 里加字段不会引发大量连锁错误因为 Zig 默认没有自动派生的行为。编译器会告诉你该字段没有被初始化但不会像 Rust 那样蔓延到所有构造点。代价是你需要自己写序列化/克隆代码或者用第三方库标准库没有提供与 Rust 同等的derive机制。这三者的体验没有绝对好坏只有取舍。Go 让你快速跑起来Rust 让你在编译期就建立完整性模型Zig 则更像 C 的改良版给你自由但要你自己管理边界。6. Zig 的现状与坑它能进生产环境了吗6.1 生态成熟度还处在值得关注、谨慎上车阶段实话说Zig 的生态目前0.13/0.14 时代离成熟还有明显距离。标准库覆盖了常见数据结构ArrayList、StringHashMap、PriorityQueue 等网络库有std.http但功能远不如 tokio 或 Go 的 net 库那么完整。第三方生态里值得关注的有zig-http、h11等 HTTP 底层实现但 API 变化快zap基于水平封装了 http_parser 和 ev但成熟度较低图形库、音频库、游戏引擎的 Zig 绑定大多处在概念验证阶段通过cImport导入 C 库是主力方案但要注意 C 库的构建依赖如果你是做一个嵌入式固件、CLI、编译器、解释器、游戏引擎的底层模块Zig 完全可以上手如果你要快速做一个标准 Web 后端服务那我目前的建议仍然是 Go 或 Rust。6.2 版本迭代带来的 API 变动最大的学习成本我用 Zig 写的第一个程序是参照一篇 0.11 时代的博客。结果 0.12 一发布std.fs.cwd().openFile的签名变了std.heap.GeneralPurposeAllocator的初始化方式也变了。最离谱的是 async 相关 APIZig 在 0.11 里直接移除了 async/await计划未来重新设计。这意味着任何一年前学习 Zig 的资料都可能有过期风险。这也引出一个实际问题如果你希望语言生态稳定Zig 目前不是最好的选择。Linus Torvalds 曾在邮件里评价过 Zig说它有点意思但还在不断自我突破。这句话很准确Zig 的自我突破精神值得尊重但作为生产依赖确实需要耐心等待 1.0 之后 API 冻结。提示如果你是初学者建议直接锁定一个最近的稳定版本如 0.13 或 0.14并用zig init生成的项目模板开始不要盲目下载最新的 master 构建。官方文档和 release notes 是比网上教程更可信的信息源。6.3 构建系统和交叉编译真正让人眼前一亮的部分Zig 的build.zig是一个用 Zig 语言写的构建脚本而不是 C 的 Makefile 或 CMake 脚本。它把依赖管理、编译选项、目标平台、输出文件全部用代码表达调试起来比 CMake 舒服得多。一个最小项目只需要zig init zig build run生成式的重复代码很少所有配置都是强类型错误信息也是 Zig 编译器输出的可读格式。最让我惊喜的是交叉编译能力。我在旧笔记本上为树莓派和 Windows 交叉编译代理程序以往用 Rust 时需要rustup target add还要装对应平台的链接器用 Go 时虽然简单但 CGo 场景会麻烦。Zig 内置了针对几乎所有主流 target 的 libc 和链接器我只需zig build -Dtargetaarch64-linux-gnu zig build -Dtargetx86_64-windows-gnu zig build -Dtargetarm-linux-musleabihf整个过程没有额外安装任何工具链这体验甚至比 Go 还要好因为 Zig 连 musl libc 都自带了不需要手动配置 CGO_ENABLED 之类的东西。7. 最终答案这个概念本身就是错的回到标题的问题Zig 会是系统级编程的最终答案吗我的观点是把任何一门语言称为最终答案都是对现实复杂性的低估。Rust、Go、Zig 解决的问题域不同优势与代价也不同更适合以工具-场景的方式来看待场景更推荐原因云服务、Web API、CLI 工具Go开发效率极高部署简单生态成熟高性能基础软件、安全关键系统Rust编译期强保证生态最丰富适合大型项目嵌入式、驱动程序、网络设备、零运行时环境Zig无 GC、无运行时、二进制极小、直接对接 C ABI与既有 C 代码库深度集成Zig可编译 C 代码跨平台交叉编译体验最好学习系统级编程底层原理Zig显式内存管理、无抽象混乱比 C 更安全比 Rust 更容易入门底层机制我自己的体会是这三门语言不是取代关系而是在不同的层次上服务不同的人。Rust 教你把安全变成类型系统的一部分Go 教你把并发变成一种日常思维Zig 教你把内存和 C 生态的复杂性重新捡起来并用现代语言设计把它们整理得井井有条。如果非要给一句建议如果你已经有 C 基础却受够了 C 的脆弱如果你欣赏 Rust 的严谨又不喜欢它的约束如果你喜欢 Go 的开发效率又希望得到更底层的控制力那 Zig 确实值得你花一个周末试试。它未必是终点但它让我重新觉得系统级编程还有新的可能性这本身就是它最大的价值。