Zig para Programadores Go: Da Simplicidade ao Controle Total

Introdução

Se você vem do Go, Zig vai parecer familiar em alguns aspectos e surpreendente em outros. Ambas as linguagens valorizam simplicidade, compilação rápida e binários estáticos. Porém, Zig opera em um nível mais baixo — sem garbage collector, sem runtime, e com controle explícito de memória.

Este guia mapeia conceitos de Go para Zig, ajudando você a aproveitar seu conhecimento existente. Para um guia de migração completo, veja Guia de Migração: Go para Zig. Para a comparação detalhada, consulte Zig vs Go.

Mapeamento Rápido de Conceitos

GoZigNotas
var x int = 42var x: i32 = 42Tipos explícitos
x := 42const x = 42Inferência de tipo
funcfnFunções
structstructSimilar
interfaceComptime + tipo genéricoSem interfaces de runtime
goroutinestd.Thread / asyncSem green threads
channelSem equivalente diretoUsar mutex/atomic
deferdeferMuito similar
errorError unionsMais tipado
nilnullPara optionals
sliceSlice []TConceito similar
GCAllocatorsDiferença fundamental

Variáveis e Tipos

Go

var nome string = "Zig Brasil"
idade := 25
pi := 3.14
ativo := true

Zig

const nome: []const u8 = "Zig Brasil";
var idade: u32 = 25;
const pi: f64 = 3.14;
const ativo: bool = true;

Em Zig, const é o padrão — variáveis são imutáveis por default. Use var apenas quando precisar mutar. Em Go, todo valor é mutável.

Structs e Métodos

Go

type Ponto struct {
    X, Y float64
}

func (p Ponto) Distancia(outro Ponto) float64 {
    dx := p.X - outro.X
    dy := p.Y - outro.Y
    return math.Sqrt(dx*dx + dy*dy)
}

Zig

const std = @import("std");

const Ponto = struct {
    x: f64,
    y: f64,

    pub fn distancia(self: Ponto, outro: Ponto) f64 {
        const dx = self.x - outro.x;
        const dy = self.y - outro.y;
        return std.math.sqrt(dx * dx + dy * dy);
    }
};

Em Zig, métodos são funções dentro do namespace da struct. O self é explícito, não implícito como o receiver em Go.

Interfaces vs Comptime

Go usa interfaces de runtime com dispatch dinâmico. Zig não tem interfaces — em vez disso, usa comptime para polimorfismo:

Go

type Writer interface {
    Write(p []byte) (n int, err error)
}

func escrever(w Writer, dados []byte) error {
    _, err := w.Write(dados)
    return err
}

Zig

fn escrever(writer: anytype, dados: []const u8) !void {
    try writer.writeAll(dados);
}

// OU com tipo explícito via comptime
fn escreverGenerico(comptime Writer: type, writer: Writer, dados: []const u8) !void {
    try writer.writeAll(dados);
}

O compilador Zig verifica em tempo de compilação se o tipo passado tem o método necessário. Não há vtable ou dispatch dinâmico.

Error Handling

Go

func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divisão por zero")
    }
    return a / b, nil
}

resultado, err := dividir(10, 3)
if err != nil {
    log.Fatal(err)
}

Zig

const DivisaoError = error{DivisaoPorZero};

fn dividir(a: f64, b: f64) DivisaoError!f64 {
    if (b == 0) return error.DivisaoPorZero;
    return a / b;
}

const resultado = dividir(10, 3) catch |err| {
    std.debug.print("Erro: {}\n", .{err});
    return;
};

// Ou com try (propaga o erro para cima)
const resultado2 = try dividir(10, 3);

Zig combina o valor e o erro em um único tipo (error union), eliminando a necessidade de verificar if err != nil manualmente. Veja Error Sets Customizados e Padrões Try/Catch.

Slices

Slices em Go e Zig são conceitualmente similares, mas com diferenças importantes:

Go

numeros := []int{1, 2, 3, 4, 5}
sub := numeros[1:3] // [2, 3]
numeros = append(numeros, 6)

Zig

const numeros = [_]u32{ 1, 2, 3, 4, 5 };
const sub = numeros[1..3]; // [2, 3]

// Para append, usar ArrayList (requer allocator)
var lista = std.ArrayList(u32).init(allocator);
defer lista.deinit();
try lista.appendSlice(&numeros);
try lista.append(6);

Em Zig, slices não crescem automaticamente. Para coleções dinâmicas, use ArrayList com um allocator explícito.

Goroutines vs Threads

Go

func main() {
    ch := make(chan int, 10)
    go func() {
        ch <- 42
    }()
    valor := <-ch
    fmt.Println(valor)
}

Zig

const std = @import("std");

fn trabalho() void {
    // fazer algo
}

pub fn main() !void {
    const thread = try std.Thread.spawn(.{}, trabalho, .{});
    thread.join();
}

Zig usa threads do sistema operacional, não green threads. Não há channels embutidos — use std.Thread.Mutex, std.atomic, ou implemente suas próprias estruturas de comunicação. Veja Concorrência em Zig.

Gerenciamento de Memória: A Maior Diferença

Em Go, o garbage collector cuida de tudo. Em Zig, o programador gerencia memória explicitamente:

Go

func criarLista() []int {
    lista := make([]int, 0, 100)
    for i := 0; i < 100; i++ {
        lista = append(lista, i)
    }
    return lista // GC cuida da memória
}

Zig

fn criarLista(allocator: std.mem.Allocator) ![]u32 {
    var lista = std.ArrayList(u32).init(allocator);
    errdefer lista.deinit();
    for (0..100) |i| {
        try lista.append(@intCast(i));
    }
    return lista.toOwnedSlice();
}

// O chamador é responsável por liberar:
const lista = try criarLista(allocator);
defer allocator.free(lista);

Veja Substituir malloc/free por Allocators e ArenaAllocator.

Defer

Ambas as linguagens têm defer, mas com semântica diferente:

  • Go: defer executa na saída da função
  • Zig: defer executa na saída do escopo (bloco)
fn exemplo() void {
    {
        const recurso = obterRecurso();
        defer liberarRecurso(recurso);
        // recurso é liberado ao sair deste bloco
    }
    // recurso já foi liberado aqui
}

Zig também tem errdefer, que executa apenas quando a função retorna um erro — sem equivalente em Go.

Testes

Go

func TestSoma(t *testing.T) {
    resultado := Soma(2, 3)
    if resultado != 5 {
        t.Errorf("esperava 5, obteve %d", resultado)
    }
}

Zig

test "soma" {
    const resultado = soma(2, 3);
    try std.testing.expectEqual(@as(u32, 5), resultado);
}

Testes em Zig são integrados ao arquivo fonte com o bloco test. Execute com zig test arquivo.zig. Veja Testes Unitários Básicos e Testes com Allocator.

Conclusão

Vindo de Go, as maiores adaptações serão o gerenciamento manual de memória e a ausência de garbage collector. Em compensação, Zig oferece performance previsível, binários menores, e controle total sobre o hardware.

A filosofia de simplicidade de Go ressoa em Zig — ambas rejeitam complexidade desnecessária. Se você gosta de Go pela simplicidade, provavelmente vai gostar de Zig pelo mesmo motivo, com a adição de poder trabalhar em nível mais baixo.

Para dar os primeiros passos, visite Introdução ao Zig e Como Instalar Zig.

Continue aprendendo Zig

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