Skip to content

Zig Style Guide

Good code style is the foundation of team collaboration and code maintenance. This chapter introduces the official Zig code style and best practices.

Naming Conventions

Variable and Function Naming

Use snake_case for variables and functions:

zig
const std = @import("std");

// ✅ Good naming
const max_buffer_size = 4096;
var current_user_count: u32 = 0;
const default_timeout_ms = 5000;

fn calculateDistance(point1: Point, point2: Point) f64 {
    const dx = point1.x - point2.x;
    const dy = point1.y - point2.y;
    return @sqrt(dx * dx + dy * dy);
}

fn processUserInput(input_buffer: []const u8) ![]u8 {
    // Process user input
    return input_buffer;
}

// ❌ Avoid these names
// const maxBufferSize = 4096;  // camelCase
// const MaxBufferSize = 4096;  // PascalCase
// const MAX_BUFFER_SIZE = 4096; // Constants should use lowercase

Type Naming

Use PascalCase for types:

zig
const std = @import("std");

// ✅ Good type naming
const Point = struct {
    x: f64,
    y: f64,
};

const Color = enum {
    Red,
    Green,
    Blue,
    Yellow,
};

const HttpRequest = struct {
    method: HttpMethod,
    url: []const u8,
    headers: std.StringHashMap([]const u8),
};

const DatabaseConnection = struct {
    host: []const u8,
    port: u16,
    username: []const u8,

    pub fn connect(self: *DatabaseConnection) !void {
        // Connection logic
    }
};

// ❌ Avoid these names
// const httpRequest = struct { ... };  // camelCase
// const HTTP_REQUEST = struct { ... }; // SCREAMING_SNAKE_CASE

Constant Naming

Use snake_case for constants. Uppercase is only for true compile-time constants:

zig
const std = @import("std");

// ✅ Good constant naming
const default_port = 8080;
const max_connections = 1000;
const api_version = "v1.0";

// Compile-time known mathematical constants can use uppercase
const PI = 3.14159265358979323846;
const E = 2.71828182845904523536;

// Configuration constants
const config = struct {
    const server_timeout = 30; // seconds
    const retry_attempts = 3;
    const buffer_size = 4096;
};

// ❌ Avoid these names
// const DEFAULT_PORT = 8080;     // Unnecessary uppercase
// const MaxConnections = 1000;   // PascalCase
// const apiVersion = "v1.0";     // camelCase

Enum Value Naming

Use PascalCase for enum values:

zig
const std = @import("std");

// ✅ Good enum naming
const HttpMethod = enum {
    Get,
    Post,
    Put,
    Delete,
    Patch,
    Head,
    Options,
};

const LogLevel = enum {
    Debug,
    Info,
    Warning,
    Error,
    Critical,
};

const ConnectionState = enum {
    Disconnected,
    Connecting,
    Connected,
    Reconnecting,
    Failed,
};

// ❌ Avoid these names
// const HttpMethod = enum {
//     GET,    // All uppercase
//     get,    // All lowercase
//     Post,   // Inconsistent
// };

Code Formatting

Indentation and Spacing

Use 4 spaces for indentation, not tabs:

zig
const std = @import("std");

// ✅ Correct indentation
pub fn main() void {
    const numbers = [_]i32{ 1, 2, 3, 4, 5 };

    for (numbers) |number| {
        if (number % 2 == 0) {
            std.debug.print("Even number: {}\n", .{number});
        } else {
            std.debug.print("Odd number: {}\n", .{number});
        }
    }

    const result = switch (numbers.len) {
        0 => "Empty array",
        1 => "Single element",
        2...5 => "Few elements",
        else => "Many elements",
    };

    std.debug.print("Array description: {s}\n", .{result});
}

Line Length

Recommended maximum of 100 characters per line, can be extended when necessary:

zig
const std = @import("std");

// ✅ Reasonable line length
pub fn processLongParameterList(
    first_parameter: []const u8,
    second_parameter: i32,
    third_parameter: bool,
    fourth_parameter: ?*SomeStruct,
) !ProcessResult {
    // Function body
}

