This is inspired by https://fasterthanli.me/blog/2020/a-half-hour-to-learn-rust/
the command zig run my_code.zig will compile and immediately run your Zig program. Each of these cells contains a zig program that you can try to run (some of them contain compile-time errors that you can comment out to play with)
You'll want to declare a main() function to get started running code.
This program does almost nothing:
You can import from the standard library by using the @import builtin and assigning the namespace to a const value. Almost everything in zig must be explicitly assigned its identifier. You can also import other zig files this way, and C files in a similar fashion with @cImport.
Note:
- I'll explain the funny second parameter in the print statement, later in the structs section.
var declares a variable, in most cases you should declare the variable type.
const declares that a variable's value is immutable.
Zig is very picky and will NOT let you shadow identifiers from an outside scope, to keep you from being confused:
Constants in the global scope are by default compile-time "comptime" values, and if you omit the type they are comptime typed and can turn into runtime types for your runtime values.
You can explicitly choose to leave it undefined if it will get set later. Zig will fill in a dummy value with 0XAA bytes to help detect errors in debug mode if you cause an error from accidentally using it in debug.
In some cases, zig will let you omit the type information if it can figure it out.
But be careful, integer literals are comptime-typed, so this won't work:
Here's a function (foo) that returns nothing. The pub keyword means that the function is exportable from the current scope, which is why main must be pub. You call functions just as you would in most programming languages:
Here's a function that returns an integer value:
Zig won't let you ignore return values for functions:
but you can if you assign it to the throw-away _.
You can make a function that can take a parameter by declaring its type:
structs are declared by assigning them a name using the const keyword, they can be assigned out of order, and they can be used by dereferencing with the usual dot syntax.
structs can have default values; structs can also be anonymous, and can coerce into another struct so long as all of the values can be figured out:
You can drop functions into an struct to make it work like a OOP-style object. There is syntactic sugar where if you make the functions' first parameter be a pointer to the object, it can be called "Object-style", similar to how Python has the self-parametered member functions. The typical convention is to make this obvious by calling the variable self.
By the way, that thing we've been passing into the std.debug.print's second parameter
is a tuple. Without going into too much detail, it's an anonymous struct with number fields.
At compile time, std.debug.print figures out types of the parameters in that tuple and
generates a version of itself tuned for the parameters string that you provided, and that's
how zig knows how to make the contents of the print pretty.
Enums are declared by assigning the group of enums as a type using the const keyword.
Note:
- In some cases you can shortcut the name of the Enum.
- You can set the value of an Enum to an integer, but it does not automatically coerce, you have to use @enumToInt or @intToEnum to do conversions.
zig has Arrays, which are contiguous memory with compile-time known length. You can initialize them by declaring the type up front and providing the list of values. You can access the length with the len field of the array.
Note:
- Arrays in zig are zero-indexed.
zig also has slices, which are have run-time known length. You can construct slices from arrays or other slices using the slicing operation. Similarly to arrays, slices have a len field which tells you its length.
Note:
- The interval parameter in the slicing operation is open (non-inclusive) on the big end.
Attempting to access beyond the range of the slice is a runtime panic (this means your program will crash).
string literals are null-terminated utf-8 encoded arrays of const u8 bytes.
Unicode characters are only allowed in string literals and comments.
Note:
- length does not include the null termination (officially called "sentinel termination")
- it's safe to access the null terminator.
- indices are by byte, not by unicode glyph.
const arrays can be coerced into const slices.
Zig gives you an if statement that works as you would expect.
as well as a switch statement
Zig provides a for-loop that works only on arrays and slices.
Zig provides a while-loop that also works as you might expect:
Errors are special union types, you denote that a function can error by prepending ! to the front. You throw the error by simply returning it as if it were a normal return.
If you write a function that can error, you must decide what to do with it when it returns. Two common options are try which is very lazy, and simply forwards the error to be the error for the function. catch explicitly handles the error.
- try is just sugar for catch | err | {return err}
You can also use if to check for errors.
Pointer types are declared by prepending * to the front of the type. No spiral declarations like C! They are dereferenced, with the .* field:
Note:
- in Zig, pointers need to be aligned correctly with the alignment of the value it's pointing to.
For structs, similar to Java, you can dereference the pointer and get the field in one shot with the . operator. Note this only works with one level of indirection, so if you have a pointer to a pointer, you must dereference the outer pointer first.
Zig allows any type (not just pointers) to be nullable, but note that they are unions of the base type and the special value null. To access the unwrapped optional type, use the .? field:
Note:
- when you use pointers from C ABI functions they are automatically converted to nullable pointers.
Another way of obtaining the unwrapped optional pointer is with the if statement:
Zig's metaprogramming is driven by a few basic concepts:
- Types are valid values at compile-time
- most runtime code will also work at compile-time.
- struct field evaluation is compile-time duck-typed.
- the zig standard library gives you tools to perform compile-time reflection.
Here's an example of multiple dispatch (though you have already seen this in action with std.debug.print, perhaps now you can imagine how it's implemented:
Here's an example of generic types:
From these concepts you can build very powerful generics!
Zig gives you many ways to interact with the heap, and usually requires you to be explicit about your choices. They all follow the same pattern:
- Create an Allocator factory struct.
- Retrieve the std.mem.Allocator struct creacted by the Allocator factory.
- Use the alloc/free and create/destroy functions to manipulate the heap.
- (optional) deinit the Allocator factory.
Whew! That sounds like a lot. But
- this is to discourage you from using the heap.
- it makes anything which calls the heap (which are fundamentally failable) obvious.
- by being unopinionated, you can carefully tune your tradeoffs and use standard datastructures without having to rewrite the standard library.
- you can run an extremely safe allocator in your tests and swap it out for a different allocator in release/prod.
Ok. But you can still be lazy. Do you miss just using jemalloc everywhere?
Just pick a global allocator and use that everywhere (being aware that some allocators are
threadsafe and some allocators are not)! Please don't do this if you are writing
a general purpose library.
In this example we'll use the std.heap.GeneralPurposeAllocator factory to create an allocator with a bunch of bells and whistles (including leak detection) and see how this comes together.
One last thing, this uses the defer keyword, which is a lot like go's defer keyword! There's also an errdefer keyword, but to learn more about that check the Zig docs (linked below).
That's it! Now you know a fairly decent chunk of zig. Some (pretty important) things I didn't cover include:
- tests! Dear god please write tests. Zig makes it easy to do it.
- the standard library
- the memory model (somewhat uniquely, zig is aggressively unopinionated about allocators)
- async
- cross-compilation
- build.zig
For more details, check the latest documentation: https://ziglang.org/documentation/master/
or for a less half-baked tutorial, go to: https://ziglearn.org/
.png)

