Zig or Go: How I Choose
I have written several books on Go and one on Zig, so every time I start something new I have to decide which of the two to use. The answer is not always the same and I do not think one language is better than the other. In What Go Taught Me That Made Learning Zig Easier I wrote about how the two languages think alike. This post is about the other side, the things that make me pick one over the other for a given program.
When I use Go Link to heading
I use Go for network services and for programs that talk to other systems, such as databases, message queues and HTTP APIs. The Go standard library has net/http, crypto/tls, encoding/json and database/sql, so most of these programs need few or no external packages. Goroutines make it easy to give every connection its own flow of control and let the runtime deal with the threads. The tools are also part of the deal. The go test, go vet and pprof tools and the race detector come with the compiler and they work the same on every project. With Go I get from an idea to a working service faster than with any other language I know.
When I use Zig Link to heading
I use Zig for code where I want to decide where every byte of memory comes from and for small UNIX tools, such as the cat, ls and mkdir implementations on this blog. I also use it when I work with binary formats, when I call C libraries and when I want to understand what the Operating System does underneath. Zig does not hide system calls or memory from me and that makes it a better language for learning how things work.
Memory Link to heading
Go has a Garbage Collector and it is a very good one. For most servers its pauses do not matter and I would not give it up for a typical web service. Zig has no Garbage Collector. Every function that allocates takes an allocator and I decide which one, as I explained in Allocators in Zig: Which One and When. I choose Zig when the program must stay under a fixed amount of memory or when I want an arena per request that I free with a single call. If neither applies, the Garbage Collector is not a reason to avoid Go.
Concurrency Link to heading
Goroutines and channels are still the easiest way to write concurrent code that I know of. A Go server can have thousands of goroutines and the runtime spreads them over the CPU cores without any help from me. Zig gives me Operating System threads, mutexes and atomics, which I wrote about in Threads in Zig. Since Zig 0.16 it also has the std.Io interface with async and await. That is more control and more work. When a program spends most of its time waiting for the network, I choose Go. When I want to know exactly which thread does what, I choose Zig.
Binary size Link to heading
I built a Hello World program in both languages on my Mac. The Go version uses fmt.Println() and the Zig version uses std.debug.print().
| Build | Size in bytes |
|---|---|
Go, go build |
2,429,714 |
Go, go build -ldflags="-s -w" |
1,587,714 |
Zig, debug mode |
2,001,520 |
Zig, -O fast |
395,056 |
Zig, -O small |
165,088 |
Every Go executable carries the runtime, including the Garbage Collector and the goroutine scheduler. Both languages cross compile with a single command. With GOOS=linux GOARCH=amd64 go build the Go executable for Linux is 2,350,801 bytes and with zig build-exe -O small -target x86_64-linux the Zig one is 134,144 bytes. Both Linux executables are static and have no dependencies. For a server that runs in one place the difference does not matter. For a small tool that I copy to many machines or put in many containers it does.
Stability Link to heading
Since Go 1.0 in 2012, Go has promised that code written for Go 1 will keep compiling with every later Go 1 release and it has kept that promise. Zig has not reached 1.0 yet. Zig 0.16 changed the way I/O works and added a new signature for main(), so I had to rewrite a big part of my Zig book while I was still writing it. With Zig 0.17 I had to change 35 of the 152 files in the book repository. The changes were small, but every Zig release so far has needed some. If a team is going to maintain a program for years and nobody on the team wants to follow Zig releases, Go is the safer choice.
How I choose Link to heading
These are the questions I ask myself before I write the first line:
- Will other people maintain it for years? I use Go.
- Is it a network service that talks to databases, queues or other services? I use Go.
- Do I need a working version quickly? I use Go.
- Is it a small tool where the size of the executable matters? I use Zig.
- Do I need a hard limit on memory or control over every allocation? I use Zig.
- Does it call a C library or read and write binary formats? I use Zig. Go can call C with cgo, but cgo makes building and cross compiling harder.
- Do I want to learn how the Operating System works? I use Zig.
Conclusion Link to heading
Most of the time the first two questions decide it. If the program is a service that other people will run and maintain, I write it in Go. If it is a tool or a piece of code where I care about every allocation and every system call, I write it in Zig. I do not plan to stop using either of them.
My books on the subject are Practical Systems Programming in Go and Systems Programming with Zig.