What Zig felt like, coming from Rust

What Zig felt like, coming from Rust

从 Rust 转向 Zig 的体验

Intro

引言

I’ve spent the last 7 years as a Rust developer, working mostly on open source projects, and I’d like to think I’ve built a solid feel for the language and its ecosystem along the way. I gravitate toward the functional side of Rust like clean functions, expressive types, that sort of thing. But I’m always curious about other languages, and Zig has been on my radar for a while as a candidate C successor: lower-level, lighter-weight, and steadily earning its place among the languages people take seriously. I spent time with C earlier in my career, so the comparison always felt like it would be interesting to make.

过去 7 年里,我一直是一名 Rust 开发者,主要从事开源项目。我想我已经在这一过程中对这门语言及其生态系统建立了扎实的认知。我倾向于 Rust 的函数式编程特性,比如简洁的函数、富有表现力的类型系统等。但我一直对其他语言充满好奇,Zig 作为 C 语言的潜在继任者,早已进入了我的视野:它更底层、更轻量,并且正在稳步跻身于受人重视的编程语言之列。我职业生涯早期曾使用过 C 语言,因此我一直觉得将两者进行对比会是一件很有趣的事情。

One caveat worth stating up front: my experience with Zig begins with this project. Some of the observations will look naive and obvious for the people who work with Zig on daily basis and some of the decisions I made along the way were almost certainly not the optimal ones, they were shaped more by habits carried over from Rust than by deep Zig idiom. That’s fine, everyone has to start somewhere, and in the meantime I’m leaning on whatever cross-language intuition I’ve built up over the years, for better or worse.

首先需要说明一点:我对 Zig 的经验始于这个项目。对于那些每天使用 Zig 的人来说,我的一些观察可能显得幼稚且显而易见;我在此过程中做出的一些决定几乎肯定不是最优的,它们更多是受 Rust 习惯的影响,而非对 Zig 惯用法的深刻理解。这没关系,每个人都要从零开始,在此期间,我只能依靠多年来积累的跨语言直觉,无论好坏。

To make the comparison fair, I decided to reimplement something I’d already built in Rust, not a toy, but not a sprawling project either, and ideally something the community could actually use. I settled on JSONPath: a query language for JSON, specified in RFC 9535. The Rust version already existed (jsonpath-rust), and the goal was to bring the same thing to Zig: zig-jsonpath.

为了公平起见,我决定重新实现一个我已经在 Rust 中构建过的项目。它既不是一个玩具项目,也不是一个庞大的工程,而且最好是社区真正能用得上的东西。我选择了 JSONPath:一种由 RFC 9535 规范定义的 JSON 查询语言。Rust 版本已经存在(jsonpath-rust),我的目标是为 Zig 带来同样的功能:zig-jsonpath。

IDE support

IDE 支持

The first thing that caught me off guard — and honestly, who would’ve expected this to be the memorable part — was IDE support, or the near-total lack of it. I’d been using RustRover for Rust and various JetBrains flavors for other languages, and Zig, by comparison, offered little beyond syntax highlighting and basic autocompletion. It wasn’t exactly surprising, but it did force me back to basics: learning to work with the language largely from the command line. What started as a drawback turned into one of the more interesting parts of the experience. It turns out I’d simply forgotten how straightforward it can be to rely on bare CLI tooling.

第一件让我措手不及的事情——老实说,谁会想到这竟然成了最令人难忘的部分——是 IDE 支持,或者说几乎完全缺乏支持。我之前一直使用 RustRover 开发 Rust,并使用各种 JetBrains 系列工具开发其他语言。相比之下,Zig 除了语法高亮和基本的自动补全外,几乎没有提供什么支持。这并不令人意外,但它确实迫使我回归基础:学习主要通过命令行来使用这门语言。起初这被视为一个缺点,但后来却成了这段经历中最有趣的部分之一。事实证明,我只是忘记了仅仅依赖纯粹的命令行工具是多么直接高效。

The first real lesson here was build.zig, which handles this with surprising ease. I eventually settled on this setup: zig build test # run all tests zig build test -Dfilter="filter match function basic" # run one test zig build test -Ddebug-query=true # all tests with debug zig build compliance # compliance suite zig build check # unit tests + compliance Once you accept the terms, it’s genuinely refreshing to work with.

这里学到的第一个真正的教训是 build.zig,它处理这些任务的简便程度令人惊讶。我最终确定了这样的配置: zig build test # 运行所有测试 zig build test -Dfilter="filter match function basic" # 运行单个测试 zig build test -Ddebug-query=true # 运行所有带调试信息的测试 zig build compliance # 运行合规性测试套件 zig build check # 运行单元测试 + 合规性检查 一旦你接受了这种方式,你会发现它用起来确实令人耳目一新。

I have Zig to thank, in a roundabout way, for kicking off a bigger chain reaction, namely my move away from a full IDE toward a helix + alacritty + zellij setup.

