Guia de Migração: Go para Zig

Introdução

Go e Zig compartilham valores de simplicidade, mas operam em níveis diferentes. Go tem garbage collector, goroutines e channels; Zig tem allocators manuais, threads do SO e controle de baixo nível. Este guia cobre a conversão prática de código Go para Zig.

Para uma visão conceitual, veja Zig para Programadores Go e Zig vs Go.

Pré-requisitos

Tipos e Variáveis

Go

var nome string = "Zig Brasil"
idade := 25
var valores []int = []int{1, 2, 3}
mapa := map[string]int{"a": 1, "b": 2}

Zig

const nome: []const u8 = "Zig Brasil";
var idade: u32 = 25;
const valores = [_]i32{ 1, 2, 3 };

var mapa = std.StringHashMap(i32).init(allocator);
defer mapa.deinit();
try mapa.put("a", 1);
try mapa.put("b", 2);

Structs e Métodos

Go

type Servidor struct {
    Host string
    Porta int
    ativo bool
}

func (s *Servidor) Iniciar() error {
    s.ativo = true
    return nil
}

func (s Servidor) Endereco() string {
    return fmt.Sprintf("%s:%d", s.Host, s.Porta)
}

Zig

const Servidor = struct {
    host: []const u8,
    porta: u16,
    ativo: bool,

    pub fn iniciar(self: *Servidor) !void {
        self.ativo = true;
    }

    pub fn endereco(self: Servidor, buffer: []u8) ![]u8 {
        return try std.fmt.bufPrint(buffer, "{s}:{}", .{ self.host, self.porta });
    }
};

Interfaces

Go

type Reader interface {
    Read(p []byte) (n int, err error)
}

func processar(r Reader) error {
    buf := make([]byte, 1024)
    n, err := r.Read(buf)
    if err != nil {
        return err
    }
    fmt.Println(string(buf[:n]))
    return nil
}

Zig

// Opção 1: duck typing via anytype (estático)
fn processar(reader: anytype) !void {
    var buf: [1024]u8 = undefined;
    const n = try reader.read(&buf);
    std.debug.print("{s}\n", .{buf[0..n]});
}

// Opção 2: interface explícita com vtable (dinâmico)
const Reader = struct {
    ptr: *anyopaque,
    readFn: *const fn (*anyopaque, []u8) usize,

    pub fn read(self: Reader, buf: []u8) usize {
        return self.readFn(self.ptr, buf);
    }
};

Error Handling

Go

func abrir(caminho string) (*os.File, error) {
    f, err := os.Open(caminho)
    if err != nil {
        return nil, fmt.Errorf("erro ao abrir %s: %w", caminho, err)
    }
    return f, nil
}

f, err := abrir("config.json")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

Zig

fn abrir(caminho: []const u8) !std.fs.File {
    return std.fs.cwd().openFile(caminho, .{}) catch |err| {
        std.log.err("erro ao abrir: {}", .{err});
        return err;
    };
}

const f = try abrir("config.json");
defer f.close();

Veja Padrões Try/Catch e Error Logging.

Slices

Go

numeros := []int{1, 2, 3, 4, 5}
sub := numeros[1:3]
numeros = append(numeros, 6)
copia := make([]int, len(numeros))
copy(copia, numeros)

Zig

const numeros_fixo = [_]i32{ 1, 2, 3, 4, 5 };
const sub = numeros_fixo[1..3];

// Para crescimento dinâmico, usar ArrayList
var numeros = std.ArrayList(i32).init(allocator);
defer numeros.deinit();
try numeros.appendSlice(&numeros_fixo);
try numeros.append(6);

// Cópia
const copia = try allocator.dupe(i32, numeros.items);
defer allocator.free(copia);

Goroutines e Channels

Go

func main() {
    ch := make(chan string, 10)

    go func() {
        ch <- "resultado"
    }()

    valor := <-ch
    fmt.Println(valor)
}

Zig

const std = @import("std");

pub fn main() !void {
    var resultado: []const u8 = undefined;
    var mutex = std.Thread.Mutex{};
    var cond = std.Thread.Condition{};

    const thread = try std.Thread.spawn(.{}, struct {
        fn trabalho(r: *[]const u8, m: *std.Thread.Mutex, c: *std.Thread.Condition) void {
            m.lock();
            defer m.unlock();
            r.* = "resultado";
            c.signal();
        }
    }.trabalho, .{ &resultado, &mutex, &cond });

    {
        mutex.lock();
        defer mutex.unlock();
        cond.wait(&mutex);
    }

    std.debug.print("{s}\n", .{resultado});
    thread.join();
}

Zig não tem channels nativos. Use mutex, condições, ou atomics. Veja Concorrência em Zig.

Defer

Go e Zig têm defer, mas com semântica diferente:

  • Go: defer executa na saída da função, em ordem LIFO
  • Zig: defer executa na saída do escopo (bloco), em ordem LIFO
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 — sem equivalente em Go:

fn criarConexao(allocator: std.mem.Allocator) !*Conexao {
    const conn = try allocator.create(Conexao);
    errdefer allocator.destroy(conn); // só executa se retornar erro

    try conn.conectar();
    return conn;
}

Veja Padrões Errdefer.

Gerenciamento de Memória

Go (GC automático)

func criarLista() []string {
    lista := make([]string, 0)
    lista = append(lista, "item1", "item2")
    return lista // GC cuida da memória
}

Zig (manual via allocators)

fn criarLista(allocator: std.mem.Allocator) !std.ArrayList([]const u8) {
    var lista = std.ArrayList([]const u8).init(allocator);
    errdefer lista.deinit();
    try lista.append("item1");
    try lista.append("item2");
    return lista;
}

// O chamador gerencia o ciclo de vida:
var lista = try criarLista(allocator);
defer lista.deinit();

Veja Substituir malloc/free por Allocators e ArenaAllocator.

Go Modules para build.zig

go.mod

module meu-projeto
go 1.21
require github.com/pkg/errors v0.9.1

build.zig

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "meu-projeto",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Dependências via build.zig.zon
    b.installArtifact(exe);
}

Testes

Go

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

func BenchmarkSoma(b *testing.B) {
    for i := 0; i < b.N; i++ {
        soma(2, 3)
    }
}

Zig

test "soma" {
    try std.testing.expectEqual(@as(i32, 5), soma(2, 3));
}

// Benchmarks via testes customizados
test "benchmark soma" {
    const timer = try std.time.Timer.start();
    var i: usize = 0;
    while (i < 1_000_000) : (i += 1) {
        _ = soma(2, 3);
    }
    const elapsed = timer.read();
    std.debug.print("Tempo: {}ns\n", .{elapsed});
}

Veja Testes Unitários e Benchmarking.

Conclusão

A migração de Go para Zig troca a conveniência do garbage collector e goroutines pela performance previsível e controle de baixo nível. As maiores adaptações são o gerenciamento manual de memória e a ausência de channels/goroutines nativos.

Para avaliar se a migração faz sentido, leia Quando Usar Zig. Para começar com Zig, visite Introdução ao Zig.

Continue aprendendo Zig

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