Zig Functions and Structs: Custom Types

Zig Functions and Structs: Custom Types
Zig does not follow the Object-Oriented Programming (OOP) paradigm of "Classes," "Inheritance," or "Private/Public" access modifiers for fields. There are no "Constructors" that run hidden logic when you create an object. Instead, Zig embraces a "Data-Oriented" philosophy where Structs define memory layout and Functions define logic.
This 1,500+ word guide explores the "Explicit Structure" of Zig. We will master the pub (visibility) keyword, learn how to attach Methods via pointer captures, and explore the "Init/Deinit" pattern that allows Zig to manage resources without the overhead of a Garbage Collector or the complexity of C++ Destructors.
1. Structs: The Physical Reality of Memory
A struct in Zig is a named collection of fields. However, Zig offers three different ways to store that data depending on how much control you need over the actual bytes on the CPU.
A. The Standard struct (Zig-Optimized)
In a standard struct, the compiler is allowed to reorder your fields to minimize Padding.
const User = struct {
id: u32,
is_active: bool,
full_name: []const u8,
};If you put a bool (1 byte) between two u32 (4 bytes), a normal compiler might waste 3 bytes of "Padding" to keep the alignment. Zig will move the bool to the end of the struct automatically to keep your memory footprint as small as possible.
B. The extern struct (C-Compatibility)
If you are passing data to a C library, you use extern struct. This guarantees that the field order remains EXACTLY as you wrote it, matching the C application binary interface (ABI).
C. The packed struct (Hardware Mapping)
This is Zig's "Superpower." A packed struct has Zero Padding. You specify exactly how many bits each field takes.
const GDTEntry = packed struct {
limit_low: u16,
base_low: u24,
// ...
};This is essential for writing Operating System kernels or working with hardware registers where every single bit is a switch that controls a physical device.
2. The Physics of Padding: Struct Layout and the Cache
In high-performance systems, the order of your fields is not just an aesthetic choice; it is a Cache Optimization.
The Cache Line Mirror
- The Concept: CPUs read memory in Cache Lines (usually 64 bytes).
- The Physics: If a struct field crosses a cache line boundary, the CPU must perform two memory reads instead of one, doubling the latency.
- Zig's Logic: For standard
structtypes, Zig uses Field Reordering. It places smaller fields into the gaps created by larger fields (padding). - The Hardware Result: This keeps your struct compact, ensuring that more instances of your type fit into the L1 Data Cache, resulting in massive throughput gains for data-intensive loops.
3. Functions: Precision Execution
Functions in Zig are straightforward, but they have strict rules regarding parameters.
Parameters are Constants
In Zig, function parameters are always immutable (const). You cannot change the value of a parameter passed to a function.
fn calculate(x: i32) void {
x = 10; // COMPILE ERROR!
}If you need to change a value, you must pass a Pointer (*i32). This forces high-quality code: you always know if a function is going to modify your data just by looking at its signature.
Returning Errors
Almost every professional Zig function returns an "Error Union."
pub fn connect(ip: []const u8) !Connection {
// ...
}The ! means "Maybe a Connection, Maybe an Error." This ensures that the caller must handle the failure case using try, catch, or if.
4. Methods: Pointer vs. Value Captures
Zig doesn't have "Methods" in the Java sense, but it provides Dot Notation Syntax. If the first argument of a function is a struct type, you can call it from an instance of that struct.
The self vs *self Decision
const Vec3 = struct {
x: f32, y: f32, z: f32,
// Value Capture (Read-Only)
pub fn magnitude(self: Vec3) f32 {
return @sqrt(self.x*self.x + self.y*self.y + self.z*self.z);
}
// Pointer Capture (Mutable)
pub fn reset(self: *Vec3) void {
self.x = 0; self.y = 0; self.z = 0;
}
};- Use
self: Vec3for simple calculations. It is thread-safe because it works on a copy. - Use
self: *Vec3for any function that updates the state.
5. The "Init / Deinit" Lifecycle
Because Zig has no "Constructors" or "Garbage Collection," resource management is explicit and beautiful. The community standard is the "Init/Deinit" pattern.
const FileLogger = struct {
allocator: std.mem.Allocator,
buffer: []u8,
pub fn init(allocator: std.mem.Allocator, size: usize) !FileLogger {
return FileLogger{
.allocator = allocator,
.buffer = try allocator.alloc(u8, size),
};
}
pub fn deinit(self: *FileLogger) void {
self.allocator.free(self.buffer);
}
};The "Shotgun" Release
The power of this pattern comes when you use defer.
var logger = try FileLogger.init(allocator, 1024);
defer logger.deinit(); // This will run even if the function errors!This ensures that your memory is always cleaned up, but the timing is 100% predictable.
6. Dot-Notation Internals: Zero-Cost Methods
Zig provides "Dot-Notation" (e.g., user.init()), but it is critical to understand that this is Syntactic Sugar, not a runtime lookup.
The Dispatch Mirror
- The Process: In Java or Python, calling a method often involves a "Virtual Table" (vtable) lookup at runtime.
- The Physics: In Zig,
user.activate()is converted by the compiler intoUser.activate(&user)at compile-time. - The Result: There is Zero Runtime Overhead. The "Method" is just a standard function, and the "Instance" is just the first parameter. By mastering this, you gain the readability of OOP with the raw speed of C.
7. Anonymous Structs and Tuples
Zig allows you to create structures without naming them first. This is primarily used for returning multiple values or passing arguments to formatting functions.
- Tuples:
const pair = .{ 10, "Hello" };Access viapair[0]. - Anonymous Struct:
const p = .{ .x = 10, .y = 5 };Access viap.x.
These are "Comptime" constructs that allow Zig to have powerful type-reflection without the runtime cost of "Interfaces" or "Reflection APIs" found in Java.
Structs are the "Blueprints" of your system. By mastering the distinction between packed memory and the explicit lifecycle of pointers, you gain the ability to build data structures that are both incredibly efficient and perfectly readable. You graduate from "Managing variables" to "Architecting Types."
Phase 7: Type Architecture Checklist
- Audit your data structures: Convert "Manager" classes into simple Structs + Functions.
- Implement the Init/Deinit pattern for every struct that handles external resources (RAM, Files, Sockets).
- Optimize your memory footprint: Use
@sizeOfto verify if Zig's Field Reordering is saving you space. - Use
extern structfor any data that must be shared with a C library (ABI-stability). - Leverage Anonymous Structs for clean, ad-hoc grouping of return values in internal logic.
Read next: Error Handling: The Logic of Failure →
Frequently Asked Questions
Q: How do you add methods to a struct in Zig?
Define functions inside the struct body and give the first parameter the name self (or any name) typed as the struct or a pointer to it. Call them with dot syntax: myPoint.distanceTo(other). There is no special method keyword — a function inside a struct namespace that takes the struct type as its first argument is simply a method by convention.
Q: What is the difference between passing a struct by value versus by pointer in Zig?
Passing by value copies the entire struct onto the stack — safe but potentially expensive for large types. Passing *MyStruct (a pointer) avoids the copy and allows mutation. Use *const MyStruct for read-only access without copying. Zig does not automatically choose pass-by-reference for large structs the way some languages do, so the choice is explicit.
Q: Can Zig structs have default field values and constructors?
Fields can have default values declared inline: name: []const u8 = "unknown". Zig has no constructors, but the convention is to define an init function that takes necessary parameters and returns an initialised struct value (or an error union if initialisation can fail). This explicit pattern makes object construction easy to trace and test.
Part of the Zig Mastery Course — engineering the types.
