Gerenciando Transações em Go
Neste artigo, exploraremos em detalhes de uma forma de gerenciar transações em go usando uma função callback que gerencia as operações de commit e rollback.
Introdução
Neste artigo, exploraremos em detalhes de uma forma de gerenciar transações em go usando uma função callback que gerencia as operações de commit e rollback.
Entendendo o Papel das Transações
As transações são fundamentais para garantir a consistência dos dados em sistemas que interagem com bancos de dados. Uma transação é uma unidade de trabalho que deve ser executada completamente ou revertida em caso de falha. A API database/sql
do Go fornece suporte nativo para trabalhar com transações, incluindo métodos para begin
, commit
e rollback
.
Encapsular lógica da transação
Podemos encapsular lógica de transação em uma função que realiza a transação realizando o commit ou o rollback automaticamente, segue o exemplo da função:
func WithTransaction(ctx context.Context, fn func(*sql.Tx, *sql.DB) error) (err error) {
db, err := AbrirConexaoComBancoDeDados()
if err != nil {
return fmt.Errorf("erro ao abrir conexão com banco: %v", err)
}
tx, err := IniciarTransacao(ctx, db)
if err != nil {
return fmt.Errorf("erro ao iniciar transação: %v", err)
}
// Garante que a transação seja finalizada
defer func() {
if p := recover(); p != nil {
if rbErr := tx.Rollback(); rbErr != nil {
log.Printf("erro ao fazer rollback após erro: %v", rbErr)
}
panic(p)
} else if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
log.Printf("erro ao fazer rollback: %v", rbErr)
}
} else {
if cmErr := tx.Commit(); cmErr != nil {
err = fmt.Errorf("erro ao fazer commit da transação: %w", cmErr)
}
}
}()
err = fn(tx, db)
return err
}
A função WithTransaction
é projetada para encapsular o ciclo de vida completo de uma transação, gerenciando conexões, erros e panics. Aqui está uma visão detalhada de como ela funciona:
Passo a passo para entender o código
1. Abertura de Conexão
A função utiliza AbrirConexaoComBancoDeDados
para garantir que a conexão seja aberta apenas uma vez (usando sync.Once
) e configurada adequadamente.
db, err := AbrirConexaoComBancoDeDados()
if err != nil {
return fmt.Errorf("erro ao abrir conexão com banco: %v", err)
}
2. Início da Transação
A transação é iniciada com db.BeginTx
e vinculada ao contexto ctx
, permitindo maior controle sobre o tempo de execução.
tx, err := IniciarTransacao(ctx, db)
if err != nil {
return fmt.Errorf("erro ao iniciar transação: %v", err)
}
3. Tratamento de Erros e Panics com defer
O bloco defer
garante que, independentemente do sucesso ou falha, a transação seja encerrada corretamente:
- Panic: Um
rollback
é realizado e o erro é propagado novamente. - Erro: O erro é tratado e a transação é revertida.
- Sucesso: A transação é confirmada com
commit
.
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
} else if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
4. Execução da Lógica de Negócio
A lógica de negócios fornecida pelo usuário é executada dentro do contexto da transação. Caso um erro seja retornado, a transação será automaticamente revertida.
err = fn(tx, db)
return err
Vantagens da Abordagem WithTransaction
- Segurança dos Dados: Garante que transações sejam revertidas em caso de erro ou falha inesperada.
- Redução de Código Repetitivo: Centraliza o ciclo de vida da transação.
- Tratamento Robusto de Erros: Utiliza
defer
para tratar erros e panics de forma consistente. - Reutilização: Facilita a manutenção e o uso da lógica em múltiplas partes do sistema.
- Melhor Gerenciamento de Conexões: Evita conexões desnecessárias com o banco.
Exemplo de Uso Prático
Imagine um sistema bancário onde é necessário transferir dinheiro entre contas. Aqui está um exemplo de uso:
err := WithTransaction(ctx, func(tx *sql.Tx, db *sql.DB) error {
// Atualiza o saldo da conta de origem
_, err := tx.Exec("UPDATE contas SET saldo = saldo - 100 WHERE id = $1", contaOrigemID)
if err != nil {
return err
}
// Atualiza o saldo da conta de destino
_, err = tx.Exec("UPDATE contas SET saldo = saldo + 100 WHERE id = $1", contaDestinoID)
if err != nil {
return err
}
return nil
})
if err != nil {
log.Printf("Erro na transação: %v", err)
}
Conclusão
A função WithTransaction
é uma abordagem robusta para gerenciar transações em Go. Ela simplifica o código, melhora a segurança e assegura a consistência dos dados, sendo especialmente útil em sistemas complexos. Adotar padrões como este é essencial para construir aplicações escaláveis, confiáveis e fáceis de manter.