Chapter 01

Zig 哲学:无隐藏控制流与显式设计

探索 Zig 的创建背景与核心哲学,搭建开发环境,写下第一行 Zig 代码

Zig 的诞生

Andrew Kelley 与 Zig 的起源

2015 年,Andrew Kelley 开始开发 Zig 语言。他在使用 C 语言开发开源项目(包括音频处理工具 libsoundio)时,深感 C 语言有很多不必要的复杂性——隐式类型转换、未定义行为、难以调试的宏展开、以及各种 C 预处理器的"魔法"。

他的目标不是替代 Rust(Rust 致力于通过类型系统消除内存安全问题),而是创造一门"比 C 更好的 C":在保持 C 级别性能和控制力的同时,消除语言本身的复杂性和不可预测性。

2015 年
Andrew Kelley 开始 Zig 项目,最初是个人业余项目,目标是解决 C 语言的痛点。
2016 年
Zig 0.1.0 发布,引起开源社区关注,Andrew 开始全职开发 Zig。
2018 年
Zig 软件基金会成立(ZSF),通过捐款支持 Zig 的持续开发。
2021 年
Bun JavaScript 运行时宣布使用 Zig 构建,大幅提升了 Zig 的知名度。
2023 年
TigerBeetle(金融级数据库)选择 Zig,验证了 Zig 在关键系统中的可用性。
2024 年
Zig 0.13 发布,语言趋于稳定,正式面向生产环境推进。

核心哲学:无隐藏控制流

什么是"隐藏控制流"?

在许多编程语言中,看似简单的语法背后隐藏着复杂的控制流。例如在 C++ 中,析构函数会在对象离开作用域时自动调用;异常可以从任何函数调用点抛出并跳过多层调用栈;运算符重载会让 a + b 执行任意复杂的逻辑。这些特性让代码更难以理解和预测。

Zig 的核心原则是:代码中不存在你看不见的控制流。每一行代码的执行路径都是显而易见的。

Zig 的"无隐藏"承诺

与其他语言的哲学对比

语言 内存安全方式 元编程 错误处理 学习曲线
C 无保证,全靠程序员 预处理器宏(文本替换) 返回错误码(约定俗成) 中等
C++ RAII/智能指针(可选) 模板(强大但复杂) 异常 + 错误码混用 极陡峭
Rust 借用检查器(编译期强制) 过程宏 + 声明宏 Result<T,E> 类型系统 陡峭
Go GC + 无指针算术 代码生成(有限) 多返回值 (value, error) 平缓
Zig 显式 Allocator,无GC comptime(编译期Zig代码) 错误联合类型 !T 中等

Zig 不是 Rust 的替代品

常见的误解是将 Zig 和 Rust 看作竞争关系。实际上它们解决的是不同问题:

如果你需要编译期强制保证内存安全,选 Rust。如果你需要极度简洁、与 C 无缝互操作、或者替代 C 进行嵌入式开发,Zig 是更好的选择。

安装 Zig

从官网下载

访问 ziglang.org/download 下载对应平台的预编译二进制包。Zig 不需要额外的安装步骤,解压后将 zig 可执行文件添加到 PATH 即可。

# macOS (Apple Silicon)
wget https://ziglang.org/download/0.13.0/zig-macos-aarch64-0.13.0.tar.xz
tar -xf zig-macos-aarch64-0.13.0.tar.xz
export PATH="$PATH:/path/to/zig-macos-aarch64-0.13.0"

# macOS (Homebrew)
brew install zig

# Linux (x86_64)
wget https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz
tar -xf zig-linux-x86_64-0.13.0.tar.xz

# Windows (Scoop)
scoop install zig

# 验证安装
zig version   # 输出: 0.13.0
注意:Zig 版本稳定性

Zig 尚未发布 1.0,版本间可能有 breaking changes。建议使用固定版本号(如 0.13.0)而非 master 分支,并在升级时检查 changelog。生产环境建议锁定版本。

版本管理工具 zigup

# 安装 zigup(Zig 版本管理器,类似 rustup)
# 从 https://github.com/marler8997/zigup 下载

zigup 0.13.0        # 安装指定版本
zigup master        # 安装最新 nightly
zigup default 0.13.0 # 设置默认版本

Zig 工具链命令

核心命令一览

zig init
在当前目录初始化新的 Zig 项目,生成 build.zig、build.zig.zon 和 src/main.zig。
zig build
根据 build.zig 构建项目,相当于运行 build.zig 中定义的默认步骤。
zig run src/main.zig
编译并立即运行单个 .zig 文件,适合快速测试代码片段。
zig test src/main.zig
运行文件中所有 test 块,内置测试框架无需额外依赖。
zig build test
运行 build.zig 中定义的测试步骤,支持 --summary all 查看详细结果。
zig fmt src/
格式化指定目录下所有 .zig 文件,Zig 有官方统一的代码风格。
zig cc
将 Zig 作为 C/C++ 编译器使用,可直接替代 gcc/clang,支持交叉编译。
zig targets
列出所有支持的编译目标(CPU 架构 + 操作系统组合),数量超过 100 个。

