From Go to Zig

Getting to know Zig, a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.

My friend Christine has spoken well of Zig for a while now, and my interest was piqued when I read that it can integrate with C libraries without the use of FFI/bindings. In this article I will try to contrast Zig against my favorite language, Go.

(I will leave it to others to compare Zig with C++, D or Rust)


Similarities with Go…

Let’s start off by listing some of the characteristics shared between Go and Zig:

  • Ahead-of-time (AOT) compiled
  • Open Source
  • Statically typed
  • Cross-compilation
  • No operator overloading
  • Tool for one canonical code formatting style (go fmt and zig fmt)
  • Errors are values
  • WebAssembly as a compile target
  • Multiline string literals
  • defer
  • Source code is encoded in UTF-8

How is it different from Go?

Note: These are just a few of the things I’ve picked up when reading about Zig. I still have a lot to learn before I feel confident in using the language.

  • Build modes on scope level
  • Zig Build System
  • Manual memory management
  • Algebraic data types (Union and Enum types!)
  • Optional type instead of null pointers
  • Can output tiny binaries
  • Generics and compile-time code execution
  • First-class support for no standard library
  • Somewhat easier to Google
  • ~zero overhead when calling out to C
  • “Zig is better at using C libraries than C is at using C libraries.”
  • Enforcing the handling of returned errors
  • Arbitrary bit-width integers (i7 for example)
  • Undefined values
  • Thread local variables
  • No multiline comments

The Zen of Zig

  • Communicate intent precisely.
  • Edge cases matter.
  • Favor reading code over writing code.
  • Only one obvious way to do things.
  • Runtime crashes are better than bugs.
  • Compile errors are better than runtime crashes.
  • Incremental improvements.
  • Avoid local maximums.
  • Reduce the amount one must remember.
  • Minimize energy spent on coding style.
  • Resource deallocation must succeed.
  • Together we serve end users.

Installing Zig

If you are using macOS and have Homebrew installed, then you can install Zig like this:

$ brew install zig

The current version of zig as of this writing is 0.6.0.

Make sure you also get zig.vim.

Hello, World!

The entry point of a Zig binary is the main fn. The binary is named after the .zig file.

zighello/zighello.zig

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().outStream();
    try stdout.print("Hello, {}!\n", .{"world"});
}

This can be compiled with zig build-exe zighello.zig --release-small --strip --single-threaded and results in a ~10 KB static executable.

(Not strictly true that you get a static executable under macOS, it is linked to libSystem.B.dylib)

And as you probably know, the Go entry point is the main func.

gohello/main.go

package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}

This can be compiled with go build -ldflags "-s -w" -trimpath main.go and results in a ~1.6 MB executable (when using Go version 1.15 under macOS)

Note: The size of your binaries doesn’t matter in most cases, but sometimes it matters a lot, for example when compiling to WebAssembly

Learn more about Zig

Bonus: Linking to the shared library from the article Go and Ruby-FFI

An example of importing a shared library from Zig, using the @cInclude feature:

const std = @import("std");

const c = @cImport({
    @cInclude("libsum.h");
});

pub fn main() void {
    const sum: i64 = c.add(4, 2);

    std.debug.warn("{}\n", .{sum});
}

Then you can compile this by calling zig -isystem . --library ./libsum.so build-exe zigsum.zig


Captain: Take off every ‘ZIG’!!