Zig 的诞生
Andrew Kelley 与 Zig 的起源
2015 年,Andrew Kelley 开始开发 Zig 语言。他在使用 C 语言开发开源项目(包括音频处理工具 libsoundio)时,深感 C 语言有很多不必要的复杂性——隐式类型转换、未定义行为、难以调试的宏展开、以及各种 C 预处理器的"魔法"。
他的目标不是替代 Rust(Rust 致力于通过类型系统消除内存安全问题),而是创造一门"比 C 更好的 C":在保持 C 级别性能和控制力的同时,消除语言本身的复杂性和不可预测性。
核心哲学:无隐藏控制流
什么是"隐藏控制流"?
在许多编程语言中,看似简单的语法背后隐藏着复杂的控制流。例如在 C++ 中,析构函数会在对象离开作用域时自动调用;异常可以从任何函数调用点抛出并跳过多层调用栈;运算符重载会让 a + b 执行任意复杂的逻辑。这些特性让代码更难以理解和预测。
Zig 的核心原则是:代码中不存在你看不见的控制流。每一行代码的执行路径都是显而易见的。
- 无隐式内存分配:任何需要分配内存的函数必须接受 Allocator 参数,不可能悄悄分配堆内存
- 无运算符重载:
a + b永远只是整数或浮点数加法,不会有其他含义 - 无异常机制:错误通过返回值显式传递,不存在可以从任意位置抛出的异常
- 无宏:Zig 没有 C 风格的预处理器宏,用 comptime 代替,更安全更可调试
- 无隐式类型转换:所有类型转换必须显式进行,不存在意外的精度损失
与其他语言的哲学对比
| 语言 | 内存安全方式 | 元编程 | 错误处理 | 学习曲线 |
|---|---|---|---|---|
| C | 无保证,全靠程序员 | 预处理器宏(文本替换) | 返回错误码(约定俗成) | 中等 |
| C++ | RAII/智能指针(可选) | 模板(强大但复杂) | 异常 + 错误码混用 | 极陡峭 |
| Rust | 借用检查器(编译期强制) | 过程宏 + 声明宏 | Result<T,E> 类型系统 | 陡峭 |
| Go | GC + 无指针算术 | 代码生成(有限) | 多返回值 (value, error) | 平缓 |
| Zig | 显式 Allocator,无GC | comptime(编译期Zig代码) | 错误联合类型 !T | 中等 |
Zig 不是 Rust 的替代品
常见的误解是将 Zig 和 Rust 看作竞争关系。实际上它们解决的是不同问题:
- Rust:目标是在编译期自动防止内存安全错误,借用检查器是强制性的约束
- Zig:目标是提供一门比 C 更简洁可预测的语言,内存安全靠程序员纪律和工具辅助
如果你需要编译期强制保证内存安全,选 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 尚未发布 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 工具链命令
核心命令一览
项目结构
# 创建新项目
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", .{});
}
分析这段代码的每个部分:
const std = @import("std"):导入标准库。@import是内建函数(以@开头),不是宏。pub fn main() void:定义公开的 main 函数,返回void。注意:如果 main 可能返回错误,类型应写为!void。std.debug.print:向 stderr 输出调试信息。第一个参数是格式字符串,第二个是包含参数的匿名结构体字面量.{}。
向 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});
}
!void 是"错误联合类型",表示函数可能返回错误,也可能正常返回(void)。try 关键字会在操作失败时自动将错误传播给调用者(这里是 main,main 再交给运行时处理)。这是 Zig 显式错误处理的核心机制。
格式字符串说明符
Zig 的版本稳定性挑战
为什么版本间有 breaking changes?
Zig 尚未发布 1.0 版本,官方明确表示在 1.0 之前不承诺向后兼容性。这是一个有意为之的决策:宁可在 1.0 之前做出正确的设计,也不愿背负早期设计的技术债务。每个 Zig 版本(如 0.11、0.12、0.13)之间可能有语法变化、标准库 API 变化、以及构建系统变化。
- 学习时使用当前最新稳定版(0.13.0),不要使用 nightly/master
- 生产项目中锁定具体版本号,并在 build.zig.zon 中指定
- 升级版本前查看官方 changelog,关注 breaking changes
- 社区维护的 zig-bootstrap 项目提供更稳定的构建环境
IDE 支持
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);
}
unreachable 成为真正的未定义行为,不会 panic。适合极度性能敏感的场景。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
传统的 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);
}
# 运行所有测试
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,内置于语言本身。
本章小结
- Zig 是"比 C 更好的 C":不是 Rust 的竞品,而是在保持 C 级别控制力的同时消除 C 的隐藏复杂性——无宏、无隐式分配、无运算符重载、无异常。
- 无隐藏控制流是核心哲学:每一行代码的执行路径都应该显而易见;
try是显式的错误传播,不是异常;内存分配必须通过显式的 Allocator 参数。 - zig init 生成 build.zig:构建脚本本身就是 Zig 代码,拥有完整语言能力;四种优化模式(Debug/ReleaseSafe/ReleaseFast/ReleaseSmall)覆盖不同场景。
- zig cc 内置交叉编译:无需额外工具链即可在 macOS 上编译出 Linux、Windows、ARM 二进制,是 Zig 在 CI/CD 和构建系统中流行的重要原因。
- 内置测试框架:test 块直接写在源文件中,
std.testing.allocator自动检测内存泄漏,无需第三方测试库。 - 版本尚未稳定:0.13.0 是当前推荐版本;1.0 发布前使用固定版本号,升级前检查 changelog。