liminfo

Zig Reference

Free reference guide: Zig Reference

29 results

About Zig Reference

The Zig Reference is a comprehensive, searchable guide to the Zig programming language, a systems programming language designed as a practical alternative to C and C++ with a focus on explicit memory management, compile-time evaluation, and zero hidden control flow. This reference covers Zig's core syntax including const/var declarations, if/else expressions (not statements), while/for loops with capture syntax, switch expressions with range patterns, function definitions, structs with methods, enums, and tagged unions.

This reference organizes Zig's features into six critical categories: Basic Syntax (declarations, control flow, functions, structs, enums, unions), Types (integer types from u8 to u64/usize, floats, optionals with ?T and payload capture via if-unwrap, slices []T, and pointers *T), Memory (allocator interface, ArrayList, defer/errdefer for deterministic cleanup, @memcpy/@memset builtins), Error Handling (error unions !T, try for propagation, catch for recovery, named error sets), Comptime (compile-time execution blocks, comptime parameters for generic programming, @typeInfo reflection, inline for loop unrolling), and Testing (test blocks, expect/expectEqual assertions, test filtering, std.testing.allocator for leak detection).

Zig stands apart from other systems languages through its comptime system that replaces macros, templates, and generics with a single unified mechanism. Functions can accept comptime parameters to generate specialized types at compile time, @typeInfo enables reflection-based generic programming, and inline for unrolls loops at compile time for each struct field. Combined with explicit allocator passing (no hidden allocations), error unions that make error handling visible in the type system, and defer/errdefer for deterministic resource cleanup, Zig provides the low-level control of C with modern safety features.

Key Features

  • Complete integer type system from u8/i8 to u64/i64 with usize, hex (0xFF), octal (0o77), and visual separators
  • Optional type (?T) with if-unwrap payload capture syntax for null-safe value extraction
  • Error union (!T) system with named error sets, try propagation, and catch with default values
  • Comptime metaprogramming with compile-time function parameters, @typeInfo reflection, and inline for unrolling
  • Allocator-based memory management with page_allocator, ArrayList, and defer/errdefer cleanup patterns
  • Tagged union (union(enum)) for type-safe variant types with switch-based pattern matching
  • Built-in test framework with test blocks, std.testing.expectEqual assertions, and memory leak detection via std.testing.allocator
  • Slice ([]T) and pointer (*T) types with bounds-checked indexing and explicit dereferencing via ptr.*

Frequently Asked Questions

How does Zig's comptime system replace macros and generics?

Zig's comptime keyword allows any code to execute at compile time using the same language, eliminating the need for separate macro syntax or template metaprogramming. Functions can accept comptime parameters (e.g., comptime T: type) to generate specialized types, effectively implementing generics. For example, fn Matrix(comptime T: type, comptime rows: usize, comptime cols: usize) type returns a struct with a data field of type [rows][cols]T. The @typeInfo builtin enables reflection, and inline for unrolls loops over struct fields at compile time.

What is the difference between defer and errdefer in Zig?

defer executes its expression when the enclosing scope exits, regardless of whether the function returns normally or with an error. It is used for deterministic resource cleanup like file.close() or allocator.free(). errdefer only executes when the function returns an error, making it ideal for cleanup that should only happen on failure paths. For example, after allocating a buffer, errdefer allocator.free(buf) ensures the buffer is freed if a subsequent operation fails, but keeps it allocated on success.

How do error unions and try/catch work in Zig?

Error unions (ErrorSet!PayloadType) encode both success and failure in the return type, making error handling visible in the type system. The try keyword propagates errors upward: const data = try readFile("input.txt") returns the error to the caller if readFile fails. The catch keyword handles errors locally: compute() catch 0 provides a default value, or catch |err| { ... } runs recovery logic. Named error sets (const E = error{NotFound, PermissionDenied}) define domain-specific error types.

How does Zig's optional type (?T) prevent null pointer errors?

Optional types (?T) explicitly encode the possibility of absence in the type system. A ?i32 can hold either an i32 value or null. To access the inner value, you must use if-unwrap: if (x) |val| { use(val); } which only enters the block when x is non-null, with val bound to the unwrapped value. This eliminates null pointer dereferences at compile time because you cannot accidentally use an optional without checking it first.

Why does Zig require explicit allocator passing instead of using malloc?

Zig requires passing allocators explicitly to make all memory allocations visible and controllable. There is no hidden global allocator or implicit heap allocation. This design enables swapping allocators per context: use page_allocator in production, std.testing.allocator in tests (which detects memory leaks), arena allocators for batch allocations, or fixed-buffer allocators for stack-based allocation. It also makes code auditable since every allocation site is explicitly visible in the source.

What are tagged unions in Zig and how are they used?

Tagged unions (union(enum)) combine a union with an enum discriminant, enabling type-safe variant types. Declaring const Value = union(enum) { int: i32, float: f64, none: void } creates a type that can hold exactly one of the variants at a time. Switch expressions on tagged unions are exhaustive, meaning the compiler ensures all variants are handled. This is Zig's approach to sum types, commonly used for representing values that can be one of several different types.

How does Zig's built-in test framework work?

Zig has a first-class test framework built into the language. Test blocks are declared with test "name" { ... } directly in source files alongside the code they test. Assertions use try std.testing.expectEqual(expected, actual) and try std.testing.expect(condition). Running zig test src/main.zig executes all tests, and --test-filter allows running specific tests. The std.testing.allocator detects memory leaks by tracking all allocations and failing the test if any are not freed.

How do slices and pointers differ in Zig?

A pointer (*T) references a single value of type T and is dereferenced with ptr.* syntax. A slice ([]T) is a fat pointer containing both a pointer to the start of a contiguous sequence and its length, enabling bounds-checked indexing. Slices are created from arrays using range syntax: arr[1..4] produces a slice of 3 elements. Const slices ([]const T) prevent modification of the underlying data. Slices are Zig's primary abstraction for working with contiguous memory regions safely.