Sintaxe Básica de Zig: Variáveis, Tipos e Funções — Guia Completo

Este é o segundo artigo da série “Zig para Iniciantes”. Se você ainda não leu o primeiro, comece por Zig para Iniciantes: Primeiros Passos.

Neste guia, vamos aprofundar na sintaxe de Zig — tudo que você precisa saber sobre variáveis, tipos de dados, funções e escopo. Ao final, você terá domínio completo dos blocos de construção fundamentais da linguagem.

🎯 Objetivo: Entender profundamente como Zig lida com dados e organização de código.


Variáveis em Zig: const vs var

A distinção entre constantes e variáveis é um dos pilares da programação segura em Zig.

const (Constantes)

Valores que nunca mudam após serem definidos:

const PI: f64 = 3.14159265359;
const NOME_DO_PROJETO = "MeuApp";
const MAX_USUARIOS: u32 = 1000;

// ❌ ERRO: não pode modificar const
// PI = 3.14;

var (Variáveis Mutáveis)

Valores que podem ser modificados:

var contador: u32 = 0;
contador = 1;        // ✅ OK
contador += 1;       // ✅ OK (agora é 2)

Por Que Preferir const?

const std = @import("std");

pub fn main() void {
    // Código com const é mais fácil de entender
    const raio: f64 = 5.0;
    const area = PI * raio * raio;
    
    // Você SABE que 'area' não vai mudar
    std.debug.print("Área: {d:.2}\n", .{area});
}

Benefícios de usar const:

  • 🧠 Menos coisas para se preocupar
  • 🐛 Menos bugs (valores não mudam inesperadamente)
  • ⚡ Melhor otimização pelo compilador
  • 📖 Código mais fácil de ler

Tipos de Variáveis Especiais

threadlocal

Variáveis que existem uma por thread:

threadlocal var contador_thread: u32 = 0;

volatile

Para acesso a hardware ou memória compartilhada:

const registrador_hardware: *volatile u32 = @ptrFromInt(0x1000_0000);

Sistema de Tipos do Zig

Zig tem um sistema de tipos explícito e completo. Vamos explorar cada categoria.

Inteiros

TipoBitsValor MínimoValor Máximo
i88-128127
u880255
i1616-32.76832.767
u1616065.535
i3232-2.147.483.6482.147.483.647
u323204.294.967.295
i6464~-9 quintilhões~9 quintilhões
u64640~18 quintilhões
i128128Muito grandeMuito grande
u1281280Enorme

Regras práticas:

  • Use u8 para bytes e caracteres ASCII
  • Use i32/u32 para números gerais
  • Use i64/u64 para IDs, timestamps, contadores grandes
  • Use usize para tamanhos de memória e índices
const std = @import("std");

pub fn main() void {
    // Idade humana: 0-150, cabe em u8
    const idade: u8 = 25;
    
    // População mundial: cabe em u32
    const populacao_mundial: u32 = 8_000_000_000;
    
    // Timestamp Unix: precisa de i64
    const timestamp: i64 = 1_707_686_400;
    
    std.debug.print("Idade: {d} anos\n", .{idade});
    std.debug.print("População: {d} pessoas\n", .{populacao_mundial});
    std.debug.print("Timestamp: {d}\n", .{timestamp});
}

Tipos de Tamanho Arbitrário

Você pode criar inteiros de qualquer tamanho:

const a: u7 = 100;      // 7 bits sem sinal
const b: i23 = -42;     // 23 bits com sinal
const c: u1 = 1;        // 1 bit (0 ou 1)

Tipos de Ponto Flutuante

TipoPrecisãoBytesUse Quando…
f16Meia2Pouca memória, precisão não crítica
f32Simples4Padrão para gráficos, jogos
f64Dupla8Padrão geral, cálculos científicos
f80Estendida10Cálculos x87 (raro)
f128Quadrupla16Precisão extrema (raro)
const std = @import("std");

pub fn main() void {
    const temperatura: f64 = 23.5;
    const pi: f32 = 3.14159;
    const nota_cientifica: f64 = 1.5e10;  // 15 bilhões
    
    std.debug.print("Temperatura: {d:.1}°C\n", .{temperatura});
    std.debug.print("PI (f32): {d:.5}\n", .{pi});
    std.debug.print("Notação científica: {e}\n", .{nota_cientifica});
}

Booleanos

const ativo: bool = true;
const desligado: bool = false;

// Operações lógicas
const a = true;
const b = false;