项目结构

# 创建新项目
mkdir my_project && cd my_project
zig init

# 生成的目录结构
my_project/
├── build.zig         # 构建脚本(用 Zig 代码描述)
├── build.zig.zon     # 依赖清单(类似 package.json)
└── src/
    ├── main.zig      # 可执行文件入口
    └── root.zig      # 库入口(如果同时构建库)

Hello World 详解

第一个 Zig 程序

// src/main.zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, World!\n", .{});
}

分析这段代码的每个部分:

向 stdout 输出

const std = @import("std");

pub fn main() !void {
    // 获取 stdout 写入器
    const stdout = std.io.getStdOut().writer();

    // 写入字符串
    try stdout.print("Hello, {s}!\n", .{"Zig"});

    // 写入数字
    const version: u32 = 13;
    try stdout.print("Zig version: 0.{d}\n", .{version});
}
为什么 main 返回 !void?

!void 是"错误联合类型",表示函数可能返回错误,也可能正常返回(void)。try 关键字会在操作失败时自动将错误传播给调用者(这里是 main,main 再交给运行时处理)。这是 Zig 显式错误处理的核心机制。

格式字符串说明符

{s}
字符串([]const u8 或 []u8)。
{d}
十进制整数。
{x}
十六进制整数(小写)。
{X}
十六进制整数(大写)。
{b}
二进制整数。
{e}
科学计数法浮点数。
{any}
任意类型,用默认格式化方式输出(类似 Rust 的 {:?})。
{}
使用类型的默认格式化,对于实现了 format 方法的类型自动调用。

Zig 的版本稳定性挑战

为什么版本间有 breaking changes?

Zig 尚未发布 1.0 版本,官方明确表示在 1.0 之前不承诺向后兼容性。这是一个有意为之的决策:宁可在 1.0 之前做出正确的设计,也不愿背负早期设计的技术债务。每个 Zig 版本(如 0.11、0.12、0.13)之间可能有语法变化、标准库 API 变化、以及构建系统变化。

实际建议

IDE 支持

zls
Zig Language Server,提供代码补全、错误提示、跳转定义。VS Code 安装"Zig Language"扩展自动配置。
VS Code
安装"Zig Language"(ziglang.vscode-zig)扩展,自动下载 zls,是目前最完善的 Zig 开发体验。
Neovim
通过 nvim-lspconfig 配置 zls,结合 nvim-treesitter 的 Zig 语法高亮。
IntelliJ
ZigBrains 插件提供基础支持,功能相对有限。

build.zig:Zig 的构建系统

build.zig 是什么?

Zig 的构建系统本身就是用 Zig 代码编写的。build.zig 不是配置文件(不是 YAML/TOML),而是真正的 Zig 程序,它描述了如何构建你的项目。这意味着构建脚本拥有完整的 Zig 语言能力:条件判断、循环、函数、引入外部库等。

// build.zig — 完整解析
const std = @import("std");

