基础语法:用 Go/Rust 经验快速上手 Zig

本文基于 Zig 0.16。

上一篇我们聊了为什么 Zig 值得学,以及怎么跑通你的第一个 Hello World。这一篇直接上手语法——变量、类型、控制流、函数、泛型。核心目标只有一个:让你能读懂和写出 Zig 代码

如果你有 Go 或 Rust 的基础,这里的每个概念对你来说都不会陌生。我会在关键节点做对照,帮你把已知的知识映射过来。

变量与常量

Zig 的变量声明非常简洁,核心原则是优先使用 const,只有需要修改时才用 var:

zig
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const std = @import("std");

pub fn main() void {
    // 常量(不可变)- 优先使用
    const pi: f64 = 3.14159;
    const name = "Zig"; // 类型推断

    // 变量(可变)
    var count: i32 = 0;
    count += 1;

    // 未定义值(类似 Rust 的 MaybeUninit)
    var x: i32 = undefined;
    x = 42;
}

Go/Rust 对照:

GoRustZig
不可变绑定const x = 5 (包级常量)let x = 5const x = 5
可变绑定x := 5var x = 5let mut x = 5var x = 5
类型推断x := 5 推断let x = 5 推断const x = 5 推断
类型标注var x int = 5let x: i32 = 5const x: i32 = 5

Zig 没有 :=let,用 const/var 直接声明。类型推断是默认行为,标注可选。

一个易混淆点:Zig 的 const 和 Rust 的 let 含义一致——指"这个绑定不可变",不是编译期常量。编译期常量用 comptime 表达,我们后面会看到。

undefined 和 Rust 的 MaybeUninit 类似——告诉编译器跳过初始化,由开发者自己随后赋值。在 Zig 社区中,undefined 常用于声明稍后初始化的数组缓冲区或用于测试的场景。

基本数据类型

Zig 的类型系统在保留传统分类的同时,引入了一些独特的设计:

类型类别Zig 类型说明
有符号整数i8, i16, i32, i64, i128任意位宽,如 i7, i24 等
无符号整数u8, u16, u32, u64, u128usize = 无符号指针大小整数(对应 C 的 uintptr_t)
浮点数f16, f32, f64, f80, f128IEEE 754 标准
布尔值booltrue / false
字符串[]const u8字节切片,UTF-8 编码
指针*T, *const T可变/不可变指针

Go/Rust 开发者最该注意的差异是任意位宽整数。Zig 不仅支持标准的 8/16/32/64/128 位宽,还允许任意位宽,比如 i7(有符号 7 位)、u24(无符号 24 位)。这在处理网络协议、硬件寄存器等场景下非常实用——不需要自己手动做掩码和位运算,编译器会帮你生成最合适的指令。

以 Go 为例,如果你要处理一个 10 位宽的协议字段,通常要声明为 uint16 然后手动 & 0x03FF 做掩码。Zig 直接声明 u10,编译器自动处理所有边界检查。

usize 是平台相关的无符号整数,对应 C 的 uintptr_t,常用于切片索引和数组长度。

字符串类型 []const u8 看起来比 Go 的 string 或 Rust 的 &str 啰嗦,但它很直白——就是一个"只读的 u8 切片"。Zig 没有隐式的字符串类型,"hello" 字面量的实际类型是 *const [5:0]u8(以 0 结尾的指针),但赋值给 []const u8 时会自动切片转换。

控制流

if 表达式

Zig 的 if 和 Rust 一样是表达式——可以返回值:

zig
1
2
3
4
fn max(a: i32, b: i32) i32 {
    // if 是表达式,可以返回值
    return if (a > b) a else b;
}

Go 的 if 是语句,不能返回值;Rust 的 if 是表达式;Zig 和 Rust 一致。

Zig 还有一个特色——if 可以捕获错误联合类型的成功值与错误值:

zig
1
2
3
4
5
6
7
8
fn doSomething() !void {
    const result = mayFail();
    if (result) |value| {
        // 成功,value 是成功值
    } else |err| {
        // 失败,err 是错误
    }
}

这里的 !T 是 Zig 的错误联合类型——返回值要么是 T,要么是一个错误。这相当于 Rust 的 Result<T, E> 或 Go 的 (T, error)

和 Rust 的 if let Ok(value) = result 相比,Zig 的写法在视觉上更紧凑——不需要模式匹配语法,直接通过管道符捕获。错误处理的完整内容我们留到第三篇详细讲,这里先有个印象即可。

switch 表达式

switch 也是表达式,并且必须穷举所有分支——这一点和 Rust 的 match 一致,和 Go 的 switch(只执行第一个匹配的 case)不同:

zig
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const Color = enum { red, green, blue };

fn colorToString(c: Color) []const u8 {
    return switch (c) {
        .red => "红色",
        .green => "绿色",
        .blue => "蓝色",
    };
}

// 范围匹配
fn classify(n: i32) []const u8 {
    return switch (n) {
        0 => "零",
        1...9 => "个位数",
        10...99 => "两位数",
        else => "其他",
    };
}

