zig-skills/references/style-guide.md

5.3 KiB

Zig Style Guide

Official coding conventions from the Zig language reference. These are implemented and enforced by zig fmt.

Naming Conventions

Summary Table

Element Convention Example
Types TitleCase XmlParser, HashMap
Namespace structs (0 fields) snake_case std.json, std.mem
Functions camelCase readU32Be, parseJson
Functions returning type TitleCase ArrayList, HashMap
Variables/constants snake_case const_name, global_var
File names (types) TitleCase.zig ArrayList.zig
File names (namespaces) snake_case.zig mem.zig, json.zig
Directories snake_case std/, hash_map/

Rules in Detail

Types use TitleCase:

const StructName = struct { field: i32 };
const TypeName = @import("dir_name/TypeName.zig");

Exception: Namespace structs (0 fields) use snake_case:

const namespace_name = @import("dir_name/file_name.zig");

Functions use camelCase:

fn functionName(param_name: TypeName) void { }
fn readU32Be() u32 { }  // Acronyms treated as words

Functions returning type use TitleCase:

fn ListTemplateFunction(comptime ChildType: type, comptime fixed_size: usize) type {
    return List(ChildType, fixed_size);
}

fn ShortList(comptime T: type, comptime n: usize) type {
    return struct {
        field_name: [n]T,
        fn methodName() void {}
    };
}

Variables and constants use snake_case:

var global_var: i32 = undefined;
const const_name = 42;
const primitive_type_alias = f32;
const string_alias = []u8;

Acronyms and Initialisms

Acronyms follow normal casing rules—they're treated as regular words:

// XML loses its all-caps when used in identifiers
const XmlParser = struct { field: i32 };
fn parseXml() void {}
const xml_document = "...";

// BE (Big Endian) treated as a word
fn readU32Be() u32 {}

// URL, HTTP, etc. follow the same rule
const HttpClient = struct {};
fn parseUrl() void {}
const api_url = "...";

Established Conventions

Follow established conventions when they exist (e.g., ENOENT from POSIX):

const ENOENT = error.FileNotFound;

Avoid Redundancy in Names

Words to Avoid in Type Names

Don't use these words—they apply to everything and communicate nothing:

  • Value
  • Data
  • Context
  • Manager
  • utils, misc, or somebody's initials
// BAD
const JsonValue = union(enum) { ... };
const DataManager = struct { ... };
const misc = @import("misc.zig");

// GOOD
const Value = union(enum) { ... };  // In json namespace: json.Value
const Store = struct { ... };
// Put utilities at module root, no namespace needed

Avoid Redundancy in Fully-Qualified Namespaces

Don't repeat the namespace in the type name:

// BAD - "json" appears twice in json.JsonValue
pub const json = struct {
    pub const JsonValue = union(enum) { number: f64, boolean: bool };
};

// GOOD - json.Value is clear and non-redundant
pub const json = struct {
    pub const Value = union(enum) { number: f64, boolean: bool };
};

The same applies to files (which are implicit structs):

// In json.zig:
// BAD
pub const JsonParser = struct { ... };

// GOOD
pub const Parser = struct { ... };  // Used as json.Parser

Whitespace

  • Indentation: 4 spaces (not tabs)
  • Braces: Opening brace on same line, unless wrapping is needed
  • Line length: Aim for ~100 characters; use common sense
  • Trailing commas: Use trailing commas for lists with more than 2 items
// Short list - can be on one line
const pair = .{ a, b };

// Longer list - one item per line with trailing comma
const Config = struct {
    name: []const u8,
    port: u16,
    timeout: u32,
    max_connections: usize,  // trailing comma
};

Line wrapping:

// When arguments don't fit, wrap and align
fn processRequest(
    allocator: Allocator,
    request: *const Request,
    options: ProcessOptions,
) !Response {
    // ...
}

Doc Comments

  • Omit redundant information that's already clear from the name
  • Duplicate information across similar functions (helps IDEs)
  • Use "assume" for invariants that cause unchecked illegal behavior when violated
  • Use "assert" for invariants that cause safety-checked illegal behavior when violated
/// Reads a little-endian u32 from the buffer.
///
/// Caller must **assume** buffer has at least 4 bytes remaining.
/// This is not checked and will cause undefined behavior if violated.
fn readU32Le(buf: []const u8) u32 {
    return std.mem.readInt(u32, buf[0..4], .little);
}

/// Pops the last element from the list.
///
/// **Asserts** the list is not empty. In safe modes, returns an error
/// or panics if the list is empty.
fn pop(self: *Self) T {
    std.debug.assert(self.items.len > 0);
    // ...
}

Source Encoding

  • UTF-8 encoding required
  • LF (\n, 0x0a) line endings (CRLF discouraged but tolerated)
  • End files with a newline
  • No hard tabs (spaces only)
  • zig fmt enforces all these conventions

Applying the Style Guide

Run zig fmt to automatically format code according to these conventions:

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

# Format entire project
zig fmt .

# Check without modifying (useful for CI)
zig fmt --check src/