Zig 教程(超详细)

为什么选择 Zig 作为你的下一项编程技能?

在编程语言的世界里,Zig 正在悄然崛起。它不像 Python 那样亲民,也不像 Rust 那样复杂,但它却像一把精准的瑞士军刀——简洁、高效、可预测。如果你正在寻找一种能让你真正理解底层运行机制的语言,Zig 是一个绝佳的起点。

Zig 的设计哲学非常清晰:让开发者掌控一切,而不是被语言“保护”起来。它没有垃圾回收,没有运行时,也没有复杂的宏系统。它的目标是让你写出安全、高效、可移植的代码,同时清楚地知道每一行代码在做什么。

对于初学者来说,Zig 的语法简洁,学习曲线平缓。对于中级开发者,它提供了接近 C 的性能,同时引入了现代语言的特性,比如可选类型、错误处理、模块化结构。更重要的是,Zig 的编译器本身就是用 Zig 写的,这说明它已经具备了自我演化的能力。

在接下来的章节中,我会带你一步步掌握 Zig 的核心概念,从安装环境到编写第一个程序,再到处理错误和内存管理。这不仅是一次“Zig 教程”,更是一次对编程本质的重新思考。

安装与环境配置:迈出第一步

要开始使用 Zig,你需要先安装它的编译器。Zig 的安装过程非常简单,因为它自带跨平台的二进制文件。

打开终端(或命令提示符),运行以下命令下载并安装最新版本的 Zig:

wget https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.gz

tar -xzf zig-linux-x86_64-0.13.0.tar.gz

sudo cp -r zig-linux-x86_64-0.13.0 /usr/local/zig
sudo ln -s /usr/local/zig/zig /usr/local/bin/zig

注:如果你使用的是 macOS,可以使用 Homebrew 安装:brew install zig。Windows 用户可以访问 Zig 官网 下载安装包。

安装完成后,验证是否成功:

zig version

如果输出类似 0.13.0 的版本号,说明安装成功。

接下来,创建一个名为 hello.zig 的文件,内容如下:

// 这是 Zig 的第一个程序
// 使用 std.io.getStdOut().writer() 获取标准输出写入器
// .print() 方法用于格式化输出字符串
pub fn main() void {
    const stdout = std.io.getStdOut().writer();
    stdout.print("Hello, World!\n", .{}) catch |err| {
        // 如果写入失败,打印错误信息
        std.debug.print("Error writing to stdout: {any}\n", .{err});
    };
}

注:pub fn main() 是 Zig 程序的入口点,必须是公共函数。void 表示函数不返回值。std.io.getStdOut().writer() 获取标准输出的写入器,print 用于输出内容,.{} 是格式化占位符,表示传入的参数列表。

编译并运行:

zig build-exe hello.zig -O ReleaseFast
./hello

你将看到输出:Hello, World!。恭喜!你已经成功运行了第一个 Zig 程序。

基础语法与类型系统:理解 Zig 的“严谨”

Zig 的类型系统是它最核心的特性之一。它不像 JavaScript 那样动态,也不像 Python 那样“弱类型”,而是要求你在编写代码时就明确类型。

变量声明与赋值

Zig 使用 var 关键字声明变量,类型可以显式声明,也可以由编译器推断。

// 显式声明类型
var number: i32 = 42;

// 类型推断(编译器自动识别类型)
var text = "Hello"; // 类型为 []const u8(字符串字面量)

// 可变与不可变变量
var mutable = 10;     // 可变变量
const immutable = 20; // 不可变常量(不能被修改)

注:i32 是有符号 32 位整数,u8 是无符号 8 位整数。[]const u8 是不可变的字节数组,通常用于字符串。

基本类型一览

类型 说明 示例
i8 有符号 8 位整数 -128 到 127
u32 无符号 32 位整数 0 到 4,294,967,295
f32 单精度浮点数(32 位) 3.14
bool 布尔类型(true / false) true
void 无返回值类型 函数返回 void
[]const u8 不可变字节数组(字符串) "Hello"

注:Zig 的类型系统强调“显式”和“安全”。你不能随意将 i32f32 相加,必须显式转换。

函数定义与调用

Zig 的函数语法清晰,支持参数和返回值类型声明。

// 定义一个函数,接收两个 i32 参数,返回 i32
fn add(a: i32, b: i32) i32 {
    return a + b; // 返回两个数的和
}

pub fn main() void {
    const result = add(5, 3); // 调用函数并存储结果
    std.debug.print("5 + 3 = {}\n", .{result}); // 输出:5 + 3 = 8
}

注:fn 是函数定义关键字,pub 表示该函数可被外部模块调用。const result = ... 是常量赋值,.{} 是格式化参数列表。

错误处理:Zig 的“安全”哲学

