基础语法:用 Go/Rust 经验快速上手 Zig
本文基于 Zig 0.16。
上一篇我们聊了为什么 Zig 值得学,以及怎么跑通你的第一个 Hello World。这一篇直接上手语法——变量、类型、控制流、函数、泛型。核心目标只有一个:让你能读懂和写出 Zig 代码。
如果你有 Go 或 Rust 的基础,这里的每个概念对你来说都不会陌生。我会在关键节点做对照,帮你把已知的知识映射过来。
变量与常量
Zig 的变量声明非常简洁,核心原则是优先使用 const,只有需要修改时才用 var:
| |
Go/Rust 对照:
| Go | Rust | Zig | |
|---|---|---|---|
| 不可变绑定 | const x = 5 (包级常量) | let x = 5 | const x = 5 |
| 可变绑定 | x := 5 或 var x = 5 | let mut x = 5 | var x = 5 |
| 类型推断 | x := 5 推断 | let x = 5 推断 | const x = 5 推断 |
| 类型标注 | var x int = 5 | let x: i32 = 5 | const 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, u128 | usize = 无符号指针大小整数(对应 C 的 uintptr_t) |
| 浮点数 | f16, f32, f64, f80, f128 | IEEE 754 标准 |
| 布尔值 | bool | true / 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 一样是表达式——可以返回值:
| |
Go 的 if 是语句,不能返回值;Rust 的 if 是表达式;Zig 和 Rust 一致。
Zig 还有一个特色——if 可以捕获错误联合类型的成功值与错误值:
| |
这里的 !T 是 Zig 的错误联合类型——返回值要么是 T,要么是一个错误。这相当于 Rust 的 Result<T, E> 或 Go 的 (T, error)。
和 Rust 的 if let Ok(value) = result 相比,Zig 的写法在视觉上更紧凑——不需要模式匹配语法,直接通过管道符捕获。错误处理的完整内容我们留到第三篇详细讲,这里先有个印象即可。
switch 表达式
switch 也是表达式,并且必须穷举所有分支——这一点和 Rust 的 match 一致,和 Go 的 switch(只执行第一个匹配的 case)不同:
| |
switch 的每个分支用 => 连接值和表达式,和 Rust 的 match 箭头语法一致。Zig 额外支持 ... 范围匹配,这是 Rust 的 ..= 模式在 Zig 中的等价写法。else 充当通配分支,处理未列出的所有情况。
循环
Zig 只有 for 和 while 两种循环,没有 Rust 的 loop:
| |
三语言对比:
| 场景 | Go | Rust | Zig |
|---|---|---|---|
| 遍历集合 | for i, v := range items | for (i, v) in items.iter().enumerate() | for (items, 0..) |item, i| |
| 索引循环 | for i := 0; i < n; i++ | for i in 0..n | while :(i += 1) |
| 条件循环 | for i < n { ... } | while condition { ... } | while (condition) { ... } |
for 的语法是 for (目标, 可选索引范围) |元素, 可选索引| { 循环体 }。多个集合用逗号分隔,这个写法后面学了元组会更清楚。
while 的条件块后面可以用 : 连接续行表达式——while (condition) : (continue expression)——这在每次循环结束、下一次判断之前执行,相当于 C 系语言的 for 第三个表达式。Go 没有这个写法,只能在循环体末尾手动写递增。
函数
Zig 用 fn 关键字定义函数,返回值类型写在参数列表之后:
| |
对照:
| 特性 | Go | Rust | Zig |
|---|---|---|---|
| 函数签名 | func add(a, b int) int | fn add(a: i32, b: i32) -> i32 | fn 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 中更简洁的泛型写法,允许函数参数接受任意类型,编译器根据实际传入的值自动推断:
| |
Go/Rust 对照:
| 语言 | 写法 | 约束方式 |
|---|---|---|
| Go(1.18+) | func add[T constraints.Ordered](a, b T) T | 显式声明类型约束 |
| Rust | fn add\<T: Add\<Output = T\>\>(a: T, b: T) -> T | trait bound |
| Zig | fn 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 是表达式,comptime 和 anytype 在编译期做泛型。
下一篇我们深入 Zig 最独特的特性:错误处理——错误集(error set)、错误联合类型(!T)、try 和 catch。这是 Zig 安全编程的基石,也是 Go/Rust 开发者最需要适应的地方。