switch 的每个分支用 => 连接值和表达式,和 Rust 的 match 箭头语法一致。Zig 额外支持 ... 范围匹配,这是 Rust 的 ..= 模式在 Zig 中的等价写法。else 充当通配分支,处理未列出的所有情况。

循环

Zig 只有 forwhile 两种循环,没有 Rust 的 loop:

zig
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pub fn main() void {
    // for 循环(遍历切片)
    const items = [_]i32{ 1, 2, 3, 4, 5 };
    for (items) |item| {
        std.debug.print("{}\n", .{item});
    }

    // 带索引的 for 循环
    for (items, 0..) |item, i| {
        std.debug.print("[{}] {}\n", .{i, item});
    }

    // while 循环
    var i: usize = 0;
    while (i < 10) : (i += 1) {
        std.debug.print("{}\n", .{i});
    }

    // 带续行表达式的 while
    var sum: i32 = 0;
    var j: i32 = 1;
    while (j <= 100) : (j += 1) {
        sum += j;
    }
}

三语言对比:

场景GoRustZig
遍历集合for i, v := range itemsfor (i, v) in items.iter().enumerate()for (items, 0..) |item, i|
索引循环for i := 0; i < n; i++for i in 0..nwhile :(i += 1)
条件循环for i < n { ... }while condition { ... }while (condition) { ... }

for 的语法是 for (目标, 可选索引范围) |元素, 可选索引| { 循环体 }。多个集合用逗号分隔,这个写法后面学了元组会更清楚。

while 的条件块后面可以用 : 连接续行表达式——while (condition) : (continue expression)——这在每次循环结束、下一次判断之前执行,相当于 C 系语言的 for 第三个表达式。Go 没有这个写法,只能在循环体末尾手动写递增。

函数

Zig 用 fn 关键字定义函数,返回值类型写在参数列表之后:

zig
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 基本函数
fn add(a: i32, b: i32) i32 {
    return a + b;
}

// 多返回值(通过匿名结构体)
fn divide(a: f64, b: f64) struct { quotient: f64, remainder: f64 } {
    return .{
        .quotient = a / b,
        .remainder = @mod(a, b),
    };
}

// 泛型函数(使用 comptime 类型参数)
fn genericAdd(comptime T: type, a: T, b: T) T {
    return a + b;
}

// 调用
const result = genericAdd(i32, 1, 2);
const fresult = genericAdd(f64, 1.5, 2.5);

对照:

特性GoRustZig
函数签名func add(a, b int) intfn add(a: i32, b: i32) -> i32fn add(a: i32, b: i32) i32
多返回值原生支持通过元组/结构体通过匿名结构体
泛型[T any]fn foo\<T: Trait\>()fn foo(comptime T: type, ...)

Zig 没有 Go 那种原生多返回值,但匿名结构体不仅达到了同样效果,而且返回值字段名使调用方可以直接通过名称访问,语义更清晰。

comptime T: type 是 Zig 泛型的核心模式——comptime 告诉编译器这个参数在编译期确定,type 表示这个参数是一个类型。编译器在编译期用具体类型实例化函数,零运行时开销。

anytype 泛型参数

anytype 是 Zig 中更简洁的泛型写法,允许函数参数接受任意类型,编译器根据实际传入的值自动推断:

zig
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 使用 anytype 的泛型函数
fn add(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
    return a + b;
}

// 使用 @TypeOf 获取类型信息
fn printType(value: anytype) void {
    std.debug.print("类型: {}\n", .{@TypeOf(value)});
}

pub fn main() void {
    // 自动推断类型
    const int_result = add(1, 2);      // i32
    const float_result = add(1.5, 2.5); // f64

    printType(42);     // 类型: comptime_int
    printType("hello"); // 类型: *const [5:0]u8
}

Go/Rust 对照:

语言写法约束方式
Go(1.18+)func add[T constraints.Ordered](a, b T) T显式声明类型约束
Rustfn add\<T: Add\<Output = T\>\>(a: T, b: T) -> Ttrait bound
Zigfn add(a: anytype, b: @TypeOf(a)) @TypeOf(a)无显式约束,编译器推断

anytype 本质上就是"鸭子类型"的编译期版本——只要传入的类型支持函数中使用的操作(比如这里的 +),就能编译通过。写法比 Go 和 Rust 的泛型短得多,但代价是错误消息可能更晦涩:如果在不支持 + 的类型上调了 add,编译器的报错会从函数体内抛出,而不是在函数签名上。

@TypeOf 是 Zig 的内置函数(builtin),作用是在编译期获取表达式的类型。它返回的是类型本身,可以赋值给 type 类型的变量,供其他编译期操作使用。

本篇小结

到此我们已经覆盖了 Zig 基础语法的核心——变量与常量、基本数据类型、控制流、函数,以及 anytype 泛型参数。你现在已经能读懂大部分 Zig 代码了。

如果记住一句话就够了:Zig 的语法介于 C 的简洁和 Rust 的表达力之间——用 const/var 声明,fn 定义函数,if/switch 是表达式,comptimeanytype 在编译期做泛型。

下一篇我们深入 Zig 最独特的特性:错误处理——错误集(error set)、错误联合类型(!T)、trycatch。这是 Zig 安全编程的基石,也是 Go/Rust 开发者最需要适应的地方。