O pacote database/sql é a interface padrão do Go para acesso a bancos de dados relacionais. Ele fornece uma camada de abstração que permite trabalhar com diferentes SGBDs (como MySQL, PostgreSQL, SQLite) de forma consistente, sem precisar conhecer os detalhes específicos de cada um. Nesta aula, vamos explorar os conceitos fundamentais desse pacote, desde a escolha do driver adequado até o gerenciamento eficiente do pool de conexões.

É importante entender que o database/sql não é um driver em si, mas uma interface que define funções e tipos comuns. Para se conectar a um banco de dados específico, você precisa de um driver que implemente essa interface. Vamos aprender como configurar tudo isso corretamente.

Drivers

Para usar o database/sql, você deve importar um driver específico para o seu banco de dados. Existem muitos drivers disponíveis, mantidos pela comunidade ou por empresas. Cada driver registra-se com um nome, que será usado na string de conexão. Por exemplo, para MySQL usamos o driver github.com/go-sql-driver/mysql que se registra como mysql. Para PostgreSQL, o driver github.com/lib/pq registra-se como postgres. Para SQLite, o driver github.com/mattn/go-sqlite3 registra-se como sqlite3.

Para instalar um driver, você usa o comando go get. Por exemplo, para MySQL:

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

Depois de importar o driver no seu código, ele se registra automaticamente no pacote sql por meio da função init do driver. Você não precisa chamar nenhuma função de registro manualmente. Apenas importar o pacote com um alias em branco (_) é suficiente para que o driver seja registrado, como mostrado abaixo:

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

É crucial escolher um driver que seja bem mantido e compatível com a sua versão do Go e do banco de dados. Consulte a documentação oficial do driver para obter detalhes sobre a string de conexão e opções adicionais.

Abrindo conexões

A função principal para abrir uma conexão é sql.Open. Ela recebe o nome do driver e a string de conexão (DSN - Data Source Name), e retorna um ponteiro para sql.DB. É importante notar que sql.Open não estabelece uma conexão imediatamente; ela apenas configura um pool de conexões que será usado quando as consultas forem executadas. A conexão real é criada de forma lazy, na primeira operação que requer acesso ao banco.

Exemplo de abertura de conexão com MySQL:

package main

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

func main() {
    // DSN: usuario:senha@tcp(host:porta)/nome_do_banco
    dsn := "root:senha@tcp(127.0.0.1:3306)/meubanco"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        panic(err)
    }
    defer db.Close()

    // Agora podemos usar db para executar consultas
}

É uma boa prática verificar se a conexão está realmente funcionando usando db.Ping(), que tenta estabelecer uma conexão e retorna um erro se falhar. Por exemplo:

err = db.Ping()
if err != nil {
    // tratar erro
}

Você também pode definir configurações do pool de conexões antes de usar o banco, como veremos na seção sobre pool.

Query e Exec

O pacote database/sql oferece dois tipos principais de operações: Query para consultas que retornam linhas (como SELECT) e Exec para comandos que não retornam linhas (como INSERT, UPDATE, DELETE). Cada uma tem variações que aceitam contextos e parâmetros.

Exec é usado para executar comandos que não retornam dados. Ele retorna um sql.Result, que contém informações como o número de linhas afetadas e o último ID inserido (se o banco suportar). Exemplo:

result, err := db.Exec("INSERT INTO usuarios (nome, email) VALUES (?, ?)", "João", "joao@email.com")
if err != nil {
    // tratar erro
}

id, _ := result.LastInsertId()
linhas, _ := result.RowsAffected()
fmt.Printf("ID inserido: %d, linhas afetadas: %d\n", id, linhas)

Note que usamos ? como placeholders para parâmetros, o que previne ataques de injeção de SQL. O driver converte os parâmetros para o formato adequado do banco.

Query é usado para consultas que retornam linhas. Ele retorna um *sql.Rows, que você deve percorrer com Next() e ler os valores com Scan. Exemplo:

rows, err := db.Query("SELECT id, nome, email FROM usuarios")
if err != nil {
    // tratar erro
}
defer rows.Close()

for rows.Next() {
    var id int
    var nome, email string
    err = rows.Scan(&id, &nome, &email)
    if err != nil {
        // tratar erro
    }
    fmt.Printf("ID: %d, Nome: %s, Email: %s\n", id, nome, email)
}

// Verifica se houve erro durante a iteração
if err = rows.Err(); err != nil {
    // tratar erro
}

É importante sempre fechar o Rows com defer rows.Close() para liberar recursos. Além disso, você pode usar QueryRow para consultas que retornam no máximo uma linha:

var nome string
err := db.QueryRow("SELECT nome FROM usuarios WHERE id = ?", 1).Scan(&nome)
if err != nil {
    if err == sql.ErrNoRows {
        // nenhuma linha encontrada
    } else {
        // tratar erro
    }
}
fmt.Println(nome)

Para evitar problemas de concorrência, lembre-se de que um sql.Rows não pode ser usado simultaneamente por múltiplas goroutines. Se precisar, você pode copiar os dados ou usar rows.Close() antes de continuar.

Pool de conexões