// ✅ Long string handling
const long_message =
    "This is a very long message that needs to be split into multiple lines to maintain code readability." ++
    "We can use string concatenation to handle this case.";

// ✅ Long expression line breaks
const complex_calculation = (first_value * second_value) +
    (third_value / fourth_value) -
    (fifth_value % sixth_value);

Blank Lines

Use blank lines appropriately to separate logical blocks:

zig
const std = @import("std");

// Blank line after imports
const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator;

// Constant definitions
const DEFAULT_CAPACITY = 16;
const MAX_RETRIES = 3;

// Blank lines before and after type definitions
const User = struct {
    id: u32,
    name: []const u8,
    email: []const u8,

    // Blank line between methods
    pub fn init(id: u32, name: []const u8, email: []const u8) User {
        return User{
            .id = id,
            .name = name,
            .email = email,
        };
    }

    pub fn isValid(self: User) bool {
        return self.name.len > 0 and self.email.len > 0;
    }
};

// Blank lines between functions
pub fn main() void {
    const user = User.init(1, "Zhang San", "zhangsan@example.com");

    if (user.isValid()) {
        std.debug.print("Valid user: {s}\n", .{user.name});
    }
}

Comment Style

Documentation Comments

Use /// for documentation comments:

zig
const std = @import("std");

/// Represents a point in 2D space
const Point = struct {
    /// X coordinate
    x: f64,
    /// Y coordinate
    y: f64,

    /// Creates a new point
    ///
    /// Parameters:
    /// - x: X coordinate value
    /// - y: Y coordinate value
    ///
    /// Returns: A new Point instance
    pub fn init(x: f64, y: f64) Point {
        return Point{ .x = x, .y = y };
    }

    /// Calculates distance to another point
    ///
    /// Parameters:
    /// - other: Target point
    ///
    /// Returns: Euclidean distance between two points
    pub fn distanceTo(self: Point, other: Point) f64 {
        const dx = self.x - other.x;
        const dy = self.y - other.y;
        return @sqrt(dx * dx + dy * dy);
    }

    /// Checks if the point is at the origin
    ///
    /// Returns: true if the point is at the origin
    pub fn isOrigin(self: Point) bool {
        return self.x == 0.0 and self.y == 0.0;
    }
};

Regular Comments

Use // for regular comments:

zig
const std = @import("std");

pub fn processData(data: []const u8) ![]u8 {
    // Validate input data
    if (data.len == 0) {
        return error.EmptyInput;
    }

    // Allocate output buffer
    var allocator = std.heap.page_allocator;
    var result = try allocator.alloc(u8, data.len * 2);

    // Process each byte
    for (data, 0..) |byte, i| {
        // Convert each byte to hexadecimal representation
        const hex_chars = "0123456789ABCDEF";
        result[i * 2] = hex_chars[byte >> 4];
        result[i * 2 + 1] = hex_chars[byte & 0x0F];
    }

    return result;
}

Top-Level Documentation Comments

Use //! for file-level documentation comments:

zig
//! Math utilities library
//!
//! This module provides various mathematical calculation features, including:
//! - Basic geometric calculations
//! - Statistical functions
//! - Numerical analysis tools
//!
//! Example usage:
//! ```zig
//! const math_utils = @import("math_utils.zig");
//! const point = math_utils.Point.init(3.0, 4.0);
//! const distance = point.distanceTo(math_utils.Point.init(0.0, 0.0));
//! ```

const std = @import("std");

// Module content...

Error Handling Style

Error Propagation

Prefer using try for error propagation:

zig
const std = @import("std");

const FileError = error{
    NotFound,
    PermissionDenied,
    ReadError,
};

// ✅ Good error handling
pub fn readConfigFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();

    const file_size = try file.getEndPos();
    const contents = try allocator.alloc(u8, file_size);
    _ = try file.readAll(contents);

    return contents;
}

// ✅ Use catch to provide default values
pub fn getConfigValue(config: []const u8, key: []const u8) []const u8 {
    return parseConfigValue(config, key) catch "default_value";
}

