把 C 语言奉为最爱的系统程序员,正在一个个关掉新建 .c 文件的窗口。2026 年 11 月 12 日,技术作家 Mihalis Tsoukalos 继 4 月探讨为何弃用 Rust 之后,再次拿 Mac 环境下的 Zig 0.17 与 clang 做了一场硬碰硬的代码拆解:他没有背叛 C 的极简哲学,只是受够了在 C 里为日常工程摩擦白白消耗时间。但在跟着这波热潮把 C 换成 Zig 之前,工程团队必须看清一个现实——Zig 只是一把人体工学更好的手术刀,绝不是能替你兜底内存安全的防弹衣。
先把这场技术迁移的核心账本摊开:
- 发生了什么.资深 C 开发者转向 Zig 0.17,看中的是它在保持无垃圾回收、贴近硬件和直接复用 C ABI 的同时,用语言原语修补了资源清理、数组越界和错误忽略三大历史痛点。
- 为什么重要.在 Rust 高昂的语法仪式感之外,Zig 提供了第二条现代系统编程路线,并已在终端项目 Ghostty 等生产软件中跑通了混合架构。
- 核心争议点.外界极易把 Zig 的切片边界检查等同于内存安全,却忽略了它不管理指针生命周期,且编译器至今仍在经历高频破坏性变更。
终结 goto 与静默越界:Zig 砍掉了哪些日常内耗
写 C 语言最折磨人的时刻,往往是函数申请了三四个资源,半途失败时得按逆序一个个释放。哪怕是 Linux 内核,标准答案也是在函数末尾堆一串 goto 标签。分配在头,清理在尾,顺序全靠人肉对齐。
Zig 0.17 用 errdefer 终结了这种精神内耗。每次分配资源后紧跟一行 errdefer,只有函数返回错误时才会按逆序自动触发。更让系统程序员心动的是测试能力:在 C 里想测试半途内存分配失败(OOM),得自己写 malloc() 包装器或挂外部工具;而在 Zig 中,直接调用标准库的 std.testing.FailingAllocator,写个 0..3 的循环去改变 fail_index,就能在几行单测里穷尽 3 条分配失败路径,确保一字节都不泄漏。事实上,Zig 早在 0.10.0 版本的发布说明中就专门记录过常见 errdefer 作用域错误的文档,并配套提供了基于 FailingAllocator 的 checkAllAllocationFailures 工具来做内存消毒。
第二个被砍掉的顽疾是数组退化。C 语言把数组传进函数时会丢掉长度变成裸指针。作者在 Mac 上对 {1, 2, 3} 数组求和,故意把循环条件 < 写成 <=,结果程序静默越界读出脏内存,算出 sum = 7 并以状态码 0 正常退出,连开了 -Wall -Wextra 的 clang 都一声不吭。换成 Zig 的切片(slice),因为切片自带长度,同样的错误在 debug 和 safe 模式下会当场触发 panic,精准报错 index 3, len 3。字符串也是同理,从此告别遍历内存找零字节的 strlen() 和少算一位终止符的缓冲区溢出。
再加上强制错误处理与 comptime 编译期执行:C 代码里随手忽略 close() 或 fclose() 返回值是家常便饭,而 Zig 把错误塞进返回类型,不写 try、catch 或 if 根本过不了编译;曾经满天飞的 #ifdef 文本宏,也变成了受编译器类型检查的普通 if 分支与泛型函数。
刺破安全幻觉:带长度的切片管不住悬垂指针
看到这里,很多人会顺理成章地把 Zig 当成比 Rust 更轻巧的内存安全救星。这恰恰是最危险的认知错觉。
我不太买账的是原文那种略显不公的擂台赛:拿开启了运行时检查的 Zig,去打仅开了 -Wall -Wextra 却没挂任何消毒器的裸奔 C 语言。真要在 C 里抓 {1, 2, 3} 的越界和有符号整数 INT_MAX + 1 溢出(在 clang 下会静默算出 -2147483648),挂上 Clang 自带的 UBSan(未定义行为消毒器)照样能当场拦下。巧的是,Zig 自己的 zig cc 编译器在 debug 模式下之所以能抓出 C 代码的整数溢出,底层正是默认帮开发者打开了 UBSan。
切片只管得住空间的边界,管不住时间的生死。
更本质的分野在于,带长度的切片不等于内存安全。Zig 官方文档写得很明白,指针生命周期的正确性依然完全留给程序员负责。复制一个切片,只是复制了指针和长度;一旦底层内存被提前释放,或者动态容器扩容重分配,手里的切片瞬间变成悬垂引用。释放后使用(Use-After-Free)和指向过期栈内存的漏洞,Zig 的编译器不会像 Rust 借用检查器那样替你拦截。
况且,连边界检查本身也是有条件的。Zig 的安全检查高度依赖构建配置:虽然当前的 safe 优化模式会保留运行时检查,但一旦团队为了追求极致性能在生产环境切到 fast 或 small 模式(或通过 @setRuntimeSafety 局部关闭),这些护栏就会默认撤除。Zig 官方把未定义行为改名叫非法行为(illegal behavior),但名字变了规矩没变——关闭检查后一旦发生越界或溢出,优化器依然拥有跟 C 语言未定义行为一模一样的自由度,照样能把你的安全判断分支直接优化蒸发。
哪怕语言原语再精巧,底层逻辑的坑也得自己填。就在 2026 年 11 月,同一位作者在另一篇 Zig 并发缓存基准测试文章里就披露过一次典型的实现缺陷:因为分片选择器与底层哈希表不小心复用了相同的哈希函数与种子,导致严重哈希冲突;直到修复分片种子后,单线程耗时才从 147 ns/op 骤降至 68 ns/op,8 线程耗时从 38 ns/op 降至 19 ns/op。语言能帮你的手脚省力,却替不了脑子。
- 风险.如果团队面临消除内存漏洞的硬性合规要求,把 C 重写为 Zig 并不能根除生命周期漏洞;上生产环境若选 Zig,必须默认锁死
safe模式并维持严苛的代码所有权审查。
生产环境的真实算盘:拿十年稳定换现代工具链划算吗
韩非子讲,明主之治国也,适其时而已矣。选技术栈也是算时代账:C 语言最大的护城河从来不是语法优美,而是跨越三十年的源码稳定性。1990 年代写的 C 代码,今天拿一本薄薄的 K&R 教材对照,依然能在几乎所有 CPU 架构上直接编译通过。
Zig 还在剧烈演进的施工期。作者自己都坦承,当升级到 Zig 0.17 时,由于 @cImport() 被移除并强制改用 zig translate-c,他不得不动手修改了自己书稿中 152 个 Zig 源码文件里的 35 个。截至 2026 年 11 月 13 日,Zig 最新发布的标记版本是 0.17.1,单这一个小版本就修复了 0.17.0 之后的超过 57 个问题(尤其是回归错误和升级阻碍)。把新项目押在 Zig 上,意味着你省下了写 goto 和排查低级越界的时间,但必须为定期跟进编译器的破坏性更新预留专门的工程预算。
好在 Zig 给存量 C 团队留了一条极其务实的退路:它本身就是一个开箱即用的 C 交叉编译器。敲一行 zig cc -target x86_64-linux-musl,就能在 Mac 上直接打出静态链接的 Linux 可执行文件,连复杂的交叉工具链都不用装。
这让渐进式渗透成了最聪明的玩法。像热门终端项目 Ghostty 在生产实践中做的那样,团队完全可以维护一个基于 C ABI 的共享 Zig 核心,在 macOS 端让 Swift 调用这个核心,在 Linux 端则用 Zig 直接调用 GTK 的 C API。
- 建议.存量 C 研发团队不必全盘重写,先拿
zig cc接管构建与交叉编译,再通过极小的 C ABI 边界(传递不透明句柄与显式长度)把高危解析或新模块逐个文件替换成 Zig。
回到开头那个问题:为什么最爱 C 语言的程序员不再用 C 写新代码?因为没有人愿意在 2026 年继续手写逆序 goto 和排查少算一个字节的越界。Zig 0.17 漂亮地治好了这些陈年老病,但前提是你得清醒地知道,自己买到的是一副更顺手的编程手套,而不是免死金牌。
