The "Writergate" overhaul in Zig 0.15/0.16 completely re-architected how the standard library handles I/O. In Zig 0.16, the old std.io.Writer (which relied heavily on duck-typing with anytype) has been removed and replaced by the strict, explicit interface type under std.Io.Writer.
This change mirrors Zig's Allocator model. To perform any I/O, you now pass an explicit io: std.Io context down through your functions.
Instead of using duck-typing (anytype), std.Io.Writer is a concrete type that acts as an interface using a Virtual Method Table (vtable).
Crucially, buffering is now baked directly into the Writer interface rather than being a separate wrapper like the old std.io.BufferedWriter. A Writer owns an internal buffer, processing small writes locally in the hot path. When that buffer fills up, it flushes via its vtable.
The heart of the new interface is the drain function on the vtable:
fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize
Instead of a simple byte-by-byte or single-slice write, it accepts a slice of slices ([]const []const u8) to naturally support vectored / scatter-gather I/O at the operating system level.
When you initialize a writer from a concrete source (like a File), you don't get a std.Io.Writer directly. Instead, you get a concrete implementation type (e.g., std.Io.File.Writer), which contains an internal .interface field of type std.Io.Writer.
When a function expects a generic writer, you pass a pointer to that .interface field.
Here is how you write to stdout using a formatted print with the new buffered Writer interface:
const std = @import("std");
pub fn main(init: std.process.Init) !void {
// 1. All I/O requires an 'io' context from the environment
const io = init.io;
// 2. Allocate a userspace buffer for the writer
var buffer: [4096]u8 = undefined;
// 3. Initialize the concrete file writer bound to our io instance and buffer
var stdout_impl = std.Io.File.stdout().writer(io, &buffer);
// 4. Extract a pointer to the generic std.Io.Writer interface
const stdout = &stdout_impl.interface;
// 5. Use standard writer methods like print
try stdout.print("Hello, {s}!\n", .{"Zig 0.16"});
// 6. Explicitly flush remaining bytes when finished
try stdout.flush();
}
| Feature | Old Way (Pre-0.15/0.16) | New Way (0.16) |
|---|---|---|
| Type Signature | Handled implicitly via anytype |
Explicitly typed as *std.Io.Writer |
| I/O Context | Implicit global state (std.io.getStdOut()) |
Explicitly passed via init.io context |
| Buffering | Composited via std.io.BufferedWriter |
Baked directly into the Writer itself |
| String Formatting | std.fmt.format(writer, ...) |
Methods live directly on the Writer (e.g., writer.print) |
| In-Memory Writing | std.io.fixedBufferStream |
std.Io.Writer.fixed(buf) |
If you just want to format string data into a stack or heap allocation without touching a file or network descriptor, std.io.fixedBufferStream is gone. Instead, use the built-in fixed-buffer utility on the Writer:
var buf: [256]u8 = undefined;
var f_writer = std.Io.Writer.fixed(&buf);
const writer = &f_writer.interface;
try writer.print("Status code: {d}", .{200});