// ✅ Use if-else for error handling
pub fn processFile(path: []const u8) void {
    if (readConfigFile(std.heap.page_allocator, path)) |contents| {
        defer std.heap.page_allocator.free(contents);
        std.debug.print("File contents: {s}\n", .{contents});
    } else |err| {
        std.debug.print("Failed to read file: {}\n", .{err});
    }
}

fn parseConfigValue(config: []const u8, key: []const u8) ![]const u8 {
    // Parsing logic
    _ = config;
    _ = key;
    return "parsed_value";
}

Error Set Definitions

Organize related errors into meaningful error sets:

zig
const std = @import("std");

// ✅ Organize errors by functionality
const NetworkError = error{
    ConnectionFailed,
    Timeout,
    InvalidAddress,
    ProtocolError,
};

const DatabaseError = error{
    ConnectionFailed,
    QueryFailed,
    TransactionFailed,
    ConstraintViolation,
};

const ValidationError = error{
    InvalidInput,
    MissingField,
    FormatError,
    RangeError,
};

// ✅ Combine error sets
const ApplicationError = NetworkError || DatabaseError || ValidationError;

pub fn processRequest(request: []const u8) ApplicationError![]const u8 {
    // Validate request
    try validateRequest(request);

    // Query database
    const data = try queryDatabase(request);

    // Send network request
    const response = try sendNetworkRequest(data);

    return response;
}

fn validateRequest(request: []const u8) ValidationError!void {
    if (request.len == 0) {
        return ValidationError.InvalidInput;
    }
}

fn queryDatabase(request: []const u8) DatabaseError![]const u8 {
    _ = request;
    return "database_result";
}

fn sendNetworkRequest(data: []const u8) NetworkError![]const u8 {
    _ = data;
    return "network_response";
}

Struct and Enum Style

Struct Definitions

zig
const std = @import("std");

// ✅ Good struct style
const HttpServer = struct {
    // Fields sorted by importance and size
    allocator: std.mem.Allocator,
    address: std.net.Address,
    max_connections: u32,
    timeout_ms: u32,
    is_running: bool,

    const Self = @This();

    // Constructor
    pub fn init(allocator: std.mem.Allocator, address: std.net.Address) Self {
        return Self{
            .allocator = allocator,
            .address = address,
            .max_connections = 100,
            .timeout_ms = 5000,
            .is_running = false,
        };
    }

    // Public methods
    pub fn start(self: *Self) !void {
        if (self.is_running) {
            return error.AlreadyRunning;
        }

        // Start logic
        self.is_running = true;
        std.debug.print("Server started\n", .{});
    }

    pub fn stop(self: *Self) void {
        if (!self.is_running) {
            return;
        }

        // Stop logic
        self.is_running = false;
        std.debug.print("Server stopped\n", .{});
    }

    // Private methods
    fn handleConnection(self: *Self, connection: std.net.Stream) void {
        _ = self;
        _ = connection;
        // Handle connection
    }
};

Enum Definitions

zig
const std = @import("std");

// ✅ Good enum style
const TaskStatus = enum {
    Pending,
    Running,
    Completed,
    Failed,
    Cancelled,

    // Enum methods
    pub fn isFinished(self: TaskStatus) bool {
        return switch (self) {
            .Completed, .Failed, .Cancelled => true,
            .Pending, .Running => false,
        };
    }

    pub fn toString(self: TaskStatus) []const u8 {
        return switch (self) {
            .Pending => "Pending",
            .Running => "Running",
            .Completed => "Completed",
            .Failed => "Failed",
            .Cancelled => "Cancelled",
        };
    }
};

// ✅ Enum with values
const HttpStatusCode = enum(u16) {
    Ok = 200,
    Created = 201,
    NoContent = 204,
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404,
    InternalServerError = 500,

    pub fn isSuccess(self: HttpStatusCode) bool {
        const code = @intFromEnum(self);
        return code >= 200 and code < 300;
    }

    pub fn isClientError(self: HttpStatusCode) bool {
        const code = @intFromEnum(self);
        return code >= 400 and code < 500;
    }

    pub fn isServerError(self: HttpStatusCode) bool {
        const code = @intFromEnum(self);
        return code >= 500 and code < 600;
    }
};

