Singleton em Rust

O padrao Singleton em Rust: por que estado global mutavel e dificil, OnceLock, LazyLock, static com Mutex e exemplos praticos de configuracao e logger.

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 &Config e mais limpo e testavel
  • Estado mutavel sem necessidade real - Use const ou 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.