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.

Gerenciando Transações em Go
Gerenciando transações em Go

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

  1. Segurança dos Dados: Garante que transações sejam revertidas em caso de erro ou falha inesperada.
  2. Redução de Código Repetitivo: Centraliza o ciclo de vida da transação.
  3. Tratamento Robusto de Erros: Utiliza defer para tratar erros e panics de forma consistente.
  4. Reutilização: Facilita a manutenção e o uso da lógica em múltiplas partes do sistema.
  5. 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.

Mantido por itexto Consultoria