Function Style

Function Signatures

zig
const std = @import("std");

// ✅ Good function style
pub fn calculateStatistics(
    allocator: std.mem.Allocator,
    data: []const f64,
    options: StatisticsOptions,
) !Statistics {
    if (data.len == 0) {
        return error.EmptyDataSet;
    }

    var sum: f64 = 0;
    var min_val = data[0];
    var max_val = data[0];

    // Calculate basic statistics
    for (data) |value| {
        sum += value;
        if (value < min_val) min_val = value;
        if (value > max_val) max_val = value;
    }

    const mean = sum / @as(f64, @floatFromInt(data.len));

    // Calculate standard deviation (if needed)
    var variance: f64 = 0;
    if (options.calculate_std_dev) {
        for (data) |value| {
            const diff = value - mean;
            variance += diff * diff;
        }
        variance /= @as(f64, @floatFromInt(data.len));
    }

    return Statistics{
        .count = data.len,
        .sum = sum,
        .mean = mean,
        .min = min_val,
        .max = max_val,
        .std_dev = if (options.calculate_std_dev) @sqrt(variance) else null,
    };
}

const StatisticsOptions = struct {
    calculate_std_dev: bool = false,
};

const Statistics = struct {
    count: usize,
    sum: f64,
    mean: f64,
    min: f64,
    max: f64,
    std_dev: ?f64,
};

Function Organization

zig
const std = @import("std");

// ✅ Organize functions by functionality
const StringUtils = struct {
    // Public API first
    pub fn trim(input: []const u8) []const u8 {
        return trimLeft(trimRight(input));
    }

    pub fn split(allocator: std.mem.Allocator, input: []const u8, delimiter: u8) ![][]const u8 {
        var parts = std.ArrayList([]const u8).init(allocator);
        defer parts.deinit();

        var start: usize = 0;
        for (input, 0..) |char, i| {
            if (char == delimiter) {
                if (i > start) {
                    try parts.append(input[start..i]);
                }
                start = i + 1;
            }
        }

        if (start < input.len) {
            try parts.append(input[start..]);
        }

        return parts.toOwnedSlice();
    }

    pub fn join(allocator: std.mem.Allocator, parts: []const []const u8, separator: []const u8) ![]u8 {
        if (parts.len == 0) {
            return try allocator.dupe(u8, "");
        }

        var total_len: usize = 0;
        for (parts) |part| {
            total_len += part.len;
        }
        total_len += separator.len * (parts.len - 1);

        var result = try allocator.alloc(u8, total_len);
        var pos: usize = 0;

        for (parts, 0..) |part, i| {
            @memcpy(result[pos..pos + part.len], part);
            pos += part.len;

            if (i < parts.len - 1) {
                @memcpy(result[pos..pos + separator.len], separator);
                pos += separator.len;
            }
        }

        return result;
    }

    // Private helper functions last
    fn trimLeft(input: []const u8) []const u8 {
        var start: usize = 0;
        while (start < input.len and isWhitespace(input[start])) {
            start += 1;
        }
        return input[start..];
    }

    fn trimRight(input: []const u8) []const u8 {
        var end = input.len;
        while (end > 0 and isWhitespace(input[end - 1])) {
            end -= 1;
        }
        return input[0..end];
    }

    fn isWhitespace(char: u8) bool {
        return char == ' ' or char == '\t' or char == '\n' or char == '\r';
    }
};

Testing Style

Test Organization

zig
const std = @import("std");
const testing = std.testing;

// Code to be tested
fn add(a: i32, b: i32) i32 {
    return a + b;
}

fn divide(a: f64, b: f64) !f64 {
    if (b == 0) return error.DivisionByZero;
    return a / b;
}