// build 函数是构建系统的入口,接收 Build 对象
pub fn build(b: *std.Build) void {
    // 1. 声明支持的优化模式(Debug/ReleaseSafe/ReleaseFast/ReleaseSmall)
    const optimize = b.standardOptimizeOption(.{});

    // 2. 声明支持的编译目标(默认为当前平台,可通过 -Dtarget= 覆盖)
    const target = b.standardTargetOptions(.{});

    // 3. 定义可执行文件
    const exe = b.addExecutable(.{
        .name = "my_app",           // 输出文件名
        .root_source_file = b.path("src/main.zig"),  // 入口文件
        .target = target,
        .optimize = optimize,
    });

    // 4. 安装到输出目录(zig-out/bin/)
    b.installArtifact(exe);

    // 5. 定义 "zig build run" 步骤
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    // 6. 定义 "zig build test" 步骤
    const unit_tests = b.addTest(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    const run_tests = b.addRunArtifact(unit_tests);
    const test_step = b.step("test", "Run tests");
    test_step.dependOn(&run_tests.step);
}
Debug
默认模式。开启所有运行时安全检查(整数溢出、越界、unreachable),未初始化内存填充 0xaa,编译最快,二进制最大。
ReleaseSafe
开启优化,保留安全检查。适合大多数发布场景——性能接近 ReleaseFast 但保留崩溃时的错误信息。
ReleaseFast
最大化性能优化,禁用安全检查。unreachable 成为真正的未定义行为,不会 panic。适合极度性能敏感的场景。
ReleaseSmall
优化二进制体积而非速度。适合嵌入式或需要小体积发布的场景,会激进地内联和删除死代码。

build.zig.zon:依赖管理

build.zig.zon 是 Zig 的包清单文件(zon = Zig Object Notation)。它声明项目的依赖和基本元数据。

// build.zig.zon
.{
    .name = "my_project",
    .version = "0.1.0",
    .minimum_zig_version = "0.13.0",

    .dependencies = .{
        // 从 GitHub 添加依赖
        .httpz = .{
            .url = "https://github.com/karlseguin/http.zig/archive/refs/tags/v0.9.0.tar.gz",
            .hash = "12200abcd...",   // zig fetch 命令自动填充
        },
    },

    .paths = .{""},  // 发布为库时包含哪些路径
}
# 添加依赖(自动计算 hash)
zig fetch --save https://github.com/karlseguin/http.zig/archive/refs/tags/v0.9.0.tar.gz

# 在 build.zig 中使用依赖
# const httpz = b.dependency("httpz", .{ .target = target, .optimize = optimize });
# exe.root_module.addImport("httpz", httpz.module("httpz"));

Zig 作为 C 工具链

zig cc:直接替代 gcc/clang

Zig 内置了一个完整的 C/C++ 编译器(基于 Clang LLVM),可以直接用 zig cc 替代系统的 gcc/clang。这个能力让 Zig 在 C 项目中也非常有用,即使你完全不写 Zig 代码。

# 将 C 文件编译为当前平台可执行文件
zig cc hello.c -o hello

# 交叉编译:在 macOS 上生成 Linux x86_64 可执行文件
zig cc hello.c -o hello-linux -target x86_64-linux-musl

# 交叉编译:生成 Windows 可执行文件
zig cc hello.c -o hello.exe -target x86_64-windows-gnu

# 交叉编译:ARM 嵌入式(裸机)
zig cc hello.c -o hello.elf -target arm-freestanding-eabi

# 在 Makefile 中替代编译器
# CC = zig cc
# AR = zig ar

# 查看所有支持的目标(架构-OS-ABI 组合)
zig targets | head -20
为什么 zig cc 特别有价值?

传统的 C 交叉编译需要为每个目标平台单独安装工具链(arm-linux-gnueabihf-gcc、x86_64-w64-mingw32-gcc 等),配置复杂且容易出错。zig cc 内置了所有主流平台的交叉编译支持,无需额外安装——这是 Zig 在 C 社区中迅速获得认可的重要原因。Bun、TigerBeetle 等项目用 zig cc 简化了它们的 CI 构建矩阵。

Zig 的交叉编译目标格式

目标平台格式为 <cpu>-<os>-<abi>,也可以指定具体 CPU 型号:

# 常见目标格式
x86_64-linux-gnu        # Linux,glibc(动态链接)
x86_64-linux-musl       # Linux,musl libc(静态链接,单文件二进制)
x86_64-windows-gnu      # Windows,MinGW
aarch64-macos           # macOS Apple Silicon
x86_64-macos            # macOS Intel
arm-freestanding-eabi   # ARM 裸机,无 OS
wasm32-freestanding     # WebAssembly

# 指定具体 CPU 特性
zig build -Dtarget=x86_64-linux-gnu -Dcpu=baseline
zig build -Dtarget=aarch64-linux-gnu -Dcpu=cortex_a53

Zig 内置测试框架

test 块

Zig 内置测试支持,测试直接写在源文件中的 test 块里,无需引入外部测试库。

const std = @import("std");

// 被测试的函数
fn add(a: i32, b: i32) i32 {
    return a + b;
}

// test 块:在测试模式下编译和运行
test "add two numbers" {
    const result = add(2, 3);
    try std.testing.expectEqual(5, result);
}

test "negative numbers" {
    try std.testing.expectEqual(-1, add(-3, 2));
    try std.testing.expectEqual(0, add(-5, 5));
}

// 测试错误处理
fn divide(a: f64, b: f64) !f64 {
    if (b == 0.0) return error.DivisionByZero;
    return a / b;
}

test "division by zero returns error" {
    // expectError:期望返回特定错误
    try std.testing.expectError(error.DivisionByZero, divide(1.0, 0.0));
}

test "normal division" {
    const result = try divide(10.0, 2.0);
    try std.testing.expectApproxEqAbs(5.0, result, 1e-10);
}
std.testing.expectEqual
断言两个值相等,失败时打印期望值和实际值并返回错误。
std.testing.expectError
断言函数返回特定错误类型。第一个参数是期望的错误,第二个是要检测的表达式。
std.testing.expectApproxEqAbs
断言两个浮点数在绝对误差范围内相等(第三个参数是容差)。浮点比较应始终使用此函数而非 ==。
std.testing.expect
断言布尔条件为 true,是最基础的断言函数。
std.testing.allocator
测试专用分配器,会检测内存泄漏(未释放的内存会在测试结束时报告为失败)。
# 运行所有测试
zig test src/main.zig

# 运行特定测试(按名称过滤)
zig test src/main.zig --test-filter "division"

# 通过 build 系统运行测试(支持依赖)
zig build test

# 测试输出示例
# 1/3 main.add two numbers... OK
# 2/3 main.negative numbers... OK
# 3/3 main.division by zero returns error... OK
# All 3 tests passed.
测试内存泄漏检测

在测试中使用 std.testing.allocator 代替 std.heap.page_allocator,测试结束时 Zig 会自动检查是否有未释放的内存,并将泄漏报告为测试失败。这是发现内存泄漏最低成本的方式:无需 Valgrind,内置于语言本身。

本章小结

本章核心要点