Singleton em Zig
O padrão Singleton garante que uma classe/struct tenha apenas uma instância durante toda a execução do programa, fornecendo um ponto de acesso global a ela. Em Zig, esse padrão é implementado de forma diferente de linguagens OOP tradicionais, utilizando variáveis globais, std.once e comptime.
Quando Usar
- Gerenciador de configuração global
- Pool de conexões com banco de dados
- Sistema de logging centralizado
- Cache global da aplicação
- Acesso a hardware (ex: GPIO em sistemas embarcados)
Implementação Básica
Singleton com variável global
const std = @import("std");
const Logger = struct {
nivel: NivelLog,
arquivo: ?std.fs.File,
const NivelLog = enum { debug, info, warn, err };
// Instância global única
var instancia: ?Logger = null;
var mutex = std.Thread.Mutex{};
pub fn obterInstancia() *Logger {
mutex.lock();
defer mutex.unlock();
if (instancia == null) {
instancia = Logger{
.nivel = .info,
.arquivo = null,
};
}
return &instancia.?;
}
pub fn log(self: *Logger, nivel: NivelLog, mensagem: []const u8) void {
if (@intFromEnum(nivel) < @intFromEnum(self.nivel)) return;
std.debug.print("[{s}] {s}\n", .{ @tagName(nivel), mensagem });
}
};
pub fn main() void {
const logger = Logger.obterInstancia();
logger.log(.info, "Aplicação iniciada");
logger.log(.warn, "Recurso quase esgotado");
logger.log(.debug, "Esta mensagem não aparece (nível < info)");
// Em qualquer parte do código, mesma instância
const mesmo_logger = Logger.obterInstancia();
mesmo_logger.log(.err, "Erro crítico!");
}
Singleton com std.once (thread-safe garantido)
const std = @import("std");
const Config = struct {
porta: u16,
host: []const u8,
max_conexoes: u32,
var instancia: Config = undefined;
var once = std.once(inicializar);
fn inicializar() void {
instancia = Config{
.porta = 8080,
.host = "localhost",
.max_conexoes = 100,
};
}
pub fn obter() *Config {
once.call(); // executado apenas uma vez, thread-safe
return &instancia;
}
};
pub fn main() void {
const config = Config.obter();
std.debug.print("Servidor: {s}:{d}\n", .{ config.host, config.porta });
// Sempre retorna a mesma instância
const config2 = Config.obter();
std.debug.print("Max conexões: {d}\n", .{config2.max_conexoes});
}
Singleton Genérico com comptime
fn Singleton(comptime T: type, comptime initFn: fn () T) type {
return struct {
var instancia: ?T = null;
var mutex = std.Thread.Mutex{};
pub fn obter() *T {
mutex.lock();
defer mutex.unlock();
if (instancia == null) {
instancia = initFn();
}
return &instancia.?;
}
pub fn resetar() void {
mutex.lock();
defer mutex.unlock();
instancia = null;
}
};
}
// Uso:
const MeuSingleton = Singleton(MinhaStruct, MinhaStruct.criar);
const inst = MeuSingleton.obter();
Quando Evitar
- Testes unitários: Singletons criam estado global que dificulta testes isolados. Prefira Dependency Injection quando testabilidade é prioridade.
- Acoplamento excessivo: Se muitas partes do código dependem do singleton, considere passar a dependência explicitamente.
- Concorrência pesada: Para dados compartilhados entre muitas threads, considere um Pool de Objetos ou dados por thread.
Alternativa Idiomática em Zig
Em Zig, muitas vezes é preferível simplesmente passar a dependência como parâmetro, seguindo a filosofia de tornar tudo explícito:
const Logger = struct {
nivel: NivelLog,
// ...
};
fn processarRequisicao(logger: *Logger, dados: []const u8) !void {
logger.log(.info, "Processando requisição");
// ...
}
Considerações de Performance
std.onceé a forma mais eficiente: internamente,std.onceusa uma operação atômica para verificar se a inicialização já ocorreu. Após a primeira chamada,once.call()é apenas uma leitura atômica com memória orderingacquire— custo mínimo, sem lock.- Mutex na versão manual: a implementação com
mutex.lock()emobterInstanciatoma um lock em toda chamada, mesmo após a inicialização. Para alto volume de acessos concorrentes, isso cria contention. Prefirastd.onceque evita o lock após a primeira inicialização. - Singleton em sistemas embarcados: variáveis globais em Zig vão para o segmento
.dataou.bss. Em microcontroladores com RAM limitada, considere singletons com estado mínimo e inicialização viacomptimequando possível.
Singleton com Comptime para Configuração Estática
Quando a configuração é conhecida em tempo de compilação, você pode ter um “singleton” sem custo de runtime algum:
// config.zig
pub const Config = struct {
porta: u16,
max_conexoes: u32,
debug: bool,
};
// Singleton comptime — zero custo em runtime
pub const config: Config = blk: {
// Em builds de debug, usa configuração de desenvolvimento
if (@import("builtin").mode == .Debug) {
break :blk .{
.porta = 8080,
.max_conexoes = 10,
.debug = true,
};
} else {
break :blk .{
.porta = 443,
.max_conexoes = 1000,
.debug = false,
};
}
};
// Uso em qualquer lugar:
// const cfg = @import("config.zig").config;
// std.debug.print("Porta: {d}\n", .{cfg.porta});
Erros Comuns
Singleton que armazena alocações sem gerenciar lifetime: se o singleton usa um ArrayList internamente, alguém precisa chamar deinit em algum momento. Em programas de vida longa, um singleton que nunca libera memória é um vazamento. Registre um handler de deinit no defer de main.
Testes que deixam o singleton em estado sujo: testes que modificam o estado do singleton afetam os testes seguintes. Implemente um método resetar (como no exemplo genérico) e chame-o no teardown de cada teste.
Race condition na inicialização manual: o padrão if (instancia == null) { instancia = ... } sem mutex pode ser executado simultaneamente por duas threads, criando duas instâncias. Sempre use mutex.lock() antes de verificar null, ou use std.once.
Perguntas Frequentes
Singletons são ruins para testes? Sim, quando contêm estado mutável. Um singleton de configuração imutável é inofensivo para testes. Um singleton de logger que acumula mensagens entre testes é problemático. A alternativa é usar Dependency Injection — passe a instância como parâmetro em vez de acessá-la globalmente.
Posso ter um singleton por thread (thread-local)?
Sim. Use threadlocal var instancia: ?MeuTipo = null;. Cada thread terá sua própria instância, sem necessidade de mutex. Útil para buffers temporários e geradores de números aleatórios.
Qual é a diferença entre std.once e um mutex simples para singleton?
std.once garante que a função de inicialização seja executada exatamente uma vez, mesmo com múltiplas threads competindo. Após a inicialização, once.call() é uma leitura atômica barata. Um mutex simples toma o lock em cada chamada a obterInstancia, mesmo após a inicialização — menos eficiente para casos de alta frequência.
Veja Também
- Dependency Injection — Alternativa ao Singleton para testabilidade
- Factory — Criação controlada de objetos
- Concorrência — Thread safety em Zig
- FAQ Produção — Padrões para código em produção