Zig 深度拆解:当一门系统语言决定「向 AI 代码说不」——从 comptime 编译期元编程到显式分配器模型,一个被 TigerBeetle 和 Bun 选中的语言如何用「零隐藏控制流 + 零隐藏内存分配」重新定义系统编程的终极形态
引言:一门「逆行者」语言的诞生
2026 年 6 月,Zig 语言创始人 Andrew Kelley 在 JetBrains 播客中扔出了一颗炸弹:「AI 辅助生成的代码贡献是垃圾。」
这不是一个普通的开发者在抱怨,而是一门正在被 TigerBeetle(金融数据库)、Bun(JavaScript 运行时)等明星项目采用的系统级编程语言的掌舵者,在 AI 编程席卷硅谷的浪潮中,选择了最坚定的逆行姿态。
Zig 的代码贡献准则明确写道:不接受任何由大语言模型生成的内容,也不接受由大语言模型改写、润色、编辑、头脑风暴或调试过的内容。在 Claude Code、OpenAI Codex 等工具推动 AI 辅助编程成为标配的今天,Zig 的选择显得格外另类。
但如果你仔细审视 Zig 的设计哲学,你会发现这种「反 AI」的态度并非心血来潮——它深深根植于这门语言的核心设计理念:零隐藏。
Zig 不隐藏控制流,不隐藏内存分配,不隐藏预处理器宏,不隐藏任何让程序员失去对程序行为掌控力的东西。在这种哲学下,AI 生成的「看起来能跑但你不完全理解」的代码,恰恰是 Zig 最不能容忍的。
本文将从 Zig 的设计哲学出发,深入拆解 comptime 编译期元编程、显式分配器模型、错误处理机制等核心特性,并与 Rust、C、C++ 进行对比分析,帮助你理解为什么 TigerBeetle 选择 Zig 来构建金融级数据库,以及为什么 Bun 的创造者在将其移植到 Rust 之前,首先选择了 Zig 作为起点。
一、Zig 的设计哲学:零隐藏原则
1.1 没有隐藏的控制流
C++ 的隐藏控制流有多恐怖?一个看似简单的赋值操作,背后可能触发:
- 构造函数链(包括虚函数调用)
- 内存分配器的
new操作 - 异常处理的展开表查询
- 隐式类型转换(甚至用户自定义转换)
// C++ 中看似无害的一行代码
std::string s = "hello";
这一行背后发生了什么?
- 调用
const char*到std::string的隐式构造函数 - 构造函数内部调用
operator new分配堆内存 - 执行
memcpy复制字符串内容 - 如果赋值发生在异常处理上下文中,还要注册析构函数到展开表
Zig 的做法完全相反:
// Zig 中同样的操作——一切显式
var buffer: [100]u8 = undefined; // 栈上分配,大小明确
const len = "hello".len;
@memcpy(buffer[0..len], "hello");
const s = buffer[0..len]; // 切片,不复制
在 Zig 中,你看到的就是你得到的。没有隐式构造函数,没有隐式内存分配,没有隐式类型转换。每一个操作的开销都是可预测的、可审计的。
1.2 没有隐藏的内存分配
这是 Zig 与几乎所有现代语言最大的区别之一。Zig 的标准库中没有全局内存分配器。所有需要动态内存的函数都显式接受一个 Allocator 参数:
// Zig 的分配器是显式传递的
var list = std.ArrayList(u32).init(allocator);
defer list.deinit(); // 显式释放
try list.append(42);
为什么要这样做?因为在系统编程中,「在哪里分配内存」和「如何分配内存」是两个至关重要的问题:
- 嵌入式系统可能没有
malloc,需要用内存池 - 数据库引擎需要自定义 arena 分配器来批量管理内存
- 游戏引擎需要帧分配器来避免逐帧碎片化
- 安全敏感场景需要安全擦除的分配器
C 语言给了你 malloc/free,但如果你在大型项目中想用不同的分配策略,你需要自己封装每一处内存操作。C++ 通过 std::allocator 和分配器感知容器部分解决了这个问题,但全局 new/delete 的存在仍然让「默认分配行为」隐藏在代码的每个角落。
Zig 的做法是:永远不假设分配策略。当你写一个函数时,如果它需要分配内存,你必须显式地传入一个分配器。这看起来多写了很多代码,但它带来了一个巨大的好处:你永远知道你的代码在哪里分配了内存,分配了多少,以及如何释放。
1.3 没有隐藏的预处理器
C/C++ 的宏预处理器是一个独立于语言的文本替换系统。它在编译之前运行,不受类型系统约束,可以生成任意代码。这导致了无数的 bug 和安全漏洞:
// C 宏的经典陷阱
#define SQUARE(x) x * x
int result = SQUARE(3 + 1); // 结果是 7,不是 16!
// 更危险的宏
#define SAFE_FREE(p) do { free(p); p = NULL; } while(0)
// 在某些编译器上,p = NULL 可能被优化掉
Zig 完全没有宏系统。取而代之的是 comptime——一套在编译期执行的、类型安全的、可调试的元编程机制(详见下一节)。这是 Zig 最核心的创新之一。
二、Comptime:编译期代码执行的革命
2.1 什么是 Comptime?
comptime 是 Zig 的编译期代码执行机制。它的核心思想是:在编译期执行任意 Zig 代码,并将结果嵌入到最终的二进制文件中。
// 编译期计算斐波那契数列
fn fibonacci(comptime n: comptime_int) comptime_int {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 编译期求值,零运行时开销
const result = fibonacci(10); // 编译期计算,结果是 55
这段代码在编译时就完成了计算,最终二进制文件中直接嵌入了数值 55。没有函数调用,没有递归,没有运行时开销。
2.2 Comptime 与 C++ 模板的对比
C++ 的模板系统也是编译期执行的,但它有严重的局限性:
// C++ 模板——图灵完备但痛苦
template<int N>
struct Fibonacci {
static constexpr int value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};
template<>
struct Fibonacci<0> { static constexpr int value = 0; };
template<>
struct Fibonacci<1> { static constexpr int value = 1; };
// 使用时
int x = Fibonacci<10>::value;
C++ 模板的问题:
- 语法怪异:模板特化、SFINAE、
std::enable_if等概念让模板元编程变成了「模板黑魔法」 - 错误信息灾难:模板错误信息通常长达数百行,难以理解
- 编译时间爆炸:复杂的模板实例化会显著增加编译时间
- 调试困难:模板代码几乎无法在调试器中单步执行
Zig 的 comptime 完全消除了这些问题:
// Zig comptime——直观、可读、可调试
fn fibonacci(comptime n: comptime_int) comptime_int {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 编译期和运行时使用相同的语法
const compile_time_result = fibonacci(10); // 编译期
var runtime_input: u32 = 10;
const runtime_result = fibonacci(runtime_input); // 运行时——但这里有个陷阱
等等,上面的运行时代码其实不会编译——因为 fibonacci 的参数是 comptime_int,不能传运行时值。这正是 Zig 的设计意图:让你明确区分编译期和运行时。
2.3 Comptime 的实际应用:类型安全的格式化
Zig 标准库中的 std.fmt 模块大量使用 comptime 来实现编译期类型检查:
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// 编译期检查格式字符串与参数类型是否匹配
try stdout.print("Name: {s}, Age: {d}\n", .{"Alice", 30});
// 下面这行会在编译期报错——类型不匹配
// try stdout.print("Value: {d}\n", .{"not a number"});
// error: expected integer, found '*const [13:0]u8'
}
C 的 printf 在运行时解析格式字符串,类型不匹配会导致未定义行为(甚至安全漏洞)。C++ 的 std::format 通过 std::format_string 在运行时检查,但需要运行时开销。Zig 的 print 在编译期就完成了所有类型检查,零运行时开销,零安全风险。
2.4 Comptime 与 Zig 的构建系统
Zig 的构建系统 build.zig 本身就是用 comptime 写的:
// build.zig——编译期执行的构建脚本
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "myapp",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
// 编译期条件——基于目标平台选择不同的源文件
if (target.result.os.tag == .linux) {
exe.addCSourceFile(.{
.file = b.path("src/linux_specific.c"),
.flags = &.{"-O2"},
});
}
}
与 CMake、Meson 等传统构建系统不同,Zig 的构建脚本是类型安全的、可调试的、并且可以使用 comptime 在编译期做出决策。这消除了「构建脚本语言」与「项目语言」不一致的问题。
三、显式分配器模型:掌控内存的终极形态
3.1 为什么「默认分配器」是危险的
在大多数编程语言中,当你写 new Object() 或 malloc(size) 时,你调用的是一个全局的、默认的内存分配器。这个分配器的行为通常是:
- 使用系统调用(如
mmap或sbrk)从操作系统获取内存 - 使用某种空闲链表或伙伴系统管理内存块
- 在多线程环境下使用锁来保证线程安全
这些行为在大多数情况下是正确的,但在以下场景中可能是灾难性的:
- 实时系统:默认分配器可能触发不可预测的延迟(如
mmap的系统调用开销) - 数据库引擎:需要自定义的内存池来避免碎片化
- 嵌入式系统:可能根本没有
malloc - 安全敏感场景:需要确保敏感数据在使用后被安全擦除
3.2 Zig 的分配器接口
Zig 定义了一个简洁的分配器接口:
pub const Allocator = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
alloc: *const fn (self: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8,
resize: *const fn (self: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool,
free: *const fn (self: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void,
};
// ... 方法省略
};
所有需要分配内存的函数都接受一个 Allocator 参数。这意味着你可以在运行时切换分配策略,而不需要修改任何业务代码:
// 同一个数据结构,不同的分配策略
fn processWithGeneralPurposeAllocator() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var list = std.ArrayList(u32).init(allocator);
defer list.deinit();
// ... 处理逻辑
}
fn processWithArenaAllocator() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var list = std.ArrayList(u32).init(allocator);
// 注意:不需要 deinit——arena 释放时一次性释放所有内存
// ... 处理逻辑
}
3.3 TigerBeetle 如何利用 Zig 的分配器模型
TigerBeetle 是一个用 Zig 编写的金融交易数据库,声称比传统 OLTP 数据库快 1000 倍。它的性能秘密之一就是利用 Zig 的显式分配器模型:
// TigerBeetle 的内存管理策略(简化示例)
// 使用 arena 分配器来管理 IO 内存
const io_allocator = std.heap.ArenaAllocator.init(page_allocator);
// 每个 IO 请求使用 arena 分配
// 请求完成后,整个 arena 一次性释放
// 避免了逐个 free 的开销和碎片化
TigerBeetle 还利用 Zig 的 comptime 来生成针对特定硬件优化的数据结构:
// 编译期根据 CPU 缓存行大小生成最优的数据结构
const CacheLineSize = switch (builtin.cpu.arch) {
.x86_64 => 64,
.aarch64 => 64, // Apple Silicon 也是 64
else => 64,
};
const PaddedAtomic = struct {
value: std.atomic.Value(u64),
padding: [CacheLineSize - @sizeOf(u64)]u8 = undefined,
};
四、错误处理:没有异常,没有 Result,只有错误联合类型
4.1 Zig 的错误联合类型
Zig 使用错误联合类型(Error Union)来处理错误:
// 错误联合类型:可能是错误,也可能是值
fn readFile(path: []const u8) ![]u8 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
return try file.readToEndAlloc(allocator, max_size);
}
// 调用时必须处理错误
pub fn main() !void {
const content = readFile("config.json") catch |err| {
std.log.err("Failed to read config: {}", .{err});
return err;
};
defer allocator.free(content);
// 使用 content...
}
4.2 与 Go 的错误处理对比
Go 也使用显式的错误处理,但方式不同:
// Go 的错误处理
content, err := os.ReadFile("config.json")
if err != nil {
log.Printf("Failed to read config: %v", err)
return err
}
defer os.Remove(string(content))
Go 的 if err != nil 模式虽然显式,但存在几个问题:
- 容易遗忘:开发者可能忘记检查
err,导致静默失败 - 错误链断裂:Go 的
fmt.Errorf和%w虽然可以包装错误,但错误链的遍历需要额外代码 - 缺少编译期保证:函数签名中的
error是接口类型,编译器无法强制检查所有可能的错误路径
Zig 的错误联合类型在编译期强制你处理所有可能的错误:
// Zig——编译器强制你处理错误
fn dangerous() !u32 {
return error.OutOfMemory;
}
pub fn main() !void {
// 下面这行不会编译——必须处理错误
// const x = dangerous();
// 正确的做法
const x = dangerous() catch |err| {
std.log.err("Error: {}", .{err});
return;
};
}
4.3 与 Rust 的 Result 对比
Rust 使用 Result<T, E> 来处理错误:
fn read_file(path: &str) -> Result<String, io::Error> {
std::fs::read_to_string(path)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = read_file("config.json")?;
println!("{}", content);
Ok(())
}
Rust 的错误处理在功能上与 Zig 类似,但 Zig 的错误联合类型有一些独特的优势:
- 更简洁的语法:Zig 的
!T比 Rust 的Result<T, E>更简洁 - 编译期错误集推断:Zig 编译器可以自动推断函数可能返回的错误集合
- 与 comptime 的深度集成:可以在编译期分析错误路径
五、Zig 的跨编译能力:内置的交叉编译工具链
5.1 零配置交叉编译
Zig 内置了交叉编译支持,无需安装额外的工具链:
# 在 macOS 上编译 Linux 二进制文件
zig build-exe main.zig -target x86_64-linux-gnu
# 在 x86 上编译 ARM 二进制文件
zig build-exe main.zig -target aarch64-linux-gnu
# 编译 WebAssembly
zig build-exe main.zig -target wasm32-wasi
这与 C/C++ 的交叉编译体验形成了鲜明对比。在 C/C++ 中,交叉编译通常需要:
- 安装目标平台的交叉编译工具链(如
gcc-aarch64-linux-gnu) - 配置 sysroot
- 处理各种库的路径问题
- 解决头文件依赖
Zig 把这一切都内置了。更重要的是,Zig 可以直接作为 C/C++ 编译器使用:
# 用 Zig 作为 C 编译器——自动处理交叉编译
zig cc -target aarch64-linux-gnu main.c -o main
# 用 Zig 构建 CMake 项目——实现无缝交叉编译
zig cc -target x86_64-windows-gnu main.c -o main.exe
5.2 Bun 为什么选择 Zig 作为起点
Bun 的创造者 Jarred Sumner 在 2022 年选择 Zig 来构建 JavaScript 运行时,一个关键原因就是 Zig 的跨编译能力。Bun 需要支持 macOS、Linux、Windows 三个平台,而 Zig 让开发者可以在任何平台上轻松编译出所有目标平台的二进制文件。
后来 Bun 从 Zig 重写为 Rust(由 Claude Code 在 9 天内完成了 100 万行 Rust 代码的生成),但 Zig 的设计哲学——显式、零隐藏、可控——深刻影响了 Bun 的架构设计。
六、Zig vs Rust:系统编程的两条路径
6.1 设计哲学的根本差异
Rust 和 Zig 都是现代系统编程语言,但它们的设计哲学有根本差异:
| 维度 | Rust | Zig |
|---|---|---|
| 内存安全模型 | 所有权 + 借用检查器 | 显式分配器 + 手动管理 |
| 元编程 | 过程宏 + 属性宏 | comptime |
| 错误处理 | Result<T, E> + ? 运算符 | 错误联合类型 + catch |
| 并发安全 | 编译期数据竞争检测 | 无内置保证 |
| 学习曲线 | 陡峭(所有权、生命周期) | 中等(概念简单但需要习惯) |
| 编译速度 | 较慢 | 较快 |
| AI 辅助 | 完全接受 | 明确禁止 |
6.2 什么时候选择 Zig?
Zig 更适合以下场景:
- 需要完全控制内存的场景:数据库引擎、操作系统内核、嵌入式系统
- 需要与 C 代码深度集成的场景:Zig 可以直接调用 C 函数,无需 FFI 绑定
- 需要极致编译速度的场景:Zig 的编译速度远快于 Rust
- 团队中有人熟悉 C 但不想用 C 的场景:Zig 的心智模型与 C 更接近
6.3 什么时候选择 Rust?
Rust 更适合以下场景:
- 需要编译期内存安全保证的场景:Rust 的所有权系统在编译期防止数据竞争和悬垂引用
- 大型团队协作的场景:Rust 的类型系统充当了文档的角色
- 需要丰富生态系统的场景:crates.io 的包数量远多于 Zig 的包管理器
- WebAssembly 目标的场景:Rust 对 WASM 的支持更成熟
七、Zig 的生态系统与未来展望
7.1 当前的生态系统
Zig 的生态系统虽然比 Rust 小,但在关键领域已经有了重量级项目:
- TigerBeetle:金融交易数据库,比传统 OLTP 快 1000 倍
- Bun(已迁移至 Rust):JavaScript 运行时,曾是 Zig 最知名的应用
- Mach Engine:用 Zig 编写的游戏引擎
- Zig Software Foundation:非营利组织,维护 Zig 语言
- MicroZig:嵌入式开发工具包
7.2 Zig 0.16.0 的新特性
Zig 最新稳定版本 0.16.0 带来了多项改进:
- 改进的 comptime 调试支持
- 更好的错误信息
- 标准库增强
- 编译速度优化
7.3 Zig 与 AI 的未来
Zig 拒绝 AI 代码的立场在短期内可能会限制其贡献者数量,但从长远来看,这种立场可能恰恰是 Zig 的竞争优势:
- 代码质量:每个贡献都经过人类审查,保证了代码的一致性和可理解性
- 知识传承:开发者必须真正理解代码才能贡献,促进了团队的技能成长
- 可审计性:在安全关键领域(如金融数据库),代码的可审计性至关重要
正如 Andrew Kelley 所说:「我们都在努力变成更好的程序员。那些提交 AI pull request 的人,并没有帮助实现这个目标。」
八、实战:用 Zig 构建一个高性能 HTTP 服务器
让我们通过一个完整的例子来展示 Zig 的核心特性:
const std = @import("std");
const net = std.net;
const Allocator = std.mem.Allocator;
// 使用 comptime 生成路由表
const Route = struct {
method: []const u8,
path: []const u8,
handler: *const fn (Allocator) []const u8,
};
// 编译期生成路由
fn buildRoutes(comptime routes: []const Route) []const Route {
return routes;
}
const routes = buildRoutes(&.{
.{ .method = "GET", .path = "/", .handler = handleIndex },
.{ .method = "GET", .path = "/health", .handler = handleHealth },
});
fn handleIndex(allocator: Allocator) []const u8 {
return allocator.dupe(u8, "Hello from Zig!") catch "Error";
}
fn handleHealth(allocator: Allocator) []const u8 {
return allocator.dupe(u8, "{\"status\":\"ok\"}") catch "Error";
}
pub fn main() !void {
const address = try net.Address.resolveIp("127.0.0.1", 8080);
var server = try address.listen(.{
.reuse_address = true,
});
std.log.info("Server listening on 127.0.0.1:8080", .{});
while (true) {
const connection = try server.accept();
// 处理连接...
_ = connection;
}
}
这个简单的 HTTP 服务器展示了 Zig 的多个核心特性:
- comptime 路由表:路由在编译期生成,零运行时开销
- 显式分配器:
handleIndex接受Allocator参数,调用者控制内存分配 - 错误联合类型:
![]const u8表示可能返回错误 - 无隐藏行为:每一行代码的行为都是可预测的
总结:零隐藏的终极形态
Zig 的设计哲学可以用一句话概括:给你完全的控制权,同时不给你犯错的机会(至少在编译期)。
在 AI 辅助编程成为主流的今天,Zig 的「反 AI」立场看似逆势而为,实则是一种深思熟虑的选择。当代码的质量取决于程序员对每一行代码的理解深度时,AI 生成的「看起来能跑但你不完全理解」的代码就成了一种风险。
Zig 不是最流行的语言,不是生态最丰富的语言,也不是最容易上手的语言。但如果你需要:
- 完全掌控程序的内存行为
- 编译期的类型安全和性能优化
- 与 C 代码的无缝互操作
- 跨平台编译的极致便利
- 一个「不会在背后搞小动作」的编程语言
那么 Zig 值得你认真考虑。
正如 TigerBeetle 选择 Zig 来构建金融级数据库,正如 Bun 曾选择 Zig 来构建 JavaScript 运行时——当你需要对程序的每一个字节都有掌控力时,Zig 的「零隐藏」哲学就是你最可靠的盟友。
参考资源:
- Zig 官方网站:https://ziglang.org
- Zig 0.16.0 发布说明:https://ziglang.org/download/0.16.0/release-notes.html
- TigerBeetle 项目:https://github.com/tigerbeetle/tigerbeetle
- Zig 代码贡献准则:https://ziglang.org/code-of-conduct/
- MicroZig 嵌入式工具包:https://microzig.tech
- Bytecode Alliance Component Model:https://component-model.bytecodealliance.org