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