const e_logico = a and b;    // false
const ou_logico = a or b;    // true
const nao_logico = !a;       // false

Caracteres (u8, u21, u32)

const letra_ascii: u8 = 'A';        // ASCII simples
const emoji: u21 = '🎉';            // Unicode (21 bits)
const qualquer_unicode: u32 = '中'; // Qualquer codepoint Unicode

Arrays e Slices

Arrays (Tamanho Fixo)

const std = @import("std");

pub fn main() void {
    // Array de inteiros (tamanho conhecido em compile-time)
    const numeros = [_]i32{ 10, 20, 30, 40, 50 };
    
    // [_] significa "inferir o tamanho"
    // É o mesmo que: const numeros: [5]i32 = ...
    
    std.debug.print("Primeiro: {d}\n", .{numeros[0]});
    std.debug.print("Tamanho: {d}\n", .{numeros.len});
    
    // Array de strings
    const nomes = [_][]const u8{ "Ana", "Bruno", "Carla" };
    
    for (nomes) |nome| {
        std.debug.print("Nome: {s}\n", .{nome});
    }
}

Slices (“Fatias” de Arrays)

Slices são referências para uma sequência de elementos:

const std = @import("std");

pub fn main() void {
    const numeros = [_]i32{ 10, 20, 30, 40, 50 };
    
    // Slice dos elementos 1 a 3 (índices 1, 2)
    const fatia: []const i32 = numeros[1..3];
    
    std.debug.print("Fatia: ", .{});
    for (fatia) |n| {
        std.debug.print("{d} ", .{n});
    }
    std.debug.print("\n", .{});
    // Saída: Fatia: 20 30
}

Arrays Multidimensionais

const std = @import("std");

pub fn main() void {
    // Matriz 3x3
    const matriz = [3][3]i32{
        [_]i32{ 1, 2, 3 },
        [_]i32{ 4, 5, 6 },
        [_]i32{ 7, 8, 9 },
    };
    
    std.debug.print("Elemento [1][2]: {d}\n", .{matriz[1][2]});  // 6
}

Strings em Zig

Strings em Zig são arrays de bytes (u8) com codificação UTF-8.

String Literais

const std = @import("std");

pub fn main() void {
    // String literal: []const u8
    const mensagem = "Olá, Zig!";
    
    std.debug.print("Mensagem: {s}\n", .{mensagem});
    std.debug.print("Tamanho: {d} bytes\n", .{mensagem.len});
    
    // Caracteres individuais
    const primeira_letra = mensagem[0];  // 'O' (79 em ASCII)
    std.debug.print("Primeira letra: {c}\n", .{primeira_letra});
}

Concatenação e Manipulação

const std = @import("std");

pub fn main() !void {
    // Para manipulação dinâmica, precisamos de um allocator
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    // Concatenar strings
    const parte1 = "Olá, ";
    const parte2 = "Mundo!";
    
    const resultado = try std.fmt.allocPrint(allocator, "{s}{s}", .{ parte1, parte2 });
    defer allocator.free(resultado);
    
    std.debug.print("{s}\n", .{resultado});
}

💡 Nota: Strings dinâmicas exigem alocação de memória. Veremos isso em detalhes no artigo sobre Gerenciamento de Memória.


Funções em Profundidade

Estrutura Básica

// Função sem parâmetros e sem retorno
fn dizOla() void {
    std.debug.print("Olá!\n", .{});
}

// Função com parâmetros
fn soma(a: i32, b: i32) i32 {
    return a + b;
}

// Função com múltiplos parâmetros de tipos diferentes
fn apresentar(nome: []const u8, idade: u8) void {
    std.debug.print("{s} tem {d} anos\n", .{ nome, idade });
}

Retorno de Múltiplos Valores

Zig permite retornar múltiplos valores usando structs anônimos:

const std = @import("std");

fn divideComResto(dividendo: i32, divisor: i32) struct { quociente: i32, resto: i32 } {
    return .{
        .quociente = @divTrunc(dividendo, divisor),
        .resto = @rem(dividendo, divisor),
    };
}

pub fn main() void {
    const resultado = divideComResto(17, 5);
    
    std.debug.print("17 / 5 = {d} (resto {d})\n", .{
        resultado.quociente,
        resultado.resto,
    });
    // Saída: 17 / 5 = 3 (resto 2)
}

Funções com Error Union

const std = @import("std");

const ErroDivisao = error{ DivisaoPorZero };

