把 Zig 讲明白:这门语言到底怎么用?

  • King
  • 发布于 2小时前
  • 阅读 15

第一次看Zig,很多人会觉得它长得有点像C,又带一点现代语言的味道。但真写起来会发现,Zig最重要的不是“像谁”,而是它有一套很明确的态度:别藏事,别偷做事,别让读代码的人猜。官方文档对Zig的定位也是“健壮、最优、可复用、可维护”,而且明确强调:没有隐式控制流、没有隐式内存分配、

第一次看 Zig,很多人会觉得它长得有点像 C,又带一点现代语言的味道。

但真写起来会发现,Zig 最重要的不是“像谁”,而是它有一套很明确的态度:别藏事,别偷做事,别让读代码的人猜。

官方文档对 Zig 的定位也是“健壮、最优、可复用、可维护”,而且明确强调:没有隐式控制流、没有隐式内存分配、没有宏预处理器;如果一段代码看起来不像函数调用,那它就不是。

所以学 Zig,别先背一堆关键字。先记住一句话:

Zig 的核心不是“语法新”,而是“把程序里重要的事写清楚”。

比如:会不会失败、会不会为空、谁负责释放资源、这件事是在编译期还是运行期完成。Zig 几乎都要求你显式表达。


先看一个最小 Zig 程序

const std = @import("std");

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

这里真正值得注意的,不是 print,而是 main 的返回类型:!void。官方文档把这种写法叫 Error Union Type,意思是:这个函数要么成功返回 void,要么返回一个错误。也就是说,失败不是隐藏机制,而是类型的一部分。

这就是 Zig 的第一层用法:用类型表达真实情况。


变量怎么写:先学会少用 var

Zig 的变量声明很简单:

const name = "zig";
var count: i32 = 3;

const 是不可变,var 是可变。实际写代码时,Zig 的习惯是优先用 const,只有真要改的时候才用 var。这不只是风格问题,而是因为 Zig 很强调“意图表达准确”,不可变值本身就更容易读、更容易推理。

如果你从 Python、JavaScript 过来,可能会觉得这没什么;但在 Zig 里,这个习惯很重要。因为后面你会越来越频繁地和指针、slice、allocator 打交道,变量到底会不会变,会直接影响你对代码的理解。


函数怎么写:别把失败藏起来

Zig 的函数定义很直接:

fn add(a: i32, b: i32) i32 {
    return a + b;
}

如果函数可能失败,就写成:

fn parsePort(text: []const u8) !u16 {
    // ...
}

这个 !u16 的意思是:返回值不是“总能拿到的 u16”,而是“要么拿到 u16,要么拿到错误”。官方文档把这种模式当成 Zig 的基本能力,而不是某种高级技巧。

实际使用时最常见的是 try

const port = try parsePort("8080");

try 的意思非常朴素: 如果成功,拿结果; 如果失败,当前函数直接把错误往外返回。

这跟很多语言的异常机制不一样。它不是“先抛了再说”,而是让你在代码表面就能看见失败路径。官方文档反复强调 Zig 没有隐式控制流,这也是它读起来比较直的原因。


Zig 里最重要的一组概念:?T!T

很多人刚学 Zig,最容易混淆的是这两个:

  • ?T可能没有值
  • !T可能失败

这两个长得像,语义完全不同。

?T:可选值

var nickname: ?[]const u8 = null;
nickname = "ziggy";

意思是:nickname 要么是一段字符串,要么是 null。官方文档里可选值是单独的核心类型机制。

!T:错误联合

fn loadConfig() ![]const u8 {
    // ...
}

意思是:要么拿到配置内容,要么拿到错误。

你可以把它们记成一句话:

?T 解决“有没有”,!T 解决“成没成”。

这是 Zig 很重要的用法习惯。很多业务代码会把“没有值”和“出错了”混在一起,但 Zig 强迫你拆开,这会让后面的逻辑清楚很多。


数组、slice、字符串:Zig 最常见也最容易绕晕的地方

Zig 里,数组和 slice 不是一回事。

固定长度数组

const arr = [_]i32{ 1, 2, 3, 4 };

这是一个真正的数组,长度固定。

slice

const sub = arr[1..3];

这不是复制出一个新数组,而是对原数组某一段的“视图”。也可以理解成:指向一段连续内存,并且带长度信息。 官方文档中 slice 是基础概念,很多标准库 API 都围绕它设计。

这个概念非常关键,因为 Zig 里的字符串通常也不是“对象”,而是字节 slice,常见写法是:

const s: []const u8 = "hello";

也就是说,Zig 的字符串本质上经常就是“只读字节切片”。这跟很多高级语言不一样。你要慢慢适应:Zig 更在乎内存里的真实形状,而不是给你包装一个万能字符串对象。


defer:Zig 最好用的东西之一

如果你只学一个 Zig 关键字,我会优先推荐 defer

fn work() void {
    std.debug.print("start\n", .{});
    defer std.debug.print("end\n", .{});

    std.debug.print("doing\n", .{});
}

它的意思是:当前作用域退出时,一定执行这句。

最常见的用途是资源清理:

const file = try std.fs.cwd().openFile("a.txt", .{});
defer file.close();

这样你就不用在函数最后、每个分支里、每个错误路径里重复写关闭逻辑。Zig 很强调代码可读性,而 defer 恰好把“申请资源”和“清理资源”放得很近,读起来非常顺。

还有一个配套的 errdefer

const buf = try allocator.alloc(u8, 1024);
errdefer allocator.free(buf);

它只会在“函数因为错误退出”时执行。这个非常适合处理中途失败的回滚逻辑。官方文档把 defer/errdefer 都当成资源管理的重要工具。


内存怎么管:先别怕 allocator

很多人一听到 Zig 里经常要传 allocator,就头大。其实它的思路很简单:

既然分配内存是重要行为,那就别偷偷做。

官方文档和学习页都明确强调 Zig 没有隐式内存分配。也就是说,一个函数如果需要堆内存,最好在接口层就让你看见。

典型写法像这样:

fn makeBuffer(allocator: std.mem.Allocator) ![]u8 {
    const buf = try allocator.alloc(u8, 128);
    return buf;
}

这意味着:

  • 调用者知道这里会分配内存
  • 分配失败必须处理
  • 谁来释放也应该设计清楚

为什么 Zig 要这么“麻烦”?因为官方文档对这件事的态度很明确:资源分配可能失败,尤其是内存不足这种边界情况,不能假装它不存在。Zig 的目标之一就是在这种边界条件下也保持行为正确。

所以别把 allocator 理解成“语法负担”,更应该把它理解成:

Zig 要求你把成本写在明面上。


测试怎么写:Zig 把测试当一等公民

Zig 写测试非常顺手:

const std = @import("std");

fn addOne(x: i32) i32 {
    return x + 1;
}

test "addOne works" {
    try std.testing.expect(addOne(41) == 42);
}

然后直接跑:

zig test main.zig

官方文档和官网学习页都把 zig test 当成日常工作流,而不是附属工具。测试块只在测试时参与构建,std.testing 还提供了一套很实用的断言接口。

这也是 Zig 很适合边学边写的原因。你学一个概念,比如 optional、slice、error union,都可以顺手写成一个 test 来验证理解,而不是靠脑补。


comptime 到底是什么:先别神化它

很多文章讲 Zig,特别爱把 comptime 讲得像黑魔法。

其实你先把它理解成一句话就够了:

这件事必须在编译期确定。

官方文档对 comptime 的核心用途之一说得很直接:编译期参数是 Zig 实现泛型的方式。 也就是说,Zig 不靠宏系统,也不靠那种层层叠叠的模板机制,而是靠“这个值在编译期已知”来完成很多抽象。

比如:

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

这里的 T 是类型,但在 Zig 里,类型也能作为编译期值参与计算。这个设计很强,但初学时别急着深挖。先记住:comptime 的意义不是炫技,而是把一些本来会拖到运行时的问题,尽量提前到编译期解决。


构建怎么做:小项目先别急着上 build.zig

这是很多新手容易走偏的地方。

Zig 自带完整构建系统没错,但官方学习页也明确说了:对于基本场景,zig build-exezig build-libzig build-objzig test 往往已经够了。只有当项目变复杂、命令行变长、需要多步骤构建、需要配置项、依赖其他项目时,才值得把逻辑写进 build.zig

所以实际建议是:

  • 单文件练习:直接 zig run xxx.zig
  • 写可执行文件:zig build-exe main.zig
  • 写测试:zig test main.zig
  • 项目开始变大:再上 build.zig

别一开始就把构建系统当成必修课,不然很容易把注意力从语言本身带偏。


Zig 的真正用法,不是记语法,而是养成这几个习惯

如果你想真正会用 Zig,而不是会抄 Zig,我建议你写代码时一直问自己这几个问题。

第一,这个值是一定存在,还是可能为空? 如果可能为空,用 ?T

第二,这个函数是一定成功,还是可能失败? 如果可能失败,用 !T,并认真决定这里该 try 还是 catch

第三,这段内存是谁的? 尤其是拿到 slice 的时候,别把它当“新对象”,它往往只是已有内存的一层视图。

第四,这个资源谁来清理? 文件、锁、缓冲区、临时对象,尽量用 defer 把清理逻辑贴在资源申请附近。

第五,这件事是在运行时做,还是更适合编译期确定? 遇到泛型和配置问题时,再考虑 comptime

你把这 5 个习惯养出来,Zig 基本就算入门了。


结论

Zig 适合怎么学?

我觉得 Zig 最好的学法不是从头背文法,而是按这个顺序:

先会写小程序,学会 const/var、函数、数组、slice、打印。 然后重点吃透 ?T!Ttrydefer。 再去理解 allocator、测试、comptime。 最后再看构建系统和更复杂的标准库。

因为 Zig 真正难的从来不是“写出能跑的代码”,而是理解代码为什么这样写才算清楚。

官方文档和学习页的整体设计,其实也是围绕这条线:把控制流、内存、错误、构建这些本该重要的事情,都放在你看得见的地方。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发