Zig was created as a modern replacement for C, focusing on safety, readability, and performance.
Developed by Andrew Kelley in 2015, Zig aims to be a “robust, optimal, and clear” programming language.
It has evolved through a community-driven open-source model under the Zig Software Foundation (ZSF).
Key design philosophies:
No hidden control flow (no operator overloading, no exceptions, no properties).
No hidden memory allocation (allocators are passed explicitly to functions).
No preprocessor macros; compile-time code execution is done via comptime in the language itself.
Native interoperability with C code and build tool integration.
Who:
Created by Andrew Kelley and maintained by the Zig Software Foundation along with global open-source contributors.
Why:
To address the flaws of C (undefined behavior, unsafe defaults, complex macros) without adding the hidden overhead, implicit runtime allocations, or learning curves of C++ or Rust.
Introduction
Advantages
No Hidden Control Flow — What you see is what executes. No properties, no operator overloading, no exceptions, no RAII destructor magic.
Explicit Memory Allocation — Functions that require memory must accept an allocator argument. You can easily track, limit, and swap allocators (e.g., GeneralPurposeAllocator vs ArenaAllocator).
Comptime Metaprogramming — Execute any valid Zig code at compile-time. Replaces macros, templates, and generics with standard Zig code.
Seamless C Interoperability — Directly @cImport headers, link C libraries, and translate C code to Zig. Zig is also a drop-in C/C++ compiler (zig cc).
Comprehensive Build Tool — The build system is written in Zig itself, replacing make, cmake, and shell scripts.
Disadvantages
Young Language — Still in pre-1.0 development, meaning syntax and standard library APIs are subject to breaking changes between versions.
Manual Memory Management — While much safer than C (with bounds checking and slice concepts), the developer must manually pair allocations with defer allocator.free().
Smaller Library Ecosystem — Fewer third-party libraries compared to mature ecosystems like C++, Go, or Rust.
Remember Points
Unused Variables — Zig compilers treat unused variables and imports as compile-time errors.
Errors are Values — Error handling does not unwind the stack. Errors are returned as normal values in error unions.
Undefined State — You can explicitly initialize variables to undefined if they will be written later, but this must be done intentionally.
Basics
Variables and Constants
const std = @import("std");pub fn main() void { // Constants use 'const' and cannot be modified after initialization const max_size: u32 = 1024; // Variables use 'var' and must specify or infer their type var current_count: i32 = 0; current_count += 1; // Type inference is supported when allocating constants/variables const ratio = 3.14159; // inferred as f64 // Variables can be initialized as undefined (uninitialized memory) var uninit_buffer: [100]u8 = undefined; uninit_buffer[0] = 42; std.debug.print("Size: {}, Count: {}, Ratio: {}\n", .{ max_size, current_count, ratio });}
Basic Primitive Types
Integers: Signed (i8, i16, i32, i64, i128, isize) and Unsigned (u8, u16, u32, u64, u128, usize).
Floats: f16, f32, f64, f80, f128.
Booleans: bool (true or false).
Arbitrary Bit-Width Integers: u3 (3-bit unsigned), i29 (29-bit signed integer). Perfect for hardware register mapping.
const std = @import("std");pub fn main() void { // 1. While loop var i: usize = 0; while (i < 5) : (i += 1) { // : (i += 1) is the update expression run at loop end std.debug.print("While: {}\n", .{i}); } // 2. For loop over arrays/slices (captures element pointer or value) const array = [_]i32{ 10, 20, 30, 40 }; for (array, 0..) |value, index| { std.debug.print("Index {}: Value {}\n", .{ index, value }); } // 3. Multi-sequence for loop (iterates over multiple arrays of the same length) const chars = [_]u8{ 'a', 'b', 'c', 'd' }; for (array, chars) |num, char| { std.debug.print("{} -> {c}\n", .{ num, char }); }}
Defer and Errdefer
Resource Cleanup Mechanics
const std = @import("std");fn processFile() !void { std.debug.print("Opening resource...\n", .{}); // 'defer' schedules an expression to run when exiting the current block scope defer std.debug.print("Closing resource (always runs)...\n", .{}); var has_error = true; if (has_error) { // 'errdefer' schedules cleanup to run ONLY if the block exits returning an error errdefer std.debug.print("Rollback changes (runs on error only)!\n", .{}); return error.FailedExecution; }}pub fn main() void { processFile() catch |err| { std.debug.print("Caught: {}\n", .{err}); };}
Errors as Values
Error Sets and Unions
const std = @import("std");// Define an error setconst FileError = error{ FileNotFound, PermissionDenied, OutOfMemory,};// A function returning an Error Union (FileError or u32)fn checkFile(path_exists: bool) FileError!u32 { if (!path_exists) { return FileError.FileNotFound; } return 42;}pub fn main() void { // 1. try keyword: Unwraps value or bubbles up the error to caller const val = checkFile(true) catch 0; // 2. catch keyword: Handles the error or provides a fallback value const val2 = checkFile(false) catch |err| { std.debug.print("Error encountered: {}\n", .{err}); return; }; // 3. if-let error checking if (checkFile(true)) |unwrapped| { std.debug.print("File ID: {}\n", .{unwrapped}); } else |err| { std.debug.print("Failed: {}\n", .{err}); }}
Zig has no global garbage collector or implicit heap allocation. All heap memory goes through an explicit allocator.
const std = @import("std");pub fn main() !void { // 1. GeneralPurposeAllocator (GPA): Detects leaks, double-frees, out-of-bounds access var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); // Check for memory leaks at program exit const allocator = gpa.allocator(); // Allocate dynamic array const buffer = try allocator.alloc(u32, 10); defer allocator.free(buffer); // Clean up allocated memory for (buffer, 0..) |*item, idx| { item.* = @intCast(idx * 10); } // 2. ArenaAllocator: Allocates rapidly, frees everything at once var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); // Frees ALL allocations made through this arena const arena_allocator = arena.allocator(); const node1 = try arena_allocator.create(i32); const node2 = try arena_allocator.create(i32); node1.* = 1; node2.* = 2;}
Comptime (Compile-time Meta)
Type Generation and Execution
const std = @import("std");// 1. Comptime evaluation of functionsfn factorial(n: u32) u32 { return if (n == 0) 1 else n * factorial(n - 1);}// 2. Generics: A function returning a type at compile-timefn GenericStack(comptime T: type) type { return struct { items: [10]T, count: usize = 0, const Self = @This(); pub fn push(self: *Self, item: T) void { self.items[self.count] = item; self.count += 1; } };}pub fn main() void { // Evaluated entirely at compile-time! const fact_10 = comptime factorial(10); // Instantiate a Generic struct var int_stack = GenericStack(i32){ .items = undefined }; int_stack.push(100); std.debug.print("Fact: {}, Stack Top: {}\n", .{ fact_10, int_stack.items[0] });}
C Interoperability
Importing C Directly
// Import standard C headers directlyconst c = @cImport({ @cInclude("stdio.h"); @cInclude("stdlib.h");});pub fn main() void { const text = "Hello from C stdio!\n"; _ = c.printf(text); // Allocate memory using C malloc const ptr = c.malloc(10); defer c.free(ptr);}
Use zig translate-c code.c to automatically generate the equivalent Zig interface files.
Build Tools
Standard Build System
Instead of Makefile or CMake, Zig uses a build.zig script written in Zig itself.