O sql.DB gerencia um pool de conexões para reutilizar conexões e maximizar a eficiência. Por padrão, o pool é ilimitado, mas você pode configurá-lo para limitar o número de conexões abertas, o que é importante para evitar sobrecarga no banco.

As principais configurações são:

  • SetMaxOpenConns(n): define o número máximo de conexões abertas simultaneamente. Se o limite for atingido, novas solicitações ficam aguardando até que uma conexão seja liberada.
  • SetMaxIdleConns(n): define o número máximo de conexões ociosas no pool. Conexões ociosas são mantidas para reutilização, mas se o limite for excedido, as extras são fechadas.
  • SetConnMaxLifetime(d): define o tempo máximo de vida de uma conexão. Conexões mais antigas que esse período são recicladas, o que é útil para evitar conexões obsoletas.
  • SetConnMaxIdleTime(d): define o tempo máximo que uma conexão pode ficar ociosa antes de ser fechada.

Exemplo de configuração:

db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(time.Hour)
db.SetConnMaxIdleTime(30 * time.Minute)

Essas configurações devem ser feitas logo após abrir a conexão, antes de executar qualquer operação, para que o pool seja ajustado corretamente.

O pool de conexões é seguro para uso concorrente, portanto você pode compartilhar o sql.DB entre várias goroutines sem problemas. No entanto, é importante não fechar o sql.DB prematuramente; use defer db.Close() apenas quando o programa terminar.

Boas práticas

Ao trabalhar com database/sql, siga estas boas práticas:

  • Sempre use placeholders (? ou $1 dependendo do driver) para parâmetros, nunca concatene strings.
  • Feche os recursos (Rows, Stmt) com defer Close() para evitar vazamentos.
  • Verifique os erros retornados por todas as operações, incluindo rows.Err() após a iteração.
  • Use context.Context para controlar timeouts e cancelamentos, especialmente em operações de longa duração.
  • Monitore o pool de conexões com db.Stats() para entender o uso e ajustar as configurações.

Referências

Exercícios

  1. Escreva um programa em Go que se conecte a um banco SQLite em memória, crie uma tabela 'produtos' com campos 'id' (INTEGER PRIMARY KEY) e 'nome' (TEXT), e insira três produtos. Use o driver github.com/mattn/go-sqlite3.

    ✓ Resposta:
    package main
    
    import (
        "database/sql"
        "fmt"
        _ "github.com/mattn/go-sqlite3"
    )
    
    func main() {
        db, err := sql.Open("sqlite3", ":memory:")
        if err != nil {
            panic(err)
        }
        defer db.Close()
    
        createTable := `CREATE TABLE produtos (id INTEGER PRIMARY KEY, nome TEXT NOT NULL)`
        _, err = db.Exec(createTable)
        if err != nil {
            panic(err)
        }
    
        insert := `INSERT INTO produtos (nome) VALUES (?)`
        _, err = db.Exec(insert, "Produto A")
        if err != nil {
            panic(err)
        }
        _, err = db.Exec(insert, "Produto B")
        if err != nil {
            panic(err)
        }
        _, err = db.Exec(insert, "Produto C")
        if err != nil {
            panic(err)
        }
    
        fmt.Println("Produtos inseridos com sucesso!")
    }
  2. Modifique o programa do exercício 1 para consultar todos os produtos e imprimir seus IDs e nomes.

    ✓ Resposta:
    rows, err := db.Query("SELECT id, nome FROM produtos")
    if err != nil {
        panic(err)
    }
    defer rows.Close()
    
    for rows.Next() {
        var id int
        var nome string
        err = rows.Scan(&id, &nome)
        if err != nil {
            panic(err)
        }
        fmt.Printf("ID: %d, Nome: %s\n", id, nome)
    }
    if err = rows.Err(); err != nil {
        panic(err)
    }
  3. Usando o MySQL, crie uma conexão e configure o pool para máximo de 10 conexões abertas e 5 ociosas, com tempo máximo de vida de 1 hora.

    ✓ Resposta:
    db, err := sql.Open("mysql", "usuario:senha@tcp(localhost:3306)/meubanco")
    if err != nil {
        panic(err)
    }
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(time.Hour)
    
  4. Escreva uma função que receba um *sql.DB e um ID, e retorne o nome do usuário correspondente usando QueryRow. Trate o caso de nenhuma linha encontrada.

    ✓ Resposta:
    func getUserName(db *sql.DB, id int) (string, error) {
        var nome string
        err := db.QueryRow("SELECT nome FROM usuarios WHERE id = ?", id).Scan(&nome)
        if err != nil {
            if err == sql.ErrNoRows {
                return "", fmt.Errorf("usuário %d não encontrado", id)
            }
            return "", err
        }
        return nome, nil
    }
  5. Explique a diferença entre Query e Exec e dê um exemplo de uso para cada.

    ✓ Resposta:

    Query é usado para consultas que retornam linhas, como SELECT. Exec é usado para comandos que não retornam linhas, como INSERT, UPDATE, DELETE. Exemplo de Query: rows, err := db.Query("SELECT * FROM usuarios"). Exemplo de Exec: _, err := db.Exec("DELETE FROM usuarios WHERE id = ?", 1).