fn divideSegura(a: f64, b: f64) ErroDivisao!f64 {
    if (b == 0) {
        return ErroDivisao.DivisaoPorZero;
    }
    return a / b;
}

pub fn main() void {
    const resultado = divideSegura(10, 2) catch |err| {
        std.debug.print("Erro: {}\n", .{err});
        return;
    };
    
    std.debug.print("Resultado: {d}\n", .{resultado});
}

Funções Anônimas (Inline)

const std = @import("std");

pub fn main() void {
    // Definir uma struct com função de callback
    const Operacao = struct {
        fn executar(a: i32, b: i32, operacao: fn (i32, i32) i32) i32 {
            return operacao(a, b);
        }
    };
    
    const soma = struct {
        fn call(x: i32, y: i32) i32 {
            return x + y;
        }
    }.call;
    
    const resultado = Operacao.executar(5, 3, soma);
    std.debug.print("5 + 3 = {d}\n", .{resultado});
}

Funções com comptime (Avançado)

// Função que aceita qualquer tipo em compile-time
fn maior(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

pub fn main() void {
    const max_int = maior(i32, 10, 20);     // 20
    const max_float = maior(f64, 3.14, 2.71); // 3.14
    
    std.debug.print("Maior inteiro: {d}\n", .{max_int});
    std.debug.print("Maior float: {d}\n", .{max_float});
}

Escopo e Visibilidade

Blocos e Escopo

const std = @import("std");

pub fn main() void {
    const x = 10;  // Escopo da função main
    
    {
        const y = 20;  // Escopo do bloco interno
        std.debug.print("x = {d}, y = {d}\n", .{ x, y });
    }
    
    // y não existe mais aqui
    std.debug.print("x = {d}\n", .{x});
    // std.debug.print("y = {d}\n", .{y});  // ❌ ERRO: y não declarado
}

Shadowing (Sobreposição)

const std = @import("std");

pub fn main() void {
    const x: i32 = 10;
    std.debug.print("x externo: {d}\n", .{x});
    
    {
        const x: f64 = 3.14;  // Shadowing permitido
        std.debug.print("x interno: {d}\n", .{x});
    }
    
    std.debug.print("x externo novamente: {d}\n", .{x});
}

pub (Visibilidade Pública)

// Este módulo
const math = struct {
    // Público: acessível de fora
    pub fn soma(a: i32, b: i32) i32 {
        return a + b;
    }
    
    // Privado: só acessível dentro deste struct
    fn subtracao(a: i32, b: i32) i32 {
        return a - b;
    }
    
    // Pode chamar função privada de pública
    pub fn operacaoSecreta(a: i32, b: i32) i32 {
        return subtracao(a, b);
    }
};

pub fn main() void {
    const resultado1 = math.soma(5, 3);           // ✅ OK
    // const resultado2 = math.subtracao(5, 3);   // ❌ ERRO: função privada
    const resultado3 = math.operacaoSecreta(5, 3); // ✅ OK
    
    std.debug.print("Soma: {d}\n", .{resultado1});
    std.debug.print("Operação secreta: {d}\n", .{resultado3});
}

Conversão de Tipos

Casting Explícito

Zig não converte tipos implicitamente. Você deve ser explícito:

const std = @import("std");

pub fn main() void {
    const a: i32 = 100;
    const b: f64 = @floatFromInt(a);  // i32 -> f64
    
    const c: f64 = 3.14;
    const d: i32 = @intFromFloat(c);  // f64 -> i32 (trunca)
    
    const e: u8 = 255;
    const f: i16 = @intCast(e);       // u8 -> i16
    
    std.debug.print("a={d}, b={d}, c={d}, d={d}\n", .{ a, b, c, d });
}

@truncate vs @intCast

const std = @import("std");

pub fn main() void {
    // @truncate: corta bits (pode perder dados)
    const grande: u16 = 1000;
    const pequeno_truncado: u8 = @truncate(grande);  // 1000 % 256 = 232
    
    // @intCast: converde com verificação em debug mode
    const valor: i32 = 100;
    const convertido: i64 = @intCast(valor);  // Sempre seguro: i32 cabe em i64
    
    std.debug.print("Truncado: {d}\n", .{pequeno_truncado});
    std.debug.print("Convertido: {d}\n", .{convertido});
}

@as (Casting Explícito)

const std = @import("std");

pub fn main() void {
    // Força o tipo de um literal
    const x = @as(u32, 100);  // 100 é tratado como u32
    const y = @as(f32, 3.14); // 3.14 é tratado como f32
    
    std.debug.print("x: {d} ({s})\n", .{ x, @typeName(@TypeOf(x)) });
}

Exercícios Práticos

Exercício 1: Calculadora Completa

Crie uma calculadora com funções separadas para cada operação:

Ver Solução
const std = @import("std");

fn soma(a: f64, b: f64) f64 { return a + b; }
fn subtracao(a: f64, b: f64) f64 { return a - b; }
fn multiplicacao(a: f64, b: f64) f64 { return a * b; }
fn divisao(a: f64, b: f64) f64 { return a / b; }

pub fn main() void {
    const x: f64 = 15.5;
    const y: f64 = 4.5;
    
    std.debug.print("{d:.1} + {d:.1} = {d:.1}\n", .{ x, y, soma(x, y) });
    std.debug.print("{d:.1} - {d:.1} = {d:.1}\n", .{ x, y, subtracao(x, y) });
    std.debug.print("{d:.1} * {d:.1} = {d:.1}\n", .{ x, y, multiplicacao(x, y) });
    std.debug.print("{d:.1} / {d:.1} = {d:.1}\n", .{ x, y, divisao(x, y) });
}

Exercício 2: Conversor de Temperatura

Converta Celsius para Fahrenheit e Kelvin:

Ver Solução
const std = @import("std");

fn celsiusParaFahrenheit(c: f64) f64 {
    return (c * 9.0 / 5.0) + 32.0;
}

fn celsiusParaKelvin(c: f64) f64 {
    return c + 273.15;
}

pub fn main() void {
    const celsius: f64 = 25.0;
    
    const fahrenheit = celsiusParaFahrenheit(celsius);
    const kelvin = celsiusParaKelvin(celsius);
    
    std.debug.print("{d:.1}°C = {d:.1}°F = {d:.2}K\n", .{
        celsius, fahrenheit, kelvin
    });
}

Exercício 3: Estatísticas de Array

Calcule média, máximo e mínimo de um array:

Ver Solução
const std = @import("std");

fn media(numeros: []const f64) f64 {
    var soma: f64 = 0;
    for (numeros) |n| {
        soma += n;
    }
    return soma / @as(f64, @floatFromInt(numeros.len));
}

fn maximo(numeros: []const f64) f64 {
    var max = numeros[0];
    for (numeros[1..]) |n| {
        if (n > max) max = n;
    }
    return max;
}

fn minimo(numeros: []const f64) f64 {
    var min = numeros[0];
    for (numeros[1..]) |n| {
        if (n < min) min = n;
    }
    return min;
}

pub fn main() void {
    const notas = [_]f64{ 8.5, 9.0, 7.5, 10.0, 6.5 };
    
    std.debug.print("Estatísticas das Notas:\n", .{});
    std.debug.print("  Média: {d:.2}\n", .{media(&notas)});
    std.debug.print("  Máximo: {d:.1}\n", .{maximo(&notas)});
    std.debug.print("  Mínimo: {d:.1}\n", .{minimo(&notas)});
}

FAQ — Perguntas Frequentes

Qual a diferença entre []u8 e [5]u8?

  • [5]u8: Array de exatamente 5 bytes (tamanho conhecido em compile-time)
  • []u8: Slice — referência para uma sequência de bytes (tamanho em runtime)

Por que Zig não tem string como tipo nativo?

Strings são semanticamente arrays de bytes. Zig evita tipos “mágicos” — tudo é explícito.

Posso retornar um array de uma função?

Sim, se o tamanho for conhecido em compile-time:

fn retornaArray() [3]i32 {
    return .{ 1, 2, 3 };
}

Para arrays dinâmicos, use alocação ou retorne um slice.

Como faço cast de inteiros de forma segura?

Use @intCast para conversões que podem falhar — Zig verifica em modo debug:

const pequeno: u8 = @intCast(grande);  // Panic em debug se não couber

Use @truncate quando quer cortar bits explicitamente.


Resumo

Neste artigo, você aprendeu:

  • ✅ Diferença entre const e var
  • ✅ Todos os tipos numéricos (inteiros e floats)
  • ✅ Arrays, slices e strings
  • ✅ Como criar e usar funções
  • ✅ Escopo e visibilidade
  • ✅ Conversão de tipos

📚 Continue Aprendendo


Este artigo faz parte da série “Zig para Iniciantes”. Tem dúvidas? Deixe um comentário!

Happy coding! 🦎

Continue aprendendo Zig

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