Zig para Desenvolvedores Rust: Guia de Transição Completo

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

AspectoMentalidade RustMentalidade Zig
Segurança“O compilador impede bugs”“O programador é responsável, com ferramentas de debug”
AbstraçãoAbstrações ricas (traits, generics)Simplicidade explícita (comptime quando necessário)
OwnershipGerenciado pelo compiladorGerenciado pelo programador via allocators
ErrosResult<T, E> e Option<T>Error unions !T e tipos opcionais ?T
MetaprogramaçãoMacros e derivecomptime — 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:

  1. Sempre passe allocator para funções que precisam alocar
  2. Use defer para liberação (similar ao Drop implícito do Rust)
  3. Use errdefer para 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 RustEquivalente 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:

  1. Não há impl blocks em Zig — métodos são declarados dentro da struct
  2. self é explícito: Point, *Point (mutável), ou *const Point (imutável)
  3. 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

CargoZig
cargo new projmkdir proj && cd proj && zig init
cargo buildzig build
cargo runzig build run
cargo testzig build test
cargo add depEdite build.zig.zon manualmente
cargo publishNã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

ConceitoRustZig
Alocação heapBox, Vecallocator.create, allocator.alloc, ArrayList
Liberação automáticaDrop, ownershipdefer, errdefer
ErrosResult<T, E>, Option<T>T!E, ?T
Propagar erro?try
Null checkif let, matchif (x) |v|, orelse
Generics<T>comptime T: type
Traitstrait + implcomptime duck typing, vtables manuais
Iteradores.iter(), .map()for em slices
StringsString, &str[]const u8, ArrayList(u8)
Módulosmod, pubconst, pub
Testes#[test]test "nome"
BuildCargo.tomlbuild.zig + build.zig.zon

Próximos Passos

  1. 🔄 Como Instalar o Zig — Configure seu ambiente
  2. 📚 Zig para Iniciantes — Revise conceitos básicos
  3. Comptime em Zig — A feature mais poderosa do Zig
  4. 🧪 Testes em Zig — Como testar código Zig efetivamente
  5. 🆚 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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.