O pacote database/sql é a abstração oficial do Go para acesso a bancos de dados SQL. Ele fornece uma interface comum para operar com diferentes bancos (MySQL, PostgreSQL, SQLite etc.) através de drivers específicos. Nesta aula, você vai entender como conectar, consultar e gerenciar conexões de forma eficiente, evitando armadilhas comuns.

Vamos percorrer desde a escolha do driver até as boas práticas de pool de conexões, com exemplos práticos que você pode adaptar ao seu banco de dados favorito. Ao final, você terá uma base sólida para construir aplicações robustas com persistência em Go.

Drivers

O database/sql não inclui nenhum driver de banco de dados; ele define apenas a interface. Para usar um banco específico, você precisa importar um driver que implemente essa interface. Os drivers são registrados no pacote database/sql através da função sql.Register, mas na prática você importa o pacote do driver (geralmente com um import em branco) e ele se registra automaticamente no init().

Existem drivers para a maioria dos bancos populares. Alguns exemplos:

  • MySQL: github.com/go-sql-driver/mysql
  • PostgreSQL: github.com/lib/pq ou github.com/jackc/pgx/v5/stdlib
  • SQLite: github.com/mattn/go-sqlite3
  • SQL Server: github.com/denisenkom/go-mssqldb

Cada driver tem suas particularidades, mas o uso através do database/sql é semelhante. É importante escolher um driver bem mantido e compatível com a versão do seu banco. Você pode instalar um driver com go get, por exemplo:

go get github.com/go-sql-driver/mysql

No código, você importa o driver com um import em branco para que o init() do driver registre a sua implementação:

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

O pacote database/sql também oferece a interface Driver, que permite implementar o seu próprio driver, mas isso é avançado e raramente necessário.

Abrindo conexões

Para abrir uma conexão com o banco, usamos a função sql.Open. Ela recebe o nome do driver (string) e uma string de conexão (DSN - Data Source Name), que varia conforme o driver. Por exemplo, para MySQL:

db, err := sql.Open("mysql", "usuario:senha@tcp(127.0.0.1:3306)/meubanco")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

É importante entender que sql.Open não estabelece uma conexão real; ele apenas valida os argumentos e prepara o pool. A primeira conexão é criada de forma preguiçosa, quando você faz uma consulta ou chama Ping. Para verificar se o banco está acessível, use Ping ou PingContext:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
    log.Fatal("banco inacessível:", err)
}

O objeto *sql.DB é seguro para concorrência e deve ser compartilhado entre goroutines. Ele gerencia um pool de conexões, então você não precisa (e não deve) abrir e fechar conexões a cada operação. A função Close deve ser chamada quando o programa terminar, mas raramente dentro de handlers HTTP.

Query e Exec

O pacote database/sql oferece duas categorias principais de operações: Query para consultas que retornam linhas (SELECT) e Exec para comandos que não retornam linhas (INSERT, UPDATE, DELETE, DDL). Ambas têm variantes com contexto: QueryContext e ExecContext.

Exemplo de Query:

rows, err := db.Query("SELECT id, nome FROM usuarios WHERE ativo = ?", true)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

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

Observe o uso de ? como placeholder. Em PostgreSQL, usa-se $1, $2 etc. O método Scan copia os valores das colunas para as variáveis indicadas. É crucial chamar defer rows.Close() para liberar recursos e rows.Err() após o loop para capturar erros de iteração.

Exemplo de Exec:

result, err := db.Exec("INSERT INTO usuarios (nome, email) VALUES (?, ?)", "João", "joao@example.com")
if err != nil {
    log.Fatal(err)
}
id, _ := result.LastInsertId()
fmt.Println("ID inserido:", id)

Exec retorna um sql.Result, que oferece métodos como LastInsertId() e RowsAffected(). Nem todos os drivers suportam LastInsertId; para PostgreSQL, você pode usar RETURNING id com QueryRow.

Para consultas que retornam uma única linha, use QueryRow:

var nome string
err := db.QueryRow("SELECT nome FROM usuarios WHERE id = ?", 1).Scan(&nome)
if err == sql.ErrNoRows {
    fmt.Println("não encontrado")
} else if err != nil {
    log.Fatal(err)
}
fmt.Println(nome)

Pool de conexões

O *sql.DB gerencia um pool de conexões, que pode ser configurado para otimizar o desempenho. As principais configurações são:

  • SetMaxOpenConns: limite máximo de conexões abertas simultaneamente.
  • SetMaxIdleConns: número máximo de conexões ociosas mantidas no pool.
  • SetConnMaxLifetime: tempo máximo de vida de uma conexão (para reciclá-la).
  • SetConnMaxIdleTime: tempo máximo que uma conexão pode ficar ociosa antes de ser fechada.

