Learning Zig
I've decided to learn Zig over the Christmas holidays.
Zig is a low-level systems language with explicit memory management and error handling, alongside a sophisticated compile-time (comptime) functionality. It's roughly in the same niche as Rust.
Why Zig
Why not Rust?
I've started, and abandoned, learning Rust several times over the years. I just don't like it — this is a personal, subjective preference. I don't have anything against the language, I just don't like to use it. Rust has been touted as “a better C++” and in my (admittedly limited) experience, that's exactly right — and that's just the problem.
Personally, I like small languages, with minimal surface area, that keep things (mostly) explicit. Languages like C, Go, Scheme, or Python. I dislike large languages with complex, often implicit, effects, like C++, Rust, Common Lisp, or Haskell.
I'm happy with my choice of Python and Go. I haven't used Scheme in a long time (since R6RS!), because the batteries-included aspect of Python (and increasingly Go) just trounces it. And while C is still the lingua franca (literally: most other languages interop using C ABI), it shows its age, especially around (non)safety and minimalistic standard library.
Zig looks like it might fit my preferences perfectly.
I first noticed Zig a couple of years ago. I didn't really have the need to learn it yet, but figured it'd be a fun thing to do over the holidays!
Hello World
Here's a hello world in Zig:
// "std" is the complete Zig standard library
const std = @import("std");
// Defining a public entry point function that returns
// nothing (void) or an error (that's the ! part)
pub fn main() !void {
// init a "writer" object (struct) with empty buffer (.{}) over stdout
var w = std.fs.File.stdout().writer(&.{});
const stdout = &w.interface;
// format the message using provided tuple and print it
// "try" doesn't catch, it immediately returns error to caller
try stdout.print("Hello {s}!\n", .{"world"});
}
The C equivalent is actually shorter:
#include <stdio.h>
void main() {
printf("Hello %s!\n", "world");
}
Hello world is too small, so it doesn't touch on the memory management, but even in this small example there are some benefits:
- In C, you have to know
printfis fromstdio, whilestd...is explicit in Zig. - I have to explicitly handle the error (by choosing to propagate it further with
try) in Zig, while I can happily ignore any runtime issues withprintfin C. - Format string and arguments are statically type-checked in Zig. C allows you to pass garbage data.
Things get more interesting when memory management comes into play. To give you a hint:
const std = @import("std");
pub fn main() !void {
// Initialize the memory allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// At the end of this block, deinitialize the allocator
defer _ = gpa.deinit();
const allocator = &gpa.allocator();
// Allocate 1024 bytes and handle errors
const buffer: []u8 = allocator.alloc(u8, 1024) catch |err| {
std.debug.print("Memory allocation failed: {}\n", .{err});
return;
};
// At the end of this block, free the allocated buffer
defer allocator.free(buffer);
// Initialize stdin reader with the provided buffer
var reader = std.fs.File.stdin().reader(buffer);
const stdin = &reader.interface;
std.debug.print("What's your name: ", .{});
// "name" is a borrowed slice from somewhere within buffer
const name = stdin.takeDelimiterExclusive('\n') catch "";
std.debug.print("Hello, {s}!\n", .{name});
// Defer runs in FIFO order from the "defer" statements
}
This is a bunch of work! In Zig, any function that needs to allocate memory takes a memory allocator struct as the parameter, making the memory management front and center via dependency injection.
Learning strategy
The official Zig docs are hit and miss. The Documentation button links to the language reference, which is not really structured for beginners. The Getting started button references zig.guide, which is out of date (see below why that's a big thing). The Learn section of the site does list several more documentation resources.
After a few false starts, I found (also on that page) Introduction to Zig, an open-source and up-to-date book, which looks pretty solid so far.
My current learning strategy is loosely following the book. I often spend more time chasing down rabbit holes (for example in UTF8 handling) I spot in a specific chapter. I'm also working on a toy busybox clone, giving me simple tasks for file and string handling. The idea is to get immersed into the language and get it into my muscle memory.
I am using LLMs as TAs, asking for more background or rationale on some detail, for example why literal strings are null-terminated in Zig (spoiler: for easy C integration).
Bleeding edge
The current version of Zig, as I write this, is 0.15.2. There was a major change in 0.15 related to how reading from and writing to files works. In 0.16 this will change again, as several parts of the standard library are reorganized.
This proved to be a real blocker for me initially. I accidentally downloaded master (future 0.16) with some of the breaking changes in, and the documentation (for 0.15 or earlier versions) was extremely confusing.
With the language very much in flux, is there a point in trying to learn it now?
I would be skeptical, if not for examples of high-quality software written in Zig, like TigerBeetle and Ghostty. To me, these show the changes are not so insurmountable and perhaps it's a good time to start learning the language. In a few years, when the language is ready, so will I!
Do I plan to write production code in Zig? Not currently — that's certainly not my motivation right now. In a few years? Who knows!