5.5 KiB
Zig Style Guide
Coding conventions based on the Zig language reference. zig fmt enforces syntactic layout; naming, documentation, and API-design conventions still require human judgment. Interoperability and established external APIs can justify exceptions.
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:
ValueDataContextManagerutils,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 unchecked preconditions whose violation may cause illegal behavior
- Use "assert" when the implementation actively checks an invariant and panics when it is violated
/// Reads a little-endian u32 from the buffer.
///
/// Caller must provide at least 4 bytes. The slice expression performs a
/// bounds check in safety-enabled builds and panics if the buffer is shorter.
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. Assertion failure panics; this function
/// has no error return.
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 fmtnormalizes source layout; encoding, naming, and documentation rules are separate checks
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/