O pacote database/sql
Nesta aula, você aprenderá a usar o pacote database/sql do Go para interagir com bancos de dados SQL. Exploraremos drivers, abertura de conexões, execução de consultas com Query e Exec, e o gerenciamento do pool de conexões, com exemplos práticos e boas práticas.
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/pqougithub.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/mysqlNo 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
rowscom defer, mesmo se você iterar todas as linhas. - Use
QueryRowContexteExecContextpara 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
Pingno início da aplicação.
Referências
- Documentação oficial do database/sql
- Go Database/SQL Tutorial
- Driver MySQL para Go
- Driver PostgreSQL (lib/pq)
- Driver SQLite (mattn)
- Gerenciando conexões com database/sql
Exercícios
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") }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) }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 }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) } }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.