Golang

Artigo 32 — Introdução ao database/sql e drivers em Go Já leu

10 min de leitura

Artigo 32 — Introdução ao database/sql e drivers em Go
Artigo 32 — Introdução ao database/sql e drivers em Go A camada de abstração que unifica todos os bancos Go oferece o pacote como interface universal para

Artigo 32 — Introdução ao database/sql e drivers em Go

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 6 — Banco de Dados


A camada de abstração que unifica todos os bancos

Go oferece o pacote database/sql como interface universal para bancos de dados relacionais. Ele define um conjunto de tipos e métodos que funcionam da mesma forma independentemente do banco usado — MySQL, PostgreSQL, SQLite ou qualquer outro que tenha um driver compatível.

Essa separação entre interface e implementação é elegante: o código da aplicação interage apenas com database/sql, enquanto o driver específico do banco fica isolado em uma dependência registrada via init(). Trocar de banco de dados, em teoria, exige mudar apenas o driver e o DSN de conexão.


Arquitetura: interface e drivers

Aplicação Go
     │
     ▼
database/sql  ← interface unificada (biblioteca padrão)
     │
     ▼
Driver         ← implementação específica (dependência externa)
     │
     ▼
Banco de Dados ← MySQL, PostgreSQL, SQLite, etc.

Os drivers mais usados:

Banco Driver Módulo
MySQL / MariaDB go-sql-driver/mysql github.com/go-sql-driver/mysql
PostgreSQL lib/pq github.com/lib/pq
PostgreSQL pgx github.com/jackc/pgx/v5/stdlib
SQLite mattn/go-sqlite3 github.com/mattn/go-sqlite3
SQLite (puro Go) modernc/sqlite modernc.org/sqlite

Registrando um driver

Drivers se registram automaticamente através de seu init() quando importados com _:

package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/go-sql-driver/mysql"  // registra o driver MySQL
)

func main() {
    // DSN: usuario:senha@protocolo(host:porta)/banco?parametros
    dsn := "root:senha@tcp(localhost:3306)/minha_aplicacao?parseTime=true&charset=utf8mb4"

    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal("Erro ao abrir conexão:", err)
    }
    defer db.Close()

    // sql.Open NÃO estabelece conexão — apenas valida o DSN
    // Ping verifica a conexão real
    if err := db.Ping(); err != nil {
        log.Fatal("Banco inacessível:", err)
    }

    fmt.Println("Conexão estabelecida com sucesso")
}

O pool de conexões

sql.DB não é uma única conexão — é um pool de conexões gerenciado automaticamente. Configurar o pool corretamente é essencial para performance e estabilidade:

func configurarPool(db *sql.DB) {
    // Máximo de conexões abertas simultâneas
    // Padrão: 0 (ilimitado) — sempre configure explicitamente
    db.SetMaxOpenConns(25)

    // Máximo de conexões ociosas no pool
    // Padrão: 2 — aumentar reduz overhead de criação de conexões
    db.SetMaxIdleConns(10)

    // Tempo máximo de vida de uma conexão
    // Útil quando o banco tem timeout de conexão ociosa
    db.SetConnMaxLifetime(5 * time.Minute)

    // Tempo máximo que uma conexão ociosa pode ficar no pool
    db.SetConnMaxIdleTime(2 * time.Minute)
}

Valores recomendados dependem da carga, mas um ponto de partida razoável para um serviço web:

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(2 * time.Minute)

Inicialização completa e verificada

Uma função de inicialização robusta para qualquer banco:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

type ConfigDB struct {
    Driver          string
    DSN             string
    MaxOpenConns    int
    MaxIdleConns    int
    ConnMaxLifetime time.Duration
    ConnMaxIdleTime time.Duration
}

