Introducao
O Singleton garante que uma classe (ou, em Rust, um tipo) tenha apenas uma instancia durante toda a vida do programa, fornecendo um ponto de acesso global a essa instancia.
Em linguagens como Java e C#, o Singleton e trivial: uma variavel estatica privada com um getter. Em Rust, porem, o Singleton e um dos padroes mais debatidos e complexos de implementar. Isso acontece porque o sistema de ownership de Rust trata estado global mutavel com extrema cautela, exigindo sincronizacao explicita para garantir seguranca entre threads.
Problema
Voce esta construindo uma aplicacao que precisa de:
- Uma configuracao global carregada uma unica vez no inicio do programa
- Um logger acessivel de qualquer lugar do codigo
- Um pool de conexoes compartilhado entre todas as threads
Sem Singleton, voce precisaria passar essas dependencias manualmente por toda a pilha de chamadas, o que pode ser impraticavel em projetos grandes.
// Sem Singleton: precisa passar config para TODAS as funcoes
fn processar_pedido(config: &Config, pedido: Pedido) -> Result<(), Erro> {
let banco = conectar_banco(&config.banco_url)?;
enviar_email(&config.smtp, pedido.email)?;
registrar_log(&config.log, "Pedido processado")?;
Ok(())
}
// E cada funcao chamada tambem precisa receber config...
// Isso vira "parameter drilling" em aplicacoes grandes
Solucao em Rust
1. OnceLock: Singleton Imutavel (Estavel desde Rust 1.70)
Para dados que sao escritos uma vez e lidos muitas vezes:
use std::sync::OnceLock;
use std::collections::HashMap;
/// Configuracao global da aplicacao
#[derive(Debug)]
pub struct Config {
pub banco_url: String,
pub porta_servidor: u16,
pub modo_debug: bool,
pub chaves_api: HashMap<String, String>,
}
/// Armazenamento global da configuracao
static CONFIG: OnceLock<Config> = OnceLock::new();
impl Config {
/// Inicializa a configuracao global (chame apenas uma vez)
pub fn inicializar(config: Config) -> Result<(), Config> {
CONFIG.set(config)
}
/// Obtem a referencia para a configuracao global
/// Retorna None se ainda nao foi inicializada
pub fn global() -> &'static Config {
CONFIG
.get()
.expect("Config::inicializar() deve ser chamado antes de Config::global()")
}
}
fn main() {
// Inicializa uma unica vez no inicio do programa
let config = Config {
banco_url: "postgres://localhost/meu_app".to_string(),
porta_servidor: 8080,
modo_debug: true,
chaves_api: {
let mut m = HashMap::new();
m.insert("github".to_string(), "ghp_abc123".to_string());
m.insert("stripe".to_string(), "sk_test_xyz".to_string());
m
},
};
Config::inicializar(config).expect("Config ja foi inicializada");
// Agora pode acessar de qualquer lugar
println!("Porta: {}", Config::global().porta_servidor);
println!("Debug: {}", Config::global().modo_debug);
// Funciona em qualquer thread
let handle = std::thread::spawn(|| {
println!(
"[thread] Banco URL: {}",
Config::global().banco_url
);
});
handle.join().unwrap();
}
2. LazyLock: Inicializacao Preguicosa (Estavel desde Rust 1.80)
Para quando a inicializacao depende de computacao que so pode acontecer em tempo de execucao:
use std::sync::LazyLock;
use std::collections::HashMap;
/// Logger global inicializado preguicosamente
#[derive(Debug)]
pub struct Logger {
nivel: NivelLog,
destinos: Vec<DestinoLog>,
}
#[derive(Debug, Clone, Copy)]
pub enum NivelLog {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Debug)]
pub enum DestinoLog {
Console,
Arquivo(String),
}
impl Logger {
fn novo_padrao() -> Self {
println!("Inicializando logger (isso so acontece uma vez)...");
Self {
nivel: NivelLog::Info,
destinos: vec![DestinoLog::Console],
}
}
pub fn log(&self, nivel: NivelLog, mensagem: &str) {
// Simplificacao: verificar se o nivel esta habilitado
let nivel_num = |n: &NivelLog| match n {
NivelLog::Trace => 0,
NivelLog::Debug => 1,
NivelLog::Info => 2,
NivelLog::Warn => 3,
NivelLog::Error => 4,
};
if nivel_num(&nivel) >= nivel_num(&self.nivel) {
for destino in &self.destinos {
match destino {
DestinoLog::Console => {
println!("[{:?}] {}", nivel, mensagem);
}
DestinoLog::Arquivo(caminho) => {
// Em producao, escreveria no arquivo
println!("[{:?}] -> {} : {}", nivel, caminho, mensagem);
}
}
}
}
}
pub fn info(&self, mensagem: &str) {
self.log(NivelLog::Info, mensagem);
}
pub fn error(&self, mensagem: &str) {
self.log(NivelLog::Error, mensagem);
}
pub fn debug(&self, mensagem: &str) {
self.log(NivelLog::Debug, mensagem);
}
}
/// Logger global: inicializado na primeira vez que for acessado
static LOGGER: LazyLock<Logger> = LazyLock::new(|| Logger::novo_padrao());
/// Funcoes de conveniencia para acesso global
pub fn log_info(mensagem: &str) {
LOGGER.info(mensagem);
}
pub fn log_error(mensagem: &str) {
LOGGER.error(mensagem);
}
fn main() {
// Primeiro acesso: inicializa o logger
log_info("Aplicacao iniciada");
log_info("Carregando configuracoes...");
log_error("Exemplo de erro para teste");
// Segundo acesso: reutiliza a mesma instancia
log_info("Logger ja estava inicializado - sem custo extra");
}
3. Mutex + OnceLock: Singleton Mutavel
Para quando voce precisa modificar o estado do singleton apos a inicializacao:
use std::sync::{Mutex, OnceLock};
use std::collections::HashMap;
/// Cache global mutavel thread-safe
#[derive(Debug)]
pub struct CacheGlobal {
dados: HashMap<String, String>,
hits: u64,
misses: u64,
}
static CACHE: OnceLock<Mutex<CacheGlobal>> = OnceLock::new();
impl CacheGlobal {
/// Obtem acesso ao cache global
fn instancia() -> &'static Mutex<CacheGlobal> {
CACHE.get_or_init(|| {
Mutex::new(CacheGlobal {
dados: HashMap::new(),
hits: 0,
misses: 0,
})
})
}
/// Obtem um valor do cache
pub fn obter(chave: &str) -> Option<String> {
let mut cache = Self::instancia().lock().unwrap();
match cache.dados.get(chave) {
Some(valor) => {
cache.hits += 1;
Some(valor.clone())
}
None => {
cache.misses += 1;
None
}
}
}
/// Insere um valor no cache
pub fn inserir(chave: String, valor: String) {
let mut cache = Self::instancia().lock().unwrap();
cache.dados.insert(chave, valor);
}
/// Retorna estatisticas do cache
pub fn estatisticas() -> (u64, u64) {
let cache = Self::instancia().lock().unwrap();
(cache.hits, cache.misses)
}
}
fn main() {
// Insere alguns valores
CacheGlobal::inserir("usuario:1".to_string(), "Maria".to_string());
CacheGlobal::inserir("usuario:2".to_string(), "Joao".to_string());
// Busca valores (de varias threads)
let handles: Vec<_> = (0..5)
.map(|i| {
std::thread::spawn(move || {
let chave = format!("usuario:{}", i % 3);
match CacheGlobal::obter(&chave) {
Some(nome) => println!("[Thread {}] Encontrou: {} = {}", i, chave, nome),
None => println!("[Thread {}] Nao encontrou: {}", i, chave),
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let (hits, misses) = CacheGlobal::estatisticas();
println!("\nEstatisticas: {} hits, {} misses", hits, misses);
}
Diagrama
SINGLETON COM OnceLock (imutavel):
Thread 1 Thread 2 Thread 3
| | |
v v v
Config::global() Config::global() Config::global()
| | |
+-------+-------+-------+-------+
|
v
+---------------------+
| static CONFIG: |
| OnceLock<Config> |
| |
| (inicializado uma |
| unica vez, |
| leitura sem lock) |
+---------------------+
SINGLETON COM Mutex (mutavel):
Thread 1 Thread 2 Thread 3
| | |
v v v
lock() lock() lock()
| (espera) (espera)
v | |
+---------------------+ |
| Mutex<CacheGlobal> | |
| (acesso exclusivo) | |
+---------------------+ |
| |
unlock() |
v v
lock() (espera)
|
v
+---------------------+
| Mutex<CacheGlobal> |
+---------------------+
Exemplo do Mundo Real
Um gerenciador de configuracao completo que carrega de arquivo e permite recarregamento:
use std::sync::{Arc, OnceLock, RwLock};
use std::collections::HashMap;
use std::path::PathBuf;
/// Configuracao da aplicacao carregada de arquivo
#[derive(Debug, Clone)]
pub struct AppConfig {
pub nome_app: String,
pub versao: String,
pub banco: BancoConfig,
pub servidor: ServidorConfig,
pub variaveis: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct BancoConfig {
pub url: String,
pub pool_max: u32,
pub timeout_ms: u64,
}
#[derive(Debug, Clone)]
pub struct ServidorConfig {
pub host: String,
pub porta: u16,
pub workers: usize,
}
/// Gerenciador de configuracao global
/// Usa RwLock para permitir leitura concorrente e escrita exclusiva
pub struct ConfigManager {
config: RwLock<AppConfig>,
caminho_arquivo: PathBuf,
}
static MANAGER: OnceLock<ConfigManager> = OnceLock::new();
impl ConfigManager {
/// Inicializa o gerenciador com o caminho do arquivo de configuracao
pub fn inicializar(caminho: impl Into<PathBuf>) -> Result<(), String> {
let caminho = caminho.into();
let config = Self::carregar_arquivo(&caminho)?;
MANAGER
.set(ConfigManager {
config: RwLock::new(config),
caminho_arquivo: caminho,
})
.map_err(|_| "ConfigManager ja foi inicializado".to_string())
}
/// Obtem a instancia global do gerenciador
pub fn global() -> &'static ConfigManager {
MANAGER
.get()
.expect("ConfigManager::inicializar() nao foi chamado")
}
/// Le a configuracao atual (leitura concorrente permitida)
pub fn config(&self) -> AppConfig {
self.config.read().unwrap().clone()
}
/// Recarrega a configuracao do arquivo
pub fn recarregar(&self) -> Result<(), String> {
let nova_config = Self::carregar_arquivo(&self.caminho_arquivo)?;
let mut config = self.config.write().unwrap();
*config = nova_config;
println!("Configuracao recarregada com sucesso!");
Ok(())
}
/// Simula carregamento de arquivo de configuracao
fn carregar_arquivo(caminho: &PathBuf) -> Result<AppConfig, String> {
println!("Carregando configuracao de: {:?}", caminho);
// Em producao, voce leria o arquivo e faria parse
// Aqui simulamos valores padrao
Ok(AppConfig {
nome_app: "MeuApp".to_string(),
versao: "1.0.0".to_string(),
banco: BancoConfig {
url: "postgres://localhost/meu_app".to_string(),
pool_max: 10,
timeout_ms: 5000,
},
servidor: ServidorConfig {
host: "0.0.0.0".to_string(),
porta: 8080,
workers: 4,
},
variaveis: HashMap::from([
("AMBIENTE".to_string(), "desenvolvimento".to_string()),
("LOG_LEVEL".to_string(), "info".to_string()),
]),
})
}
}
fn main() {
// Inicializa no inicio do programa
ConfigManager::inicializar("config.toml")
.expect("Falha ao inicializar ConfigManager");
// Acessa a configuracao de qualquer lugar
let config = ConfigManager::global().config();
println!("App: {} v{}", config.nome_app, config.versao);
println!("Servidor: {}:{}", config.servidor.host, config.servidor.porta);
println!("Banco: {}", config.banco.url);
// Varias threads podem ler simultaneamente
let mut handles = vec![];
for i in 0..3 {
handles.push(std::thread::spawn(move || {
let config = ConfigManager::global().config();
println!(
"[Thread {}] Porta do servidor: {}",
i, config.servidor.porta
);
}));
}
for h in handles {
h.join().unwrap();
}
// Recarrega configuracao (por exemplo, apos editar o arquivo)
ConfigManager::global()
.recarregar()
.expect("Falha ao recarregar");
}
Quando Usar
- Configuracoes de aplicacao que sao lidas por todo o sistema
- Pools de conexao (banco de dados, HTTP clients)
- Loggers e sistemas de telemetria
- Caches compartilhados entre threads
- Registros de fabricas, plugins ou servicos
Quando NAO Usar
- Testes unitarios - Singletons tornam testes dificeis de isolar. Prefira injecao de dependencia
- Quando a passagem de parametros e viavel - Passar
&Confige mais limpo e testavel - Estado mutavel sem necessidade real - Use
constou funcoes puras quando possivel - Bibliotecas - Singletons em bibliotecas sao geralmente um anti-padrao; deixe o usuario da lib decidir
// MELHOR: injecao de dependencia (mais testavel)
pub struct Servico {
config: Arc<Config>,
}
impl Servico {
pub fn new(config: Arc<Config>) -> Self {
Self { config }
}
}
// PIOR: dependencia oculta em Singleton global
pub struct Servico;
impl Servico {
pub fn fazer_algo(&self) {
let config = Config::global(); // dependencia oculta!
}
}
Variacoes em Rust
1. Com once_cell (crate externa, API original)
// Cargo.toml: once_cell = "1"
use once_cell::sync::Lazy;
static INSTANCIA: Lazy<MinhaStruct> = Lazy::new(|| {
MinhaStruct::new()
});
2. Com arc-swap para atualizacao sem lock
// Para cenarios de leitura intensiva onde RwLock e gargalo
// Cargo.toml: arc-swap = "1"
use std::sync::Arc;
// arc_swap::ArcSwap permite trocar o valor atomicamente
// Leitores nunca bloqueiam, escritores substituem o Arc inteiro
3. Singleton por thread com thread_local!
use std::cell::RefCell;
thread_local! {
static BUFFER_LOCAL: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(1024));
}
fn usar_buffer() {
BUFFER_LOCAL.with(|buf| {
let mut buf = buf.borrow_mut();
buf.clear();
buf.extend_from_slice(b"dados locais da thread");
println!("Buffer: {} bytes", buf.len());
});
}
Padroes Relacionados
- Builder - Frequentemente usado para construir a instancia do Singleton
- Factory - Factory pode retornar sempre a mesma instancia (tornando-se Singleton)
- Facade - Singleton muitas vezes implementa uma Facade para subsistemas complexos
Conclusao
O Singleton em Rust e deliberadamente mais dificil de implementar do que em outras
linguagens, e isso e uma feature, nao um bug. O sistema de ownership de Rust forca
voce a pensar sobre seguranca de threads, tempo de vida de dados e mutabilidade compartilhada.
As primitivas OnceLock e LazyLock da biblioteca padrao oferecem solucoes seguras e
ergonomicas para os casos mais comuns. Quando precisar de mutabilidade, Mutex ou RwLock
tornam o custo da sincronizacao explicito. Sempre que possivel, porem, prefira injecao de
dependencia ao Singleton: seu codigo sera mais testavel, mais flexivel e mais facil
de manter.