在传统 C 语言中,错误处理依赖于返回值(如 -1 或 NULL),但容易被忽略。Zig 引入了错误类型(error types),让错误处理变得显式且可追踪。

错误类型与 anyerror

Zig 使用 error 关键字定义自定义错误类型。

// 定义两个错误类型
const DivisionError = error{
    DivisionByZero,
    InvalidInput,
};

// 函数可能返回错误
fn divide(a: f32, b: f32) DivisionError!f32 {
    if (b == 0) {
        return error.DivisionByZero;
    }
    if (a < 0) {
        return error.InvalidInput;
    }
    return a / b; // 成功时返回结果
}

注:DivisionError!f32 表示该函数可能返回 DivisionError 类型的错误,也可能返回 f32 类型的值。! 是“可能失败”的符号。

使用 catch 处理错误

pub fn main() void {
    const result = divide(10.0, 0.0) catch |err| {
        // 如果发生错误,执行 catch 块
        std.debug.print("Error: {}\n", .{err});
        return; // 退出函数
    };

    std.debug.print("Result: {}\n", .{result});
}

注:catch 是 Zig 中处理错误的核心机制。它会捕获 ! 类型的错误,并允许你定义处理逻辑。

错误传播:try 关键字

当函数调用可能失败时,使用 try 将错误向上抛出。

fn processFile() error{FileNotFound, ReadError}!void {
    const file = std.fs.cwd().openFile("data.txt", .{}) catch |err| {
        return err; // 传播错误
    };
    // ... 处理文件
    file.close();
}

pub fn main() void {
    try processFile(); // 如果失败,程序终止(可被外部捕获)
}

注:try 会自动将错误向上抛出,适用于函数链中的错误传播。

内存管理:手动控制,精准高效

Zig 不提供垃圾回收,内存管理完全由开发者控制。这听起来像“危险”,但实际上是“自由”。

分配内存:std.heap.page_allocator

Zig 使用堆分配器来管理动态内存。

const std = @import("std");

pub fn main() void {
    const allocator = std.heap.page_allocator;

    // 分配 16 字节的内存
    const ptr = allocator.alloc(u8, 16) catch |err| {
        std.debug.print("Allocation failed: {}\n", .{err});
        return;
    };

    // 使用内存
    for (ptr) |*byte, i| {
        byte.* = @intCast(u8, i + 1); // 赋值 1, 2, 3, ...
    }

    // 输出前 5 个值
    for (ptr[0..5]) |val| {
        std.debug.print("{}, ", .{val});
    }
    std.debug.print("\n");

    // 释放内存
    allocator.free(ptr);
}

注:allocator.alloc(u8, 16) 从堆中分配 16 个字节。ptr[0..5] 是切片(slice),表示前 5 个元素。allocator.free(ptr) 必须显式释放,否则内存泄漏。

切片(Slice):Zig 的“视图”机制

切片是 Zig 中非常重要的概念。它不是数组本身,而是一个指向数组的“视图”。

var numbers = [_]i32{ 1, 2, 3, 4, 5 };

// 创建切片
const slice = numbers[1..4]; // 取索引 1 到 3 的元素

std.debug.print("Slice: ", .{});
for (slice) |n| {
    std.debug.print("{} ", .{n});
}
std.debug.print("\n");

注:[_]i32{ ... } 是数组字面量,[1..4] 是切片语法。切片不拥有数据,只是引用。

模块化与项目结构:组织你的代码

Zig 支持模块化编程,通过 .zig 文件组织代码。

创建 math.zig

// math.zig
pub fn multiply(a: i32, b: i32) i32 {
    return a * b;
}

pub fn square(x: i32) i32 {
    return multiply(x, x);
}

main.zig 中导入并使用:

const math = @import("math.zig");

pub fn main() void {
    const result = math.square(4);
    std.debug.print("4 的平方是 {}\n", .{result});
}

注:@import("math.zig") 是导入模块的方式。pub 函数才能被外部访问。

总结:Zig 教程的终点,也是起点

Zig 不是一门“简单”的语言,但它是一门“清晰”的语言。它不隐藏底层细节,而是引导你去理解程序如何在机器上运行。从变量声明到错误处理,从内存管理到模块化,Zig 提供了一套完整而一致的编程范式。

如果你厌倦了“魔法”般的语言行为,渴望掌控代码的每一个细节,那么 Zig 就是你的答案。它不会让你“快速上手”,但会带你“真正理解”。

现在,你已经完成了 Zig 教程的第一步。接下来,尝试用 Zig 写一个小型工具、一个命令行程序,或者一个简单的游戏引擎。真正的学习,始于动手。

记住:编程不是学会语法,而是学会思考。Zig 让你重新思考代码的本质。