Banco de dados com database/sql

1. Introdução ao pacote database/sql

O pacote database/sql é a abstração oficial da linguagem Go para interagir com bancos de dados relacionais. Ele fornece uma interface genérica que permite trabalhar com diferentes SGBDs (PostgreSQL, MySQL, SQLite, etc.) sem alterar a lógica da aplicação — apenas trocando o driver importado.

É fundamental entender que database/sql não é um driver de banco de dados. Ele define interfaces e funções comuns, enquanto os drivers específicos (como lib/pq para PostgreSQL ou go-sqlite3 para SQLite) implementam a comunicação real com cada SGBD.

O fluxo básico de trabalho com database/sql segue três etapas principais:
1. Conexão: abrir e configurar o pool de conexões
2. Execução: enviar queries e comandos
3. Resultados: processar linhas retornadas ou verificar alterações

2. Configuração e abertura de conexão

Para começar, importamos o driver específico (com o underscore para ativar seu registro automático) e o pacote database/sql:

import (
    "database/sql"
    _ "github.com/lib/pq" // driver PostgreSQL
)

A função sql.Open() não estabelece uma conexão imediata — ela apenas cria um pool gerenciado. A conexão real ocorre quando necessário (primeira query ou Ping()):

db, err := sql.Open("postgres", "host=localhost port=5432 user=admin dbname=meubanco sslmode=disable")
if err != nil {
    log.Fatal("Erro ao abrir pool:", err)
}
defer db.Close()

O pool pode ser configurado para otimizar desempenho:

db.SetMaxOpenConns(25)          // máximo de conexões abertas simultâneas
db.SetMaxIdleConns(5)           // máximo de conexões ociosas no pool
db.SetConnMaxLifetime(5 * time.Minute) // tempo máximo de vida de uma conexão

Para verificar se o banco está acessível:

if err := db.Ping(); err != nil {
    log.Fatal("Banco indisponível:", err)
}
fmt.Println("Conectado com sucesso!")

3. Execução de consultas: Query e QueryRow

Múltiplas linhas com Query()

Para consultas que retornam várias linhas, usamos db.Query() e iteramos com Rows.Next():

rows, err := db.Query("SELECT id, nome, email FROM usuarios WHERE ativo = $1", true)
if err != nil {
    log.Fatal(err)
}
defer rows.Close() // essencial para liberar recursos

for rows.Next() {
    var id int
    var nome, email string
    if err := rows.Scan(&id, &nome, &email); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%d: %s (%s)\n", id, nome, email)
}

// Verificar erro após iteração
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

Linha única com QueryRow()

Quando esperamos apenas um resultado, QueryRow() é mais direto:

var nome, email string
err := db.QueryRow("SELECT nome, email FROM usuarios WHERE id = $1", 42).Scan(&nome, &email)
if err == sql.ErrNoRows {
    fmt.Println("Usuário não encontrado")
} else if err != nil {
    log.Fatal(err)
}
fmt.Printf("Nome: %s, Email: %s\n", nome, email)

4. Execução de comandos: Exec

Para INSERT, UPDATE, DELETE (operações que não retornam linhas), usamos db.Exec():

result, err := db.Exec("INSERT INTO usuarios (nome, email) VALUES ($1, $2)", "João Silva", "joao@email.com")
if err != nil {
    log.Fatal(err)
}

linhasAfetadas, _ := result.RowsAffected()
fmt.Printf("%d linha(s) inserida(s)\n", linhasAfetadas)

// LastInsertId() depende do driver
if id, err := result.LastInsertId(); err == nil {
    fmt.Printf("ID gerado: %d\n", id)
}

Atenção: LastInsertId() não é suportado por todos os drivers (ex.: PostgreSQL) — nesses casos, use RETURNING com QueryRow().

5. Parâmetros e prepared statements

O uso de placeholders previne SQL injection automaticamente. Cada driver usa seu próprio estilo:
- PostgreSQL: $1, $2, ...
- MySQL: ?
- SQLite: ? ou $1

Prepared statements trazem benefícios de performance (query compilada uma vez) e segurança:

stmt, err := db.Prepare("INSERT INTO produtos (nome, preco, categoria) VALUES ($1, $2, $3)")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

// Reutilizar o statement
produtos := []struct{ nome string; preco float64; categoria string }{
    {"Teclado", 89.90, "Periféricos"},
    {"Mouse", 45.50, "Periféricos"},
    {"Monitor", 1299.00, "Monitores"},
}

for _, p := range produtos {
    if _, err := stmt.Exec(p.nome, p.preco, p.categoria); err != nil {
        log.Fatal(err)
    }
}

6. Transações com Begin e Tx

Transações garantem atomicidade: todas as operações são confirmadas ou revertidas juntas:

tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback() // seguro: se houver panic ou erro, faz rollback

// Operações dentro da transação
_, err = tx.Exec("UPDATE contas SET saldo = saldo - $1 WHERE id = $2", 100.00, 1)
if err != nil {
    return // defer tx.Rollback() será executado
}

_, err = tx.Exec("UPDATE contas SET saldo = saldo + $1 WHERE id = $2", 100.00, 2)
if err != nil {
    return // rollback automático
}

// Confirmar tudo
if err := tx.Commit(); err != nil {
    log.Fatal("Erro ao commitar:", err)
}

O padrão defer tx.Rollback() combinado com tx.Commit() explícito é seguro: se Commit() for chamado com sucesso, o Rollback() posterior não tem efeito.

7. Tratamento de erros e boas práticas

Erros comuns e como tratá-los:

// sql.ErrNoRows: nenhum resultado encontrado
err := db.QueryRow("SELECT * FROM usuarios WHERE id = $1", 999).Scan(...)
if errors.Is(err, sql.ErrNoRows) {
    fmt.Println("Registro não existe")
}

// sql.ErrConnDone: operação em conexão já fechada
// sql.ErrTxDone: operação em transação já finalizada

Boas práticas essenciais:

// Sempre fechar recursos
defer rows.Close()
defer stmt.Close()
defer db.Close()

// Timeout para operações longas
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

row := db.QueryRowContext(ctx, "SELECT ...")

8. Exemplo completo: CRUD básico

Estrutura e funções CRUD com PostgreSQL:

type Usuario struct {
    ID    int
    Nome  string
    Email string
}

func CriarUsuario(db *sql.DB, u *Usuario) error {
    return db.QueryRow(
        "INSERT INTO usuarios (nome, email) VALUES ($1, $2) RETURNING id",
        u.Nome, u.Email,
    ).Scan(&u.ID)
}

func ListarUsuarios(db *sql.DB) ([]Usuario, error) {
    rows, err := db.Query("SELECT id, nome, email FROM usuarios ORDER BY id")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var usuarios []Usuario
    for rows.Next() {
        var u Usuario
        if err := rows.Scan(&u.ID, &u.Nome, &u.Email); err != nil {
            return nil, err
        }
        usuarios = append(usuarios, u)
    }
    return usuarios, rows.Err()
}

func AtualizarUsuario(db *sql.DB, u *Usuario) error {
    _, err := db.Exec(
        "UPDATE usuarios SET nome = $1, email = $2 WHERE id = $3",
        u.Nome, u.Email, u.ID,
    )
    return err
}

func DeletarUsuario(db *sql.DB, id int) error {
    _, err := db.Exec("DELETE FROM usuarios WHERE id = $1", id)
    return err
}

// Exemplo com transação
func TransferirSaldo(db *sql.DB, deID, paraID int, valor float64) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback()

    _, err = tx.Exec("UPDATE contas SET saldo = saldo - $1 WHERE id = $2", valor, deID)
    if err != nil {
        return err
    }

    _, err = tx.Exec("UPDATE contas SET saldo = saldo + $1 WHERE id = $2", valor, paraID)
    if err != nil {
        return err
    }

    return tx.Commit()
}

O pacote database/sql oferece uma base sólida e portável para trabalhar com bancos de dados relacionais em Go. Dominar seus padrões — prepared statements, transações e tratamento correto de erros — é essencial para construir aplicações robustas e seguras.

Referências