Se você é um desenvolvedor Rust procurando aprender Zig, este guia é para você. Como Rustacean, você já tem uma base sólida em programação de sistemas — segurança de memória, zero-cost abstractions, e controle de baixo nível. A boa notícia é que muitos desses conceitos se aplicam a Zig, embora de formas diferentes.
Este guia foca nas diferenças práticas entre as linguagens, mostrando como traduzir seus padrões mentais de Rust para código Zig funcional.
Nota: Se você procura uma comparação geral entre as linguagens, leia primeiro Zig vs Rust: Qual Linguagem Escolher em 2026?
Visão Geral: Do que Você Precisa Desaprender
Mentalidade Rust → Mentalidade Zig
| Aspecto | Mentalidade Rust | Mentalidade Zig |
|---|---|---|
| Segurança | “O compilador impede bugs” | “O programador é responsável, com ferramentas de debug” |
| Abstração | Abstrações ricas (traits, generics) | Simplicidade explícita (comptime quando necessário) |
| Ownership | Gerenciado pelo compilador | Gerenciado pelo programador via allocators |
| Erros | Result<T, E> e Option<T> | Error unions !T e tipos opcionais ?T |
| Metaprogramação | Macros e derive | comptime — mesma linguagem |
1. Gerenciamento de Memória
1.1 A Grande Diferença: Ownership vs Allocators
Em Rust, o borrow checker gerencia memória automaticamente. Em Zig, você gerencia explicitamente via allocators.
Rust — Ownership automático:
fn process_data() {
let data = vec![1, 2, 3, 4, 5]; // Vec aloca no heap
// Memória liberada automaticamente quando 'data' sai do escopo
println!("{:?}", data);
} // Drop automático
Zig — Allocator explícito:
const std = @import("std");
fn processData(allocator: std.mem.Allocator) !void {
// Alocação explícita
const data = try allocator.alloc(i32, 5);
defer allocator.free(data); // Liberação explícita
// Preenche dados
for (data, 0..) |*item, i| {
item.* = @intCast(i + 1);
}
std.debug.print("{any}\n", .{data});
} // free chamado automaticamente pelo defer
Padrão Zig para Rustaceans:
- Sempre passe
allocatorpara funções que precisam alocar - Use
deferpara liberação (similar ao Drop implícito do Rust) - Use
errdeferpara liberação em caso de erro (similar a guard clauses)
1.2 GeneralPurposeAllocator — O “GC” de Debug do Zig
Para começar, use GeneralPurposeAllocator. Ele detecta vazamentos e double-free em debug builds:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const status = gpa.deinit();
if (status == .leak) @panic("Memory leak detected!");
}
const allocator = gpa.allocator();
// Use 'allocator' para todas as alocações
const string = try allocator.dupe(u8, "Hello, Zig!");
defer allocator.free(string);
std.debug.print("{s}\n", .{string});
}
1.3 Arena Allocator — O “Owned” do Rust
Quando você quer agrupar alocações e liberar tudo de uma vez (similar a um Vec<Box<T>> que dropa tudo):
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // Libera TUDO de uma vez
const allocator = arena.allocator();
// Faça várias alocações
const a = try allocator.alloc(u8, 100);
const b = try allocator.alloc(u8, 200);
const c = try allocator.dupe(u8, "texto");
// Não precisa de defer para cada um!
// Tudo é liberado quando arena.deinit() é chamado
1.4 Tabela de Conversão: Alocações
| Operação Rust | Equivalente Zig |
|---|---|
Box::new(x) | try allocator.create(T) + defer allocator.destroy(ptr) |
vec![1,2,3] | try allocator.alloc(T, n) + preencher |
String::new() | std.ArrayList(u8) ou allocator.alloc(u8, n) |
Vec::with_capacity(n) | try allocator.alloc(T, n) |
vec.push(x) | array_list.append(x) (use ArrayList para grow) |
drop(x) | Implícito via defer/errdefer |
Box::leak(x) | Use arena allocator ou std.heap.page_allocator |
2. Error Handling
2.1 Result<T, E> → Error Unions
Rust:
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
fn main() {
match read_file("data.txt") {
Ok(contents) => println!("{}", contents),
Err(e) => eprintln!("Error: {}", e),
}
}
Zig:
const std = @import("std");
fn readFile(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
return std.fs.cwd().readFileAlloc(allocator, path, 1024 * 1024);
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const contents = readFile(allocator, "data.txt") catch |err| {
std.debug.print("Error: {}\n", .{err});
return;
};
defer allocator.free(contents);
std.debug.print("{s}\n", .{contents});
}
2.2 O Operador try — Similar ao ? do Rust
Ambos usam try/? para propagar erros:
// Rust
fn process() -> Result<i32, Error> {
let data = read_file("input.txt")?; // Propaga erro com '?'
let parsed = parse_i32(&data)?; // Propaga erro com '?'
Ok(parsed * 2)
}
// Zig
fn process(allocator: std.mem.Allocator) !i32 {
const data = try readFile(allocator, "input.txt"); // Propaga erro com 'try'
defer allocator.free(data);
const parsed = try std.fmt.parseInt(i32, data, 10); // Propaga erro com 'try'
return parsed * 2;
}
2.3 Error Sets — Definindo Erros Específicos
Rust:
#[derive(Debug)]
enum MyError {
InvalidInput,
NotFound,
PermissionDenied,
}
fn do_something() -> Result<i32, MyError> {
Err(MyError::InvalidInput)
}
Zig:
const MyError = error{
InvalidInput,
NotFound,
PermissionDenied,
};
fn doSomething() MyError!i32 {
return error.InvalidInput;
}
2.4 errdefer — Limpeza em Caso de Erro
Equivalente a scopeguard ou Drop em código que pode falhar:
fn createResource(allocator: std.mem.Allocator) !Resource {
const resource = try allocator.create(Resource);
// Se a função retornar erro após este ponto, 'destroy' será chamado
errdefer allocator.destroy(resource);
resource.data = try allocator.alloc(u8, 100);
// Se falhar aqui, errdefer acima executa
resource.handle = try openHandle();
// Sucesso — cancela o errdefer
return resource;
}
3. Tipos e Generics
3.1 Structs Básicos
Rust:
struct Point {
x: f64,
y: f64,
}
impl Point {
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
Zig:
const Point = struct {
x: f64,
y: f64,
// "Método" — na verdade, função associada
pub fn new(x: f64, y: f64) Point {
return .{ .x = x, .y = y };
}
// Recebe self por valor (copia)
pub fn distanceFromOrigin(self: Point) f64 {
return @sqrt(self.x * self.x + self.y * self.y);
}
// Recebe self por ponteiro (modificável)
pub fn translate(self: *Point, dx: f64, dy: f64) void {
self.x += dx;
self.y += dy;
}
};
Diferenças importantes:
- Não há
implblocks em Zig — métodos são declarados dentro da struct selfé explícito:Point,*Point(mutável), ou*const Point(imutável)- Construtores são convenção, não obrigatórios
3.2 Generics via comptime
Rust:
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn new(value: T) -> Self {
Container { value }
}
fn get(&self) -> &T {
&self.value
}
}
// Uso
let c = Container::new(42);
Zig:
fn Container(comptime T: type) type {
return struct {
value: T,
pub fn new(value: T) @This() {
return .{ .value = value };
}
pub fn get(self: @This()) T {
return self.value;
}
};
}
// Uso
const IntContainer = Container(i32);
var c = IntContainer.new(42);
Padrão idiomático Zig:
// Mais limpo e comum na prática
fn Container(comptime T: type) type {
return struct {
const Self = @This();
value: T,
pub fn new(value: T) Self {
return .{ .value = value };
}
pub fn get(self: Self) T {
return self.value;
}
};
}
3.3 Traits → ?
Rust tem traits para polimorfismo. Zig não tem equivalente direto. Aqui estão as alternativas:
Opção 1: Duck typing com comptime (mais comum)
// Não precisa de trait — só funciona se o tipo tiver o método
fn printDebug(value: anytype) void {
// comptime verificação: o tipo tem método 'debug'?
std.debug.print("{any}\n", .{value.debug()});
}
Opção 2: Struct de funções (vtable manual)
const Writer = struct {
context: *anyopaque,
writeFn: *const fn (*anyopaque, []const u8) anyerror!usize,
pub fn write(self: Writer, bytes: []const u8) !usize {
return self.writeFn(self.context, bytes);
}
};
// Implementação para File
fn fileWrite(context: *anyopaque, bytes: []const u8) !usize {
const file: *std.fs.File = @ptrCast(@alignCast(context));
return file.write(bytes);
}
Opção 3: Union de tipos conhecidos
const Value = union(enum) {
int: i64,
float: f64,
string: []const u8,
pub fn format(self: Value) []const u8 {
return switch (self) {
.int => |i| std.fmt.allocPrint(allocator, "{d}", .{i}),
.float => |f| std.fmt.allocPrint(allocator, "{d}", .{f}),
.string => |s| s,
};
}
};
4. Collections
4.1 Vec → ArrayList(T)
Rust:
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3);
for item in &vec {
println!("{}", item);
}
Zig:
var list = std.ArrayList(i32).init(allocator);
defer list.deinit();
try list.append(1);
try list.append(2);
try list.append(3);
for (list.items) |item| {
std.debug.print("{d}\n", .{item});
}
4.2 HashMap<K, V>
Rust:
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("key", "value");
if let Some(value) = map.get("key") {
println!("{}", value);
}
Zig:
var map = std.StringHashMap([]const u8).init(allocator);
defer map.deinit();
try map.put("key", "value");
if (map.get("key")) |value| {
std.debug.print("{s}\n", .{value});
}
5. Strings
5.1 String → []const u8 ou ArrayList(u8)
Em Zig, strings são simplesmente slices de bytes ([]const u8).
Rust:
let s1 = "string slice"; // &str
let s2 = String::from("owned"); // String
let s3 = format!("number: {}", 42); // String formatada
Zig:
const s1 = "string slice"; // []const u8
const s2 = try allocator.dupe(u8, "owned"); // []const u8 alocado
defer allocator.free(s2);
const s3 = try std.fmt.allocPrint(allocator, "number: {d}", .{42});
defer allocator.free(s3);
// String mutável (construção)
var builder = std.ArrayList(u8).init(allocator);
defer builder.deinit();
try builder.appendSlice("Hello");
try builder.appendSlice(", World!");
const result = builder.items; // []const u8
5.2 String Slices
Rust:
let s = "hello world";
let hello = &s[0..5]; // &str
let world = &s[6..11]; // &str
Zig:
const s = "hello world";
const hello = s[0..5]; // []const u8
const world = s[6..11]; // []const u8
Nota: Zig faz bounds checking em runtime em builds de debug (como Rust em modo debug).
6. Pattern Matching
6.1 match → switch
Rust:
match value {
1 => println!("one"),
2 => println!("two"),
3..=10 => println!("three to ten"),
_ => println!("other"),
}
Zig:
switch (value) {
1 => std.debug.print("one\n", .{}),
2 => std.debug.print("two\n", .{}),
3...10 => std.debug.print("three to ten\n", .{}),
else => std.debug.print("other\n", .{}),
}
6.2 if let → if com capture
Rust:
if let Some(value) = maybe_value {
println!("{}", value);
}
if let Ok(result) = fallible_operation() {
println!("{}", result);
}
Zig:
if (maybe_value) |value| {
std.debug.print("{any}\n", .{value});
}
if (fallibleOperation()) |result| {
std.debug.print("{any}\n", .{result});
} else |err| {
std.debug.print("Error: {}\n", .{err});
}
6.3 while let → while com capture
Rust:
while let Some(item) = iterator.next() {
println!("{}", item);
}
Zig:
while (iterator.next()) |item| {
std.debug.print("{any}\n", .{item});
}
7. Option → ?T
Rust:
fn find_user(id: u32) -> Option<User> {
// ...
}
match find_user(42) {
Some(user) => println!("{}", user.name),
None => println!("User not found"),
}
// Ou com if let
if let Some(user) = find_user(42) {
println!("{}", user.name);
}
// Ou com ?
let user = find_user(42)?;
Zig:
fn findUser(id: u32) ?User {
// ...
}
if (findUser(42)) |user| {
std.debug.print("{s}\n", .{user.name});
} else {
std.debug.print("User not found\n", .{});
}
// Propaga null com 'try' em contexto opcional
const user = findUser(42) orelse return null;
// Orelse para default
const user = findUser(42) orelse User{ .name = "Unknown" };
8. Iteradores
8.1 Iterator → Slice ou While Loop
Zig não tem um sistema de iteradores tão rico quanto Rust. Você geralmente itera sobre slices diretamente.
Rust:
let sum: i32 = vec.iter()
.filter(|x| **x > 0)
.map(|x| x * 2)
.sum();
Zig:
var sum: i32 = 0;
for (vec.items) |x| {
if (x > 0) {
sum += x * 2;
}
}
Para operações mais complexas, você pode criar structs de iteração manualmente ou usar comptime para gerar código.
8.2 Iterator com Índice
Rust:
for (i, item) in vec.iter().enumerate() {
println!("{}: {}", i, item);
}
Zig:
for (vec.items, 0..) |item, i| {
std.debug.print("{d}: {any}\n", .{ i, item });
}
9. Async/Await
9.1 Conceitos Similares
A sintaxe é muito parecida, mas o modelo subjacente difere.
Rust (com Tokio):
async fn fetch_data(url: &str) -> Result<String, Error> {
let response = reqwest::get(url).await?;
let text = response.text().await?;
Ok(text)
}
#[tokio::main]
async fn main() {
let data = fetch_data("https://example.com").await.unwrap();
println!("{}", data);
}
Zig:
const std = @import("std");
fn fetchData(allocator: std.mem.Allocator, url: []const u8) ![]const u8 {
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();
const uri = try std.Uri.parse(url);
var server_header_buffer: [4096]u8 = undefined;
var request = try client.open(.GET, uri, .{
.server_header_buffer = &server_header_buffer,
});
defer request.deinit();
try request.send();
try request.finish();
try request.wait();
return try request.reader().readAllAlloc(allocator, 1024 * 1024);
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const data = try fetchData(allocator, "https://example.com");
defer allocator.free(data);
std.debug.print("{s}\n", .{data});
}
Nota: O Zig 0.13+ tem suporte a async/await mas é menos central na linguagem do que em Rust moderno. A std lib usa um modelo mais tradicional de callbacks/polling para I/O.
10. Build System: Cargo → build.zig
10.1 Comandos Comuns
| Cargo | Zig |
|---|---|
cargo new proj | mkdir proj && cd proj && zig init |
cargo build | zig build |
cargo run | zig build run |
cargo test | zig build test |
cargo add dep | Edite build.zig.zon manualmente |
cargo publish | Não há registry público ainda |
10.2 build.zig Básico
Rust (Cargo.toml):
[package]
name = "my-project"
version = "1.0.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
Zig (build.zig):
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "my-project",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Dependências via build.zig.zon são automaticamente disponíveis
// const dep = b.dependency("nome_dep", .{});
// exe.root_module.addImport("nome", dep.module("nome"));
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
10.3 build.zig.zon (Dependências)
.{
.name = "my-project",
.version = "1.0.0",
.dependencies = .{
.known_folders = .{
.url = "https://github.com/ziglibs/known-folders/archive/refs/tags/0.1.0.tar.gz",
.hash = "...",
},
},
}
11. Testes
11.1 Testes Unitários
Rust:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
#[test]
#[should_panic]
fn test_panic() {
panic!("expected");
}
}
Zig:
fn add(a: i32, b: i32) i32 {
return a + b;
}
test "add" {
try std.testing.expectEqual(add(2, 2), 4);
}
test "panic" {
// Para testar panics, use std.testing.expectError ou execute em subprocesso
}
11.2 Testes com Allocators
test "allocation" {
const allocator = std.testing.allocator; // Detecta vazamentos automaticamente
const slice = try allocator.alloc(u8, 100);
defer allocator.free(slice); // Obrigatório, ou teste falha
// Teste seus dados
@memset(slice, 0);
try std.testing.expectEqual(slice[0], 0);
} // Se houver vazamento, o teste falha
12. Macros → comptime
12.1 Derive Macros
Rust:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
Zig:
Zig não tem derive automático. Você implementa manualmente ou gera com comptime:
const Point = struct {
x: i32,
y: i32,
// Implemente manualmente
pub fn format(
self: Point,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("Point{{ x: {d}, y: {d} }}", .{ self.x, self.y });
}
};
12.2 Geração de Código com comptime
Rust (macro_rules):
macro_rules! vec_of_strings {
($($s:expr),*) => {
vec![$($s.to_string()),*]
};
}
let v = vec_of_strings!["a", "b", "c"];
Zig (comptime):
fn VecOfStrings(comptime strings: []const []const u8) type {
return struct {
const Self = @This();
items: [strings.len][]const u8,
pub fn init() Self {
var result: Self = undefined;
inline for (strings, 0..) |s, i| {
result.items[i] = s;
}
return result;
}
};
}
const MyStrings = VecOfStrings(&.{"a", "b", "c"});
var v = MyStrings.init();
Resumo: Guia Rápido de Tradução
| Conceito | Rust | Zig |
|---|---|---|
| Alocação heap | Box, Vec | allocator.create, allocator.alloc, ArrayList |
| Liberação automática | Drop, ownership | defer, errdefer |
| Erros | Result<T, E>, Option<T> | T!E, ?T |
| Propagar erro | ? | try |
| Null check | if let, match | if (x) |v|, orelse |
| Generics | <T> | comptime T: type |
| Traits | trait + impl | comptime duck typing, vtables manuais |
| Iteradores | .iter(), .map() | for em slices |
| Strings | String, &str | []const u8, ArrayList(u8) |
| Módulos | mod, pub | const, pub |
| Testes | #[test] | test "nome" |
| Build | Cargo.toml | build.zig + build.zig.zon |
Próximos Passos
- 🔄 Como Instalar o Zig — Configure seu ambiente
- 📚 Zig para Iniciantes — Revise conceitos básicos
- ⚡ Comptime em Zig — A feature mais poderosa do Zig
- 🧪 Testes em Zig — Como testar código Zig efetivamente
- 🆚 Zig vs Rust: Qual Linguagem Escolher? — Comparação mais aprofundada
Dúvidas específicas de migração? A comunidade Zig é acolhedora e muitos membros vêm de Rust. Não hesite em perguntar!
Última atualização: 10 de fevereiro de 2026
Versão do Zig: 0.13.0