func abrirBancoDados(config ConfigDB) (*sql.DB, error) {
    db, err := sql.Open(config.Driver, config.DSN)
    if err != nil {
        return nil, fmt.Errorf("abrir banco: %w", err)
    }

    db.SetMaxOpenConns(config.MaxOpenConns)
    db.SetMaxIdleConns(config.MaxIdleConns)
    db.SetConnMaxLifetime(config.ConnMaxLifetime)
    db.SetConnMaxIdleTime(config.ConnMaxIdleTime)

    // Verifica conectividade com timeout
    ctx, cancelar := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancelar()

    if err := db.PingContext(ctx); err != nil {
        db.Close()
        return nil, fmt.Errorf("verificar conexão: %w", err)
    }

    return db, nil
}

func main() {
    config := ConfigDB{
        Driver:          "mysql",
        DSN:             "root:senha@tcp(localhost:3306)/app?parseTime=true",
        MaxOpenConns:    25,
        MaxIdleConns:    10,
        ConnMaxLifetime: 5 * time.Minute,
        ConnMaxIdleTime: 2 * time.Minute,
    }

    db, err := abrirBancoDados(config)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    defer db.Close()

    fmt.Println("Banco de dados pronto")
}

QueryRow: buscando um único registro

type Usuario struct {
    ID        int
    Nome      string
    Email     string
    CriadoEm time.Time
}

func buscarUsuarioPorID(ctx context.Context, db *sql.DB, id int) (*Usuario, error) {
    query := `
        SELECT id, nome, email, criado_em
        FROM usuarios
        WHERE id = ?
    `

    var u Usuario
    err := db.QueryRowContext(ctx, query, id).Scan(
        &u.ID,
        &u.Nome,
        &u.Email,
        &u.CriadoEm,
    )

    if err == sql.ErrNoRows {
        return nil, fmt.Errorf("usuário %d não encontrado", id)
    }
    if err != nil {
        return nil, fmt.Errorf("buscarUsuarioPorID: %w", err)
    }

    return &u, nil
}

sql.ErrNoRows é o erro retornado quando nenhuma linha é encontrada — é uma condição esperada, não um erro de banco de dados. Sempre trate-o separadamente.


Query: buscando múltiplos registros

func listarUsuariosAtivos(ctx context.Context, db *sql.DB, limite int) ([]Usuario, error) {
    query := `
        SELECT id, nome, email, criado_em
        FROM usuarios
        WHERE ativo = true
        ORDER BY nome
        LIMIT ?
    `

    rows, err := db.QueryContext(ctx, query, limite)
    if err != nil {
        return nil, fmt.Errorf("listarUsuarios: %w", err)
    }
    defer rows.Close() // CRÍTICO: sempre fechar rows

    var usuarios []Usuario
    for rows.Next() {
        var u Usuario
        if err := rows.Scan(&u.ID, &u.Nome, &u.Email, &u.CriadoEm); err != nil {
            return nil, fmt.Errorf("listarUsuarios scan: %w", err)
        }
        usuarios = append(usuarios, u)
    }

    // Verificar erro após iteração — pode indicar problema de rede ou cursor
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("listarUsuarios rows: %w", err)
    }

    return usuarios, nil
}

O defer rows.Close() é obrigatório — Rows ocupa uma conexão do pool enquanto não for fechado. Esquecer o Close em código de alta carga esgota o pool.


Exec: inserindo, atualizando e deletando

func criarUsuario(ctx context.Context, db *sql.DB, nome, email, senhaHash string) (int64, error) {
    query := `
        INSERT INTO usuarios (nome, email, senha_hash, ativo, criado_em)
        VALUES (?, ?, ?, true, NOW())
    `

    resultado, err := db.ExecContext(ctx, query, nome, email, senhaHash)
    if err != nil {
        return 0, fmt.Errorf("criarUsuario: %w", err)
    }

    // ID gerado pelo banco para o novo registro
    id, err := resultado.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("obter ID inserido: %w", err)
    }

    return id, nil
}

