Anatomy of a Zig file
Zig is a modern, general-purpose systems programming language focused on robustness, optimality, and clarity, with no hidden control flow.
File extensions: .zig
Every part of the example below is labelled and explained. This page is one of 55 annotated tours on AnatomyOf, a free, open-source project by LunarWerx Studios.
What is inside a Zig file
Comment
Single-line (//) or doc comments (///), ignored by the compiler.
A double slash starts a comment that runs to the end of the line -- Zig has no block comment syntax, so every comment is single-line by construction. Three slashes (///) mark a doc comment attached to the declaration immediately below it, and two slashes with a bang (//!) write a doc comment for the containing file or module itself. Doc comments are picked up by zig autodoc to generate reference documentation directly from source. Because there is no /* ... */ form, commenting out a block means prefixing every line, which nudges toward small, deletable comments rather than large ones that quie
Import (@import)
Includes another file or package as a value.
@import is a compiler builtin, not a keyword, hence the leading @. @import("std") returns the standard library as a struct-like value that you bind to a name with const; @import("root") reaches the top-level file of the current compilation, and a relative path like @import("util.zig") pulls in a sibling file as its own module. Because the result of @import is an ordinary value, it can be stored, passed around, and referenced with . just like any other struct namespace -- there is no separate "module" grammar to learn. The same mechanism is how build.zig.zon dependencies get named and pulled in
Struct definition
Defines a custom data type with fields using struct.
const Point = struct { ... } declares an anonymous struct type and binds it to the name Point. Structs group related fields together and, unlike some languages, carry no implicit padding guarantees unless you ask for extern struct (C-compatible layout) or packed struct (bit-exact layout). Methods live inside the struct body as ordinary functions that take the instance as their first parameter (conventionally named self), so struct doubles as Zig's class-like construct without a separate class keyword. Struct literals are written Point{ .x = 10, .y = 20 }, with the type sometimes elided as .{ .
Field
A named, typed component of a struct.
Each field pairs a name with an explicit type, such as x: i32 for a signed 32-bit integer -- Zig never infers a struct's shape, so every field must be annotated. Fields may declare a default value (x: i32 = 0), which is used whenever a struct literal omits that field. Fields are accessed with dot notation (p.x) and can only be reassigned through a var-bound instance; a const instance is fully immutable, fields included. There is no field-level visibility keyword -- an entire declaration is made private to its file simply by omitting pub.
Variable declaration (const/var)
const for immutable, var for mutable bindings.
const declares a binding whose value can never change after initialization; var opts into mutation. The compiler enforces this at compile time and will refuse to build if a var is never actually mutated, nudging every binding toward the most restrictive form that still compiles. Type is usually inferred from the initializer, as in var sum: i32 = 0, but can be written explicitly after a colon when inference would be ambiguous, such as with integer literals that need a specific bit width. Top-level const and var declarations are evaluated at compile time and may appear in any order in the file -
Global constant
A const declared at the top level of the file.
A top-level const sits outside any function and is visible to every declaration in the file, evaluated once at compile time rather than on each access. Prefixing it with pub (pub const stdout = ...) exports it so other files can reach it through @import. Because top-level order does not matter in Zig, a global constant can reference a function or type defined later in the same file -- the compiler resolves the whole file's declarations as a single graph before checking any of them.
Main function (pub fn main)
The public entry point, optionally returning an error union.
pub fn main() !void { ... } is the entry point an executable starts running from; pub makes it visible to the compiler driver that links the binary, since Zig has no separate "linker sees everything" default. The bare !void return type is shorthand for an inferred error set unioned with void -- the compiler works out exactly which errors the body can produce. If main returns an error, the runtime prints it and exits with a nonzero status, which is a lightweight way to propagate startup failures without wrapping everything in your own try/catch scaffolding at the very top. A main that never fai
Error handling (!T, try)
Explicit, value-based error propagation -- no exceptions.
An error union type, written !T (or ErrorSet!T when the set is named explicitly), means a function returns either a successful T or one of a fixed set of error values -- errors are ordinary values, not a separate control-flow channel like exceptions. try expr is sugar for "evaluate expr, and if it is an error, return that error immediately from the current function," which is how errors climb back up the call stack one try at a time. catch is the other side of the same coin: expr catch |err| { ... } or expr catch default_value lets a caller handle or replace an error instead of propagating it.
Control flow (loop)
Executes code repeatedly with for or while.
for (0..5) |i| { ... } iterates a range where the upper bound is exclusive, binding each value to i; for can also walk one or more slices or arrays directly, in lockstep when given several. while (condition) { ... } repeats as long as condition holds, and both loop forms support an optional continue expression, as in while (i < 10) : (i += 1) { ... }. Every numeric loop variable and slice element in Zig is explicitly typed, so a loop over 0..5 yields usize values by default -- reaching for @intCast is common when the loop body needs to combine that index with a differently-sized integer. break
Function call (standard library)
Invokes a function from the imported std library.
std.io.getStdOut().writer() walks the standard library namespace to obtain a writer attached to standard output, and .print(fmt, args) formats and writes to it, following printf-style placeholders such as {} for default formatting and {s} for strings. Because print can fail (the underlying write can fail), it returns an error union and is almost always called with try. The standard library itself is just Zig source compiled along with your program -- there is no hidden runtime magic, so std.debug.print, std.mem, and friends can all be read, and even stepped through in a debugger, the same as y
Official Zig site · All languages on AnatomyOf
The interactive tour needs JavaScript. Enable it to hover a callout and trace it into the code.