我得感谢 Zig,它以一种迂回的方式引发了一场更大的连锁反应,即我从全功能 IDE 转向了 helix + alacritty + zellij 的组合。

Flat structure

平铺式结构

With Rust, and most other languages, I’ve always spent a fair amount of time (going back and forth) trying to find the right balance between file size and folder depth. You’re free to fragment files and grow the folder hierarchy as deep as you like. Zig, it turned out, is fine with this too, but somehow doesn’t really encourage it (like C, which is no surprise for a low-level systems language). You can nest files and folders if you want, but doing so brings a bit of import friction, and the real question becomes: why bother? What do you actually gain in readability by splitting everything across more files and folders? In theory, better readability. In practice, when you collapse related things into one larger file, you can just slice it and navigate section by section instead and there’s a real benefit to having everything in one place. Mostly, Zig nudges you toward flat. If something needs a companion for a model, I just create a model_<companion> file next to it and move on.

在使用 Rust 和大多数其他语言时,我总是花费大量时间(反复权衡)来寻找文件大小与文件夹深度之间的平衡。你可以随意拆分文件,并将文件夹层级嵌套得尽可能深。事实证明,Zig 也可以这样做,但它并不鼓励这种做法(就像 C 语言一样,对于一门底层系统语言来说这并不奇怪)。如果你愿意,你可以嵌套文件和文件夹,但这样做会带来一些导入上的麻烦,真正的问题在于:何必呢?将所有内容拆分到更多的文件和文件夹中,在可读性上到底获得了什么?理论上是更好的可读性。但在实践中,当你将相关内容合并到一个较大的文件中时,你可以直接分段浏览,将所有内容放在一处确实有其优势。总的来说,Zig 倾向于让你保持平铺结构。如果某个模型需要配套文件,我只需在它旁边创建一个 model_<companion> 文件即可。

I don’t think this scales to large projects, meaning at some point you need a real hierarchy but the threshold for needing one turned out to be much higher in Zig than I expected. In Rust, I tend to reach for folder structure early, almost by default. In Zig, I kept deferring it, and by the end of this project, I never needed it at all.

我不认为这种方式适用于大型项目,这意味着在某个阶段你确实需要真正的层级结构,但在 Zig 中,需要这种结构的门槛比我预期的要高得多。在 Rust 中,我倾向于很早就使用文件夹结构,几乎是默认行为。而在 Zig 中,我一直推迟这样做,直到项目结束,我根本没用到它。

That contrast was useful beyond just Zig, because it made me reconsider, even in other languages, whether I’m organizing files because the project genuinely needs it, or out of habit. It’s also a pretty honest way to gauge how big a project actually is: if you can’t resist reaching for folders on day one, maybe it’s smaller than it feels.

这种对比不仅对 Zig 有用,还让我重新思考:在其他语言中,我组织文件是因为项目确实需要,还是仅仅出于习惯?这也是衡量项目规模的一种诚实方式:如果你第一天就忍不住要创建文件夹,也许它比你感觉的要小得多。

(Side-by-side comparison omitted for brevity) (此处省略对比代码结构图)

Tests

测试

Setting the rfc9535 compliance suite aside for now and focusing purely on the language itself: In Rust, I tend to stick with two approaches to testing:

  1. Inline unit tests, living in the same file or same folder as the code they cover. This is the convenient default always there, no extra setup.
  2. Integration tests, in an independent folder (like tests) outside the main source tree. This is the exception not the default, and sometimes absent altogether.

暂时撇开 rfc9535 合规性测试套件,仅关注语言本身: 在 Rust 中,我倾向于坚持两种测试方法:

  1. 内联单元测试,与它们所覆盖的代码位于同一个文件或同一个文件夹中。这是默认且便捷的方式,无需额外配置。
  2. 集成测试,位于主源码树之外的独立文件夹(如 tests)中。这是例外情况而非默认,有时甚至完全不需要。

I expected roughly the same split from Zig. On paper, it looks similar: you can write tests directly inside the same file. The problem, at least for me, was verbosity. Given the flat structure I’d already settled into, I was left with two options, either a separate model_test file per model, or tests inlined directly into the model file itself. Both approaches ended up cluttering things: either the individual files or the main folder as a whole.

我原以为 Zig 也会有大致相同的划分。从理论上看,它看起来很相似:你可以直接在同一个文件中编写测试。但对我来说,问题在于冗长。鉴于我已经采用了平铺结构,我只剩下两个选择:要么为每个模型创建一个单独的 model_test 文件,要么将测试直接内联到模型文件中。这两种方法最终都会导致混乱:要么是单个文件变得臃肿,要么是主文件夹变得杂乱。

I went with the second option, which meant configuring it explicitly in build.zig. Once that was wired up, though, it… (Text cuts off)

我选择了第二种方案,这意味着需要在 build.zig 中进行显式配置。然而,一旦配置完成,它…… (原文在此处中断)