Exemplo de configuração:

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

Um pool bem configurado evita problemas como estouro de conexões no banco ou conexões obsoletas. Se você definir MaxOpenConns como 0, o número é ilimitado (padrão). Para aplicações web, valores entre 10 e 50 são comuns, mas depende da carga e do banco.

Além disso, é importante usar context.Context em operações de banco para poder cancelar consultas longas e evitar vazamentos. Por exemplo:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM relatorios")
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

Boas práticas

  • Sempre feche rows com defer, mesmo se você iterar todas as linhas.
  • Use QueryRowContext e ExecContext para respeitar contextos.
  • Evite interpolar valores em SQL; use placeholders para prevenir SQL injection.
  • Configure o pool de conexões de acordo com a carga esperada.
  • Teste a conexão com Ping no início da aplicação.

Referências

Exercícios

  1. Instale o driver MySQL e escreva um programa que abra uma conexão com um banco local e faça um Ping. Configure um timeout de 5 segundos.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "database/sql"
        "fmt"
        "log"
        "time"
        _ "github.com/go-sql-driver/mysql"
    )
    
    func main() {
        db, err := sql.Open("mysql", "root:senha@tcp(127.0.0.1:3306)/teste")
        if err != nil {
            log.Fatal(err)
        }
        defer db.Close()
    
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
    
        if err := db.PingContext(ctx); err != nil {
            log.Fatal("erro no ping:", err)
        }
        fmt.Println("conexão OK")
    }
  2. Crie uma tabela 'produtos' com colunas id (INT AUTO_INCREMENT) e nome (VARCHAR). Usando Exec, insira três produtos e exiba o número de linhas afetadas.

    ✓ Resposta:
    package main
    
    import (
        "database/sql"
        "fmt"
        "log"
        _ "github.com/go-sql-driver/mysql"
    )
    
    func main() {
        db, _ := sql.Open("mysql", "root:senha@tcp(127.0.0.1:3306)/teste")
        defer db.Close()
    
        // Criação da tabela (DDL)
        _, err := db.Exec(`CREATE TABLE IF NOT EXISTS produtos (
            id INT AUTO_INCREMENT PRIMARY KEY,
            nome VARCHAR(100) NOT NULL
        )`)
        if err != nil {
            log.Fatal(err)
        }
    
        // Inserção de três produtos
        result, err := db.Exec("INSERT INTO produtos (nome) VALUES (?), (?), (?)", "Caneta", "Caderno", "Lápis")
        if err != nil {
            log.Fatal(err)
        }
        n, _ := result.RowsAffected()
        fmt.Printf("Linhas inseridas: %d\n", n)
    }
  3. Escreva uma função que receba um ID e retorne o nome do produto correspondente usando QueryRow. Trate o caso de não encontrado.

    ✓ Resposta:
    func buscarNome(db *sql.DB, id int) (string, error) {
        var nome string
        err := db.QueryRow("SELECT nome FROM produtos WHERE id = ?", id).Scan(&nome)
        if err == sql.ErrNoRows {
            return "", fmt.Errorf("produto %d não encontrado", id)
        }
        if err != nil {
            return "", err
        }
        return nome, nil
    }
  4. Usando Query, liste todos os produtos cujo nome começa com 'C'. Itere sobre as linhas e imprima id e nome.

    ✓ Resposta:
    package main
    
    import (
        "database/sql"
        "fmt"
        "log"
        _ "github.com/go-sql-driver/mysql"
    )
    
    func main() {
        db, _ := sql.Open("mysql", "root:senha@tcp(127.0.0.1:3306)/teste")
        defer db.Close()
    
        rows, err := db.Query("SELECT id, nome FROM produtos WHERE nome LIKE 'C%'")
        if err != nil {
            log.Fatal(err)
        }
        defer rows.Close()
    
        for rows.Next() {
            var id int
            var nome string
            if err := rows.Scan(&id, &nome); err != nil {
                log.Fatal(err)
            }
            fmt.Printf("%d: %s\n", id, nome)
        }
        if err := rows.Err(); err != nil {
            log.Fatal(err)
        }
    }
  5. Configure um pool de conexões com MaxOpenConns=10, MaxIdleConns=5 e ConnMaxLifetime=3 minutos. Explique por que essas configurações são úteis.

    ✓ Resposta:
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(3 * time.Minute)

    Essas configurações limitam o número de conexões abertas para evitar sobrecarga no banco, mantêm conexões ociosas prontas para uso rápido e reciclam conexões antigas para evitar problemas de rede ou banco.