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:
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 lowercaseType Naming
Use PascalCase for types:
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_CASEConstant Naming
Use snake_case for constants. Uppercase is only for true compile-time constants:
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"; // camelCaseEnum Value Naming
Use PascalCase for enum values:
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:
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:
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:
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:
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:
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:
//! 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:
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:
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
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
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
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
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
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
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
//! 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
# 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 fmtEditor Configuration
For VS Code, create .vscode/settings.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.