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
| Tipo | Bits | Valor Mínimo | Valor Máximo |
|---|---|---|---|
i8 | 8 | -128 | 127 |
u8 | 8 | 0 | 255 |
i16 | 16 | -32.768 | 32.767 |
u16 | 16 | 0 | 65.535 |
i32 | 32 | -2.147.483.648 | 2.147.483.647 |
u32 | 32 | 0 | 4.294.967.295 |
i64 | 64 | ~-9 quintilhões | ~9 quintilhões |
u64 | 64 | 0 | ~18 quintilhões |
i128 | 128 | Muito grande | Muito grande |
u128 | 128 | 0 | Enorme |
Regras práticas:
- Use
u8para bytes e caracteres ASCII - Use
i32/u32para números gerais - Use
i64/u64para IDs, timestamps, contadores grandes - Use
usizepara 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
| Tipo | Precisão | Bytes | Use Quando… |
|---|---|---|---|
f16 | Meia | 2 | Pouca memória, precisão não crítica |
f32 | Simples | 4 | Padrão para gráficos, jogos |
f64 | Dupla | 8 | Padrão geral, cálculos científicos |
f80 | Estendida | 10 | Cálculos x87 (raro) |
f128 | Quadrupla | 16 | Precisã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(¬as)});
std.debug.print(" Máximo: {d:.1}\n", .{maximo(¬as)});
std.debug.print(" Mínimo: {d:.1}\n", .{minimo(¬as)});
}
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
constevar - ✅ 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
- ⏳ Artigo 3: Controle de Fluxo em Zig: if, switch, loops — Estruturas de decisão e repetição
- 🔗 Tratamento de Erros em Zig — Error unions e recover
- 🔗 Gerenciamento de Memória — Allocators e ponteiros
- 🔗 Zig Build System — Organização de projetos
Este artigo faz parte da série “Zig para Iniciantes”. Tem dúvidas? Deixe um comentário!
Happy coding! 🦎