This chapter offers a practical tour of core Zig concepts by building up from the anatomy of a simple program to the patterns you’ll use every day. It explains how modules and source files fit together, how to import and reuse code with @import, and why main must be public. Along the way it contrasts runtime with compile time, shows how variables work (const vs var), and emphasizes Zig’s static, strong typing with helpful type inference. You’ll define functions, return values, and use control flow; see that if/else is an expression; and encounter operators that favor safety (for example, checked integer arithmetic). The thread running through all of it is Zig’s philosophy of minimal semantics and maximum composability.
The chapter then grounds you in memory-oriented building blocks. It introduces pointers (single-item pointers, mutability tied to pointee, address-of and dereference operators) and explains narrowness and const-correctness. Arrays in Zig are values ([N]T), so copying copies data; ranges and indexing are straightforward; and generalizing across sizes is done with slices ([]T), which pair a pointer with a length and provide bounds-checked access. You’ll see coercions among pointer types, many-item pointers ([*]T), and how slices arise naturally from arrays and ranges. Practical patterns include byte buffers (often []u8) and careful use of undefined for uninitialized storage. Strings are just slices of bytes; the chapter demystifies string literals as constant pointers to nul-terminated byte arrays, making interop and performance explicit and predictable.
Finally, the chapter covers structs as Zig’s composite type and as namespaces. You declare and initialize fields, access them with dot syntax, and add functions inside structs, gaining ergonomic “method” calls when the first parameter is the struct (or a pointer to it). @This() helps write resilient APIs by naming the container type as Self. You’ll also see default field values and the configuration-struct pattern that’s common across the standard library. Throughout, Zig keeps things simple—no inheritance or virtual tables—favoring clear data layout, explicit control, and small, composable tools that scale to real systems programming.
Declaring a variable
Declaring a function
if/ else syntax
Creating an array
Summary
Every Zig project is made up of source files packaged into modules.
Zig gives you an exhaustive set of numeric and logical data types, and a full-featured set of explicit operators to interact with them.
We can semantically encode how to interact with multiple data using pointer, array, and slice types.
Strings in Zig are just bytes in memory with an encoding.
Structs are the primary mechanism in Zig to package multiple variables of potentially different types.
Zig provides ergonomic syntax "sugar" to make structs nice to work with.
FAQ
How do I import the standard library and print something?The standard library is a module named "std". Import it with the builtin @import and then call functions via dot notation. Example: const std = @import("std"); std.debug.print("{}\n", .{42});What are modules, source files, and namespaces? How do I import code from another file?A source file is Zig code that can expose pub declarations. A module is the root source file plus everything it imports. Any type that can contain declarations acts as a namespace (including structs and files). To import another file: const fibonacci = @import("fibonacci.zig").fibonacci; Ensure items you want to use from another file are marked pub.Why must main be pub, and what happens if it isn’t?Startup code in std/start.zig imports your root module and looks up root.main. If main isn’t marked pub, the compiler errors with something like: error: 'main' is not marked 'pub'. So main must be declared as pub fn main() void { ... } (or pub fn main() !void { ... }).What’s the difference between const and var? Do I need type annotations? How strict is Zig’s typing?const values can’t be reassigned; var values can. Zig is statically and strongly typed. The compiler infers types when unambiguous (e.g., const b = true; is bool). When a literal could fit many types (like 42), you may need an annotation (e.g., const x: i32 = 42;). Zig never performs ambiguous coercions; you must cast explicitly when needed.What basic types should I know first?Common types include bool; signed/unsigned integers like i32, i64, u8, u32; pointer-sized integers usize and isize; floating-point types f16, f32, f64, f80, f128; void (a single “nothing” value); and type (types are values at comptime, and the type of type is type).How do I define functions and returns? What errors will I see if types don’t match?Functions are declared with fn name(params) ReturnType { ... }. Return statements must match the function’s return type; the compiler can infer types from returned values. If you omit a needed return or change the return type incompatibly (e.g., to bool while returning integers), the compiler emits a clear error and refuses to compile.How do operators and overflow behave in Zig?Arithmetic operators +, -, * check for overflow in safe mode and panic at runtime on overflow. Wrapping variants +% and -% perform two’s-complement wraparound. / divides floating points; use @divFloor or @divExact for integer division semantics. Comparison operators are the usual ==, !=, <, <=, >, >=.How do pointers work? What are & and .*? What about pointer mutability?& takes the address of a value; .* dereferences a pointer. Single-item pointers are written *T (or *const T for pointers to constants). You can write through a pointer only if it points to mutable memory; attempting to write through a *const T is a compile error. A mutable pointer can coerce to a pointer-to-const (narrowing). Zig also has many-item pointers [*]T for “pointer to many elements” when length is tracked separately.What’s the difference between arrays, slices, and ranges?Arrays are values with fixed length known at comptime: [N]T. Copying an array copies all elements. Slices are pointers with a runtime length: []T or []const T; they carry ptr and len fields and can be indexed. You can get a slice from an array via &arr or by taking a range: arr[a..b]. Range indices include the left bound and exclude the right bound.How are strings represented? What does *const [N:0]u8 mean?Zig treats strings as UTF-8 byte sequences. A string literal has type *const [N:0]u8: a pointer to a constant N-byte array of u8 with a trailing nul (the :0). It can coerce to []const u8 for generic string processing, and the nul terminator makes string literals directly usable with C APIs without extra copying.How do I create buffers, and what is undefined for?Buffers are typically mutable byte slices/arrays. Declare an array and leave it uninitialized with undefined, e.g., var buf: [1024]u8 = undefined; then pass &buf where a buffer is needed. Use undefined carefully: reading uninitialized data is illegal and can cause serious bugs; it’s mainly appropriate for buffers that you fully initialize before reading.How do structs work, and can they act like namespaces or have “methods”?Structs group named fields and must be fully initialized. They also serve as namespaces for functions and variables. If a function’s first parameter is the struct type, you can call it with dot “method” syntax (rect.size()). Use @This() to refer to the containing struct type (often aliased to Self). To mutate through a method, take a pointer receiver (self: *Struct) and call via rect.method() or Struct.method(&rect). Fields can have defaults, which is great for “config struct” patterns with optional settings.
pro $24.99 per month
access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!