func atualizarEmail(ctx context.Context, db *sql.DB, id int, novoEmail string) error {
    query := `UPDATE usuarios SET email = ?, atualizado_em = NOW() WHERE id = ?`

    resultado, err := db.ExecContext(ctx, query, novoEmail, id)
    if err != nil {
        return fmt.Errorf("atualizarEmail: %w", err)
    }

    linhasAfetadas, err := resultado.RowsAffected()
    if err != nil {
        return fmt.Errorf("verificar linhas afetadas: %w", err)
    }
    if linhasAfetadas == 0 {
        return fmt.Errorf("usuário %d não encontrado", id)
    }

    return nil
}

func deletarUsuario(ctx context.Context, db *sql.DB, id int) error {
    resultado, err := db.ExecContext(ctx,
        "DELETE FROM usuarios WHERE id = ?", id)
    if err != nil {
        return fmt.Errorf("deletarUsuario: %w", err)
    }

    if n, _ := resultado.RowsAffected(); n == 0 {
        return fmt.Errorf("usuário %d não encontrado", id)
    }

    return nil
}

Transações

Transações garantem que um conjunto de operações seja executado atomicamente — ou todas têm sucesso, ou nenhuma persiste:

func transferirSaldo(
    ctx context.Context,
    db *sql.DB,
    contaOrigemID,
    contaDestinoID int,
    valor float64,
) error {
    tx, err := db.BeginTx(ctx, &sql.TxOptions{
        Isolation: sql.LevelReadCommitted,
        ReadOnly:  false,
    })
    if err != nil {
        return fmt.Errorf("iniciar transação: %w", err)
    }

    // Garante rollback em caso de erro ou pânico
    defer func() {
        if p := recover(); p != nil {
            tx.Rollback()
            panic(p) // repropaga o pânico
        }
    }()

    // Verifica saldo da conta de origem com lock
    var saldo float64
    err = tx.QueryRowContext(ctx,
        "SELECT saldo FROM contas WHERE id = ? FOR UPDATE",
        contaOrigemID,
    ).Scan(&saldo)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("verificar saldo: %w", err)
    }

    if saldo < valor {
        tx.Rollback()
        return fmt.Errorf("saldo insuficiente: %.2f < %.2f", saldo, valor)
    }

    // Debita da origem
    _, err = tx.ExecContext(ctx,
        "UPDATE contas SET saldo = saldo - ? WHERE id = ?",
        valor, contaOrigemID,
    )
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("debitar origem: %w", err)
    }

    // Credita no destino
    _, err = tx.ExecContext(ctx,
        "UPDATE contas SET saldo = saldo + ? WHERE id = ?",
        valor, contaDestinoID,
    )
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("creditar destino: %w", err)
    }

    // Registra a transferência
    _, err = tx.ExecContext(ctx,
        `INSERT INTO transferencias (origem_id, destino_id, valor, realizada_em)
         VALUES (?, ?, ?, NOW())`,
        contaOrigemID, contaDestinoID, valor,
    )
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("registrar transferência: %w", err)
    }

    if err := tx.Commit(); err != nil {
        return fmt.Errorf("confirmar transação: %w", err)
    }

    return nil
}

Prepared statements

Para queries executadas repetidamente, prepared statements melhoram performance e segurança:

type RepositorioUsuarios struct {
    db           *sql.DB
    stmtBuscar   *sql.Stmt
    stmtListar   *sql.Stmt
    stmtCriar    *sql.Stmt
}

func NovoRepositorioUsuarios(db *sql.DB) (*RepositorioUsuarios, error) {
    stmtBuscar, err := db.Prepare(
        "SELECT id, nome, email FROM usuarios WHERE id = ?")
    if err != nil {
        return nil, fmt.Errorf("preparar buscar: %w", err)
    }

    stmtListar, err := db.Prepare(
        "SELECT id, nome, email FROM usuarios ORDER BY nome LIMIT ?")
    if err != nil {
        stmtBuscar.Close()
        return nil, fmt.Errorf("preparar listar: %w", err)
    }

    stmtCriar, err := db.Prepare(
        "INSERT INTO usuarios (nome, email, criado_em) VALUES (?, ?, NOW())")
    if err != nil {
        stmtBuscar.Close()
        stmtListar.Close()
        return nil, fmt.Errorf("preparar criar: %w", err)
    }

    return &RepositorioUsuarios{
        db:         db,
        stmtBuscar: stmtBuscar,
        stmtListar: stmtListar,
        stmtCriar:  stmtCriar,
    }, nil
}