// ✅ Good test style
test "add function" {
    // Basic cases
    try testing.expect(add(2, 3) == 5);
    try testing.expect(add(-1, 1) == 0);
    try testing.expect(add(0, 0) == 0);

    // Edge cases
    try testing.expect(add(std.math.maxInt(i32), 0) == std.math.maxInt(i32));
    try testing.expect(add(std.math.minInt(i32), 0) == std.math.minInt(i32));
}

test "divide function - success cases" {
    // Normal cases
    try testing.expectApproxEqRel(try divide(10.0, 2.0), 5.0, 0.001);
    try testing.expectApproxEqRel(try divide(7.0, 3.0), 2.333333, 0.001);

    // Negative numbers
    try testing.expectApproxEqRel(try divide(-10.0, 2.0), -5.0, 0.001);
    try testing.expectApproxEqRel(try divide(10.0, -2.0), -5.0, 0.001);
}

test "divide function - error cases" {
    // Division by zero
    try testing.expectError(error.DivisionByZero, divide(10.0, 0.0));
    try testing.expectError(error.DivisionByZero, divide(-10.0, 0.0));
}

// ✅ Complex data structure tests
test "string utilities" {
    const allocator = testing.allocator;

    // Test splitting
    const parts = try StringUtils.split(allocator, "a,b,c", ',');
    defer allocator.free(parts);

    try testing.expect(parts.len == 3);
    try testing.expectEqualStrings("a", parts[0]);
    try testing.expectEqualStrings("b", parts[1]);
    try testing.expectEqualStrings("c", parts[2]);

    // Test joining
    const joined = try StringUtils.join(allocator, parts, " | ");
    defer allocator.free(joined);

    try testing.expectEqualStrings("a | b | c", joined);
}

// Define StringUtils above for testing
const StringUtils = struct {
    pub fn split(allocator: std.mem.Allocator, input: []const u8, delimiter: u8) ![][]const u8 {
        var parts = std.ArrayList([]const u8).init(allocator);
        defer parts.deinit();

        var start: usize = 0;
        for (input, 0..) |char, i| {
            if (char == delimiter) {
                if (i > start) {
                    try parts.append(input[start..i]);
                }
                start = i + 1;
            }
        }

        if (start < input.len) {
            try parts.append(input[start..]);
        }

        return parts.toOwnedSlice();
    }

    pub fn join(allocator: std.mem.Allocator, parts: []const []const u8, separator: []const u8) ![]u8 {
        if (parts.len == 0) {
            return try allocator.dupe(u8, "");
        }

        var total_len: usize = 0;
        for (parts) |part| {
            total_len += part.len;
        }
        total_len += separator.len * (parts.len - 1);

        var result = try allocator.alloc(u8, total_len);
        var pos: usize = 0;

        for (parts, 0..) |part, i| {
            @memcpy(result[pos..pos + part.len], part);
            pos += part.len;

            if (i < parts.len - 1) {
                @memcpy(result[pos..pos + separator.len], separator);
                pos += separator.len;
            }
        }

        return result;
    }
};

Performance Considerations

Memory Allocation

zig
const std = @import("std");

// ✅ Good memory management
pub fn processLargeDataset(allocator: std.mem.Allocator, data: []const u8) ![]u8 {
    // Pre-estimate result size to reduce reallocation
    var result = try std.ArrayList(u8).initCapacity(allocator, data.len * 2);
    defer result.deinit();

    // Batch processing for efficiency
    const batch_size = 1024;
    var i: usize = 0;

    while (i < data.len) {
        const end = @min(i + batch_size, data.len);
        const batch = data[i..end];

        // Process batch
        try processBatch(&result, batch);

        i = end;
    }

    return result.toOwnedSlice();
}

fn processBatch(result: *std.ArrayList(u8), batch: []const u8) !void {
    // Batch processing logic
    for (batch) |byte| {
        try result.append(byte);
        if (byte != 0) {
            try result.append(byte);
        }
    }
}

// ✅ Use stack allocation to avoid heap allocation
pub fn formatSmallString(comptime fmt: []const u8, args: anytype) [256]u8 {
    var buffer: [256]u8 = undefined;
    const result = std.fmt.bufPrint(&buffer, fmt, args) catch "Formatting error";

    // Zero out remaining part
    @memset(buffer[result.len..], 0);

    return buffer;
}

Code Organization

Module Structure

zig
//! HTTP client library
//!
//! Provides simple and easy-to-use HTTP client functionality, supporting:
//! - GET, POST, PUT, DELETE requests
//! - Custom headers
//! - JSON serialization/deserialization
//! - Connection pool
//! - Timeout control

const std = @import("std");

// Public type exports
pub const HttpClient = Client;
pub const HttpRequest = Request;
pub const HttpResponse = Response;
pub const HttpError = Error;

// Public constants
pub const default_timeout_ms = 30000;
pub const max_redirects = 10;

// Error definitions
const Error = error{
    InvalidUrl,
    ConnectionFailed,
    Timeout,
    InvalidResponse,
    TooManyRedirects,
};

// Main type definitions
const Client = struct {
    allocator: std.mem.Allocator,
    timeout_ms: u32,
    max_redirects: u32,

    const Self = @This();

    pub fn init(allocator: std.mem.Allocator) Self {
        return Self{
            .allocator = allocator,
            .timeout_ms = default_timeout_ms,
            .max_redirects = max_redirects,
        };
    }

    pub fn get(self: *Self, url: []const u8) Error!Response {
        const request = Request{
            .method = .Get,
            .url = url,
            .headers = null,
            .body = null,
        };

        return self.send(request);
    }

    pub fn post(self: *Self, url: []const u8, body: []const u8) Error!Response {
        const request = Request{
            .method = .Post,
            .url = url,
            .headers = null,
            .body = body,
        };

        return self.send(request);
    }

    fn send(self: *Self, request: Request) Error!Response {
        // Send request implementation
        _ = self;
        _ = request;

        return Response{
            .status_code = 200,
            .headers = null,
            .body = "Response body",
        };
    }
};

const Request = struct {
    method: Method,
    url: []const u8,
    headers: ?std.StringHashMap([]const u8),
    body: ?[]const u8,
};

const Response = struct {
    status_code: u16,
    headers: ?std.StringHashMap([]const u8),
    body: []const u8,
};

const Method = enum {
    Get,
    Post,
    Put,
    Delete,
    Patch,
    Head,
    Options,
};

// Tests
test "HTTP client basic functionality" {
    const allocator = std.testing.allocator;

    var client = Client.init(allocator);
    const response = try client.get("https://example.com");

    std.testing.expect(response.status_code == 200) catch {};
}

Tools and Automation

Using zig fmt

bash
# Format a single file
zig fmt src/main.zig

# Format entire directory
zig fmt src/

# Check if formatting is correct (without modifying files)
zig fmt --check src/

# Add formatting step in build script
zig build fmt

Editor Configuration

For VS Code, create .vscode/settings.json:

json
{
    "editor.tabSize": 4,
    "editor.insertSpaces": true,
    "editor.rulers": [100],
    "files.trimTrailingWhitespace": true,
    "files.insertFinalNewline": true,
    "zig.path": "zig",
    "zig.zls.path": "zls",
    "zig.initialLogLevel": "info"
}

Summary

This chapter introduced various aspects of Zig code style:

  • ✅ Naming conventions: Rules for variables, functions, types, and constants
  • ✅ Code formatting: Indentation, line length, and blank line usage
  • ✅ Comment style: Documentation comments, regular comments, and top-level comments
  • ✅ Error handling style: Error propagation and error set definitions
  • ✅ Struct and enum style: Definition and organization methods
  • ✅ Function style: Signatures and organization principles
  • ✅ Testing style: Test organization and writing methods
  • ✅ Performance considerations: Memory management and optimization
  • ✅ Code organization: Module structure and tool usage

Following consistent code style not only improves code readability and maintainability, but also promotes team collaboration and code review efficiency. It is recommended to establish and follow these style guidelines in projects, and use automated tools to ensure consistency.

Content is for learning and research only.