第一次看Zig,很多人会觉得它长得有点像C,又带一点现代语言的味道。但真写起来会发现,Zig最重要的不是“像谁”,而是它有一套很明确的态度:别藏事,别偷做事,别让读代码的人猜。官方文档对Zig的定位也是“健壮、最优、可复用、可维护”,而且明确强调:没有隐式控制流、没有隐式内存分配、
第一次看 Zig,很多人会觉得它长得有点像 C,又带一点现代语言的味道。
但真写起来会发现,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 的第一层用法:用类型表达真实情况。
varZig 的变量声明很简单:
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 没有隐式控制流,这也是它读起来比较直的原因。
?T 和 !T很多人刚学 Zig,最容易混淆的是这两个:
?T:可能没有值!T:可能失败这两个长得像,语义完全不同。
?T:可选值var nickname: ?[]const u8 = null;
nickname = "ziggy";
意思是:nickname 要么是一段字符串,要么是 null。官方文档里可选值是单独的核心类型机制。
!T:错误联合fn loadConfig() ![]const u8 {
// ...
}
意思是:要么拿到配置内容,要么拿到错误。
你可以把它们记成一句话:
?T解决“有没有”,!T解决“成没成”。
这是 Zig 很重要的用法习惯。很多业务代码会把“没有值”和“出错了”混在一起,但 Zig 强迫你拆开,这会让后面的逻辑清楚很多。
Zig 里,数组和 slice 不是一回事。
const arr = [_]i32{ 1, 2, 3, 4 };
这是一个真正的数组,长度固定。
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 都当成资源管理的重要工具。
很多人一听到 Zig 里经常要传 allocator,就头大。其实它的思路很简单:
既然分配内存是重要行为,那就别偷偷做。
官方文档和学习页都明确强调 Zig 没有隐式内存分配。也就是说,一个函数如果需要堆内存,最好在接口层就让你看见。
典型写法像这样:
fn makeBuffer(allocator: std.mem.Allocator) ![]u8 {
const buf = try allocator.alloc(u8, 128);
return buf;
}
这意味着:
为什么 Zig 要这么“麻烦”?因为官方文档对这件事的态度很明确:资源分配可能失败,尤其是内存不足这种边界情况,不能假装它不存在。Zig 的目标之一就是在这种边界条件下也保持行为正确。
所以别把 allocator 理解成“语法负担”,更应该把它理解成:
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-exe、zig build-lib、zig build-obj 和 zig test 往往已经够了。只有当项目变复杂、命令行变长、需要多步骤构建、需要配置项、依赖其他项目时,才值得把逻辑写进 build.zig。
所以实际建议是:
zig run xxx.zigzig build-exe main.zigzig test main.zigbuild.zig别一开始就把构建系统当成必修课,不然很容易把注意力从语言本身带偏。
如果你想真正会用 Zig,而不是会抄 Zig,我建议你写代码时一直问自己这几个问题。
第一,这个值是一定存在,还是可能为空?
如果可能为空,用 ?T。
第二,这个函数是一定成功,还是可能失败?
如果可能失败,用 !T,并认真决定这里该 try 还是 catch。
第三,这段内存是谁的? 尤其是拿到 slice 的时候,别把它当“新对象”,它往往只是已有内存的一层视图。
第四,这个资源谁来清理?
文件、锁、缓冲区、临时对象,尽量用 defer 把清理逻辑贴在资源申请附近。
第五,这件事是在运行时做,还是更适合编译期确定?
遇到泛型和配置问题时,再考虑 comptime。
你把这 5 个习惯养出来,Zig 基本就算入门了。
Zig 适合怎么学?
我觉得 Zig 最好的学法不是从头背文法,而是按这个顺序:
先会写小程序,学会 const/var、函数、数组、slice、打印。
然后重点吃透 ?T、!T、try、defer。
再去理解 allocator、测试、comptime。
最后再看构建系统和更复杂的标准库。
因为 Zig 真正难的从来不是“写出能跑的代码”,而是理解代码为什么这样写才算清楚。
官方文档和学习页的整体设计,其实也是围绕这条线:把控制流、内存、错误、构建这些本该重要的事情,都放在你看得见的地方。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!