func (r *RepositorioUsuarios) Fechar() {
    r.stmtBuscar.Close()
    r.stmtListar.Close()
    r.stmtCriar.Close()
}

func (r *RepositorioUsuarios) BuscarPorID(ctx context.Context, id int) (*Usuario, error) {
    var u Usuario
    err := r.stmtBuscar.QueryRowContext(ctx, id).Scan(&u.ID, &u.Nome, &u.Email)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return &u, err
}

SQL injection: nunca interpolação de strings

O placeholder ? (MySQL) ou $1 (PostgreSQL) é a proteção contra SQL injection. Nunca construa queries concatenando strings com dados do usuário:

// NUNCA FAÇA ISSO
query := fmt.Sprintf("SELECT * FROM usuarios WHERE nome = '%s'", nome)
// nome = "'; DROP TABLE usuarios; --"  ← SQL injection

// SEMPRE USE PLACEHOLDERS
query := "SELECT * FROM usuarios WHERE nome = ?"
db.QueryContext(ctx, query, nome) // nome é escapado automaticamente pelo driver

Monitorando o pool com sql.DBStats

func logEstatisticasPool(db *sql.DB, logger *slog.Logger) {
    stats := db.Stats()
    logger.Info("pool de conexões",
        "abertas",        stats.OpenConnections,
        "em_uso",         stats.InUse,
        "ociosas",        stats.Idle,
        "esperas",        stats.WaitCount,
        "tempo_espera",   stats.WaitDuration,
        "max_ociosas",    stats.MaxIdleClosed,
        "max_vida",       stats.MaxLifetimeClosed,
    )
}

Resumo do que foi coberto

Este artigo apresentou o pacote database/sql em profundidade: a arquitetura de interface e drivers, registro de drivers via importação em branco, configuração do pool de conexões, QueryRowContext para registro único, QueryContext para múltiplos registros com iteração segura, ExecContext para inserções, atualizações e deleções, transações com rollback automático, prepared statements para queries repetidas, a regra absoluta contra SQL injection e monitoramento do pool. Os próximos artigos aplicam esses conceitos em cada banco de dados específico.


Referências e leituras complementares

  • Documentação do pacote database/sql — Referência completa e oficial. https://pkg.go.dev/database/sql

  • Go Blog: Accessing databases — Tutorial oficial de acesso a bancos de dados. https://go.dev/doc/database/

  • Go Blog: Prepared statements — Quando e como usar prepared statements. https://go.dev/doc/database/prepared-statements

  • go-sql-driver/mysql — Driver MySQL mais usado no ecossistema Go. https://github.com/go-sql-driver/mysql

  • lib/pq — Driver PostgreSQL clássico compatível com database/sql. https://github.com/lib/pq

  • OWASP SQL Injection — Referência sobre SQL injection e prevenção. https://owasp.org/www-community/attacks/SQL_Injection


Próximo artigo: Artigo 33 — Go com MySQL e MariaDB: conexão, queries e transações**


you asked

Sim


claude response

Comentários

Mais em Golang

A biblioteca padrão: um tour pelas principais ferramentas
A biblioteca padrão: um tour pelas principais ferramentas

Uma das raz&otilde;es pelas quais Go se tornou a linguagem preferida para inf...

Funções: declaração, múltiplos retornos e variádicas
Funções: declaração, múltiplos retornos e variádicas

Em Go, fun&ccedil;&otilde;es s&atilde;o cidad&atilde;s de primeira classe. El...

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go
Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go O binário...