A review of Zig from a Gopher
By Caleb Gardner
Written on: 2025-10-15
Updated on: 2025-10-25
Lately I've been learning Zig and now that I feel relatively comfortable with the language, I thought I might write down some of my thoughts. For some background, Zig is my first "low level" language, but I know several languages, including JS, Python, Dart, and of course, Go. Of the languages I know, Go is easily my favorite and the one I know the best.
I am writing this when Zig is on version 0.15.2; the language will continue to change (and it's obvious they aren't afraid of relatively large changes) and it might not reflect the language when it stabilizes in it's final form.
What I've been doing
Every language has it's strengths and weaknesses, so any viewpoint of a language should come with the context of what they're using it for. For my introduction to the language, I decided to write something I'm fairly familiar with: a squashfs library. As a note, if you are familiar with Zig and want to roast my library, feel free; I want to get better.
The instabilities of a language in development
With v0.15, Zig completely re-wrote how they handled reading and writing data so that it's more interoperable, performant, and doesn't cause everything it touches to be generic. The change is a good one, but it took a while to integrate into my library. Not only due to my library's heavy reliance on io, but also due to the lack of documentation. If fact, the only way I could find out how to make a new Reader instance was by looking at the standard library source code. I'm confident that the documentation will get fully fleshed out eventually, but it's probably not worth it for the devs until things stabilize completely.
I suspect this won't be the first breaking change, and it will require further large re-writes. This isn't a bad thing (in fact, for a developing language, it's a good thing), but nearly every project will require far more maintenance then if a stable language was chosen.
The good
defer, errdefer, and the debug allocator helps you to be memory safe without restricting what you can do. No it doesn't prevent unsafe memory usage, but it does make it far easier without limiting your capabilities. Having only ever used GC languages before, I have found it easy to handle memory in a safe way while still allowing more complex uses. You have training wheels when you want them, but you aren't force into it. The only real negatives is that these benefits mainly come into play during debug builds and tests. That being said, no language can fix all errors and testing (whether with proper tests or ad-hoc usage) should be a regular part of any development process anyway.
comptime is an interesting concept and is used in interesting ways. I haven't had a good reason to use it in my current project, but it's really powerful and the way it's used for generics is honestly great. In addition, the anytype type can be very powerful if you know what you're doing. I hope I'll have a reason to use these in the future because it's a very interesting concept.
Using Zig for the build script is truly amazing and with comptime allows for some really cool compilation tricks.
Especially since I'm coming from Go, I really appreciate the restraint that Zig has when it comes to types, while still having the flexibility to do some really cool and useful tricks. A simple instance of Zig's great type system is 'packed struct's. Normal 'struct's have a non-guaranteed memory layout to allow for comptime optimizations, but the packed variant guarantees an exact memory layout, and, as a bonus, booleans actually only use 1bit instead of the usual 8+. That means that decoding bitfields are a breeze compared to, at least, Go. Additionally Zig supports arbitrary length integers; you aren't stuck with the usual u16 and u32, you can have fun with u12s or u29s. Combine these with packed structs and you have an extremely easy way to decode binary data. As an example, squashfs data block sizes are represented as u32s, but only the lower 24 bits are used for the size with the 25th bit representing if the block is compressed or not. Instead of having to do a bitwise operation every time, I instead can do this:
pub const DataBlock = packed struct {
size: u24,
uncompressed: bool,
_: u7,
};
Nice and simple and has been a HUGE benefit for my squashfs library.
Another "nice and have poked at, but I don't need it at this exact moment" is Zig's easy integration with C libraries. Whether you like it or not, C is the language of Linux and if you need to use some random library, it probably has a C interface and it's C interface is probably the most performant. I don't see this changing anytime soon. I'd also like to add that I have also poked at CGO (C in Go) and found the experience to be far worse then Zig's implementation.
No operation overloading. Yes this means that working with strings (or more specifically []u8s) is a bit annoying, but it's worth it to know you aren't "invisibly" allocating memory.
It's not Javascript.
The meh
As the language has progressed, it seems like it's becoming more and more reliant on builtin functions (prefixed by @). For instance, the new Reader/Writer system requires the use of @fieldParentPtr to actually interact with the parent type, which is often necessary from my experience. Last update they also changed the Linked List implementation so that, instead of the nodes being generic and holding the data, the data holds the node and then you access it via @fieldParentPtr. This isn't necessarily a bad thing, and it does help to reduce the number of generics used (which I do think is a good thing), but the way they're implemented means that they aren't as self-explanatory and you need to read the documentation to understand how to use these basic data types. You can also end up in situations where you have to do multiple builtin cast functions to get what you want, such as @ptrCast(@alignCast(@constCast(someVar))).
Probably my most controversial take is that Go does error handling better. That's not to say that Zig's handling is bad (anything that doesn't throw error and requires try-catch is good in my book), but the way it's implemented means that when you are writing code it can be non-obvious if there is an error to be handled or not. As an example, without looking at the function signitures, can you tell which of these lines returns an error and is missing try in front of it?
var cnst1 = doSomething();
var cnst2 = doSomethingElse();
Obviously not, without looking at what the functions return. In fact, neither line is wrong because it's completely valid to assign a error union to a variable and then handle the potential error later so you won't even get a red squiggle to tell you you've made a mistake. Yes it will usually be caught at compile time, but it would be nice to know earlier. Meanwhile in Go:
val := doSomething()
val, err := doSomethingElse()
It's obvious where the error is and, as a bonus, Go forces you to explicitly handle (or not) the error. In Zig it's far too easy to just try and then let someone else deal with the problem. Zig's way isn't bad and, admittedly, looks nicer, but I've already forgotten try enough for it to be a mild irritation. I would also say that a Go if err != nil block is nicer to look at then Zig's catch |err| when you do actually handle errors, but I know that part is mostly preference at this point.
A couple of (admittedly stupid) syntax things I dislike far more then I should:
- Using
andandorinstead of&&and||is bad. - You can't make a traditional
for(var i = 0; i < 10; i++)loop. Just kinda feels wrong. - No
i++&i--.i += 1is obviously inferior.
* No println only print in the standard library. Yes I know ln and \n is the same number of characters.
About the author:
Caleb Gardner
I love any thing to do with computers, from building them to programming them, it's been a passion since I was a child. My first foray into programming was on my Casio fx-9750GII graphing calculator in 5th grade after reading the user manual. Somehow, it would take me years to realize that I was programming.