编程 Zig 深度拆解:当一门系统语言决定「向 AI 代码说不」——从 comptime 编译期元编程到显式分配器模型,一个被 TigerBeetle 和 Bun 选中的语言如何用「零隐藏控制流 + 零隐藏内存分配」重新定义系统编程的终极形态

2026-08-06 07:45:33 +0800 CST views 5

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";

这一行背后发生了什么?

  1. 调用 const char*std::string 的隐式构造函数
  2. 构造函数内部调用 operator new 分配堆内存
  3. 执行 memcpy 复制字符串内容
  4. 如果赋值发生在异常处理上下文中,还要注册析构函数到展开表

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++ 模板的问题:

  1. 语法怪异:模板特化、SFINAE、std::enable_if 等概念让模板元编程变成了「模板黑魔法」
  2. 错误信息灾难:模板错误信息通常长达数百行,难以理解
  3. 编译时间爆炸:复杂的模板实例化会显著增加编译时间
  4. 调试困难:模板代码几乎无法在调试器中单步执行

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) 时,你调用的是一个全局的、默认的内存分配器。这个分配器的行为通常是:

  • 使用系统调用(如 mmapsbrk)从操作系统获取内存
  • 使用某种空闲链表或伙伴系统管理内存块
  • 在多线程环境下使用锁来保证线程安全

这些行为在大多数情况下是正确的,但在以下场景中可能是灾难性的:

  1. 实时系统:默认分配器可能触发不可预测的延迟(如 mmap 的系统调用开销)
  2. 数据库引擎:需要自定义的内存池来避免碎片化
  3. 嵌入式系统:可能根本没有 malloc
  4. 安全敏感场景:需要确保敏感数据在使用后被安全擦除

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 模式虽然显式,但存在几个问题:

  1. 容易遗忘:开发者可能忘记检查 err,导致静默失败
  2. 错误链断裂:Go 的 fmt.Errorf%w 虽然可以包装错误,但错误链的遍历需要额外代码
  3. 缺少编译期保证:函数签名中的 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 的错误联合类型有一些独特的优势:

  1. 更简洁的语法:Zig 的 !T 比 Rust 的 Result<T, E> 更简洁
  2. 编译期错误集推断:Zig 编译器可以自动推断函数可能返回的错误集合
  3. 与 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++ 中,交叉编译通常需要:

  1. 安装目标平台的交叉编译工具链(如 gcc-aarch64-linux-gnu
  2. 配置 sysroot
  3. 处理各种库的路径问题
  4. 解决头文件依赖

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 都是现代系统编程语言,但它们的设计哲学有根本差异:

维度RustZig
内存安全模型所有权 + 借用检查器显式分配器 + 手动管理
元编程过程宏 + 属性宏comptime
错误处理Result<T, E> + ? 运算符错误联合类型 + catch
并发安全编译期数据竞争检测无内置保证
学习曲线陡峭(所有权、生命周期)中等(概念简单但需要习惯)
编译速度较慢较快
AI 辅助完全接受明确禁止

6.2 什么时候选择 Zig?

Zig 更适合以下场景:

  1. 需要完全控制内存的场景:数据库引擎、操作系统内核、嵌入式系统
  2. 需要与 C 代码深度集成的场景:Zig 可以直接调用 C 函数,无需 FFI 绑定
  3. 需要极致编译速度的场景:Zig 的编译速度远快于 Rust
  4. 团队中有人熟悉 C 但不想用 C 的场景:Zig 的心智模型与 C 更接近

6.3 什么时候选择 Rust?

Rust 更适合以下场景:

  1. 需要编译期内存安全保证的场景:Rust 的所有权系统在编译期防止数据竞争和悬垂引用
  2. 大型团队协作的场景:Rust 的类型系统充当了文档的角色
  3. 需要丰富生态系统的场景:crates.io 的包数量远多于 Zig 的包管理器
  4. 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 的竞争优势:

  1. 代码质量:每个贡献都经过人类审查,保证了代码的一致性和可理解性
  2. 知识传承:开发者必须真正理解代码才能贡献,促进了团队的技能成长
  3. 可审计性:在安全关键领域(如金融数据库),代码的可审计性至关重要

正如 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 的多个核心特性:

  1. comptime 路由表:路由在编译期生成,零运行时开销
  2. 显式分配器handleIndex 接受 Allocator 参数,调用者控制内存分配
  3. 错误联合类型![]const u8 表示可能返回错误
  4. 无隐藏行为:每一行代码的行为都是可预测的

总结:零隐藏的终极形态

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

推荐文章

SpaceX 600亿美元收购Cursor(中篇)
2026-06-22 03:30:23 +0800 CST
CSS Grid 和 Flexbox 的主要区别
2024-11-18 23:09:50 +0800 CST
Vue中如何处理异步更新DOM?
2024-11-18 22:38:53 +0800 CST
File 和 Blob 的区别
2024-11-18 23:11:46 +0800 CST
程序员茄子在线接单