Banco de dados com database/sql
1. Introdução ao pacote database/sql
O pacote database/sql é a abstração oficial da linguagem Go para interagir com bancos de dados relacionais. Ele fornece uma interface genérica que permite trabalhar com diferentes SGBDs (PostgreSQL, MySQL, SQLite, etc.) sem alterar a lógica da aplicação — apenas trocando o driver importado.
É fundamental entender que database/sql não é um driver de banco de dados. Ele define interfaces e funções comuns, enquanto os drivers específicos (como lib/pq para PostgreSQL ou go-sqlite3 para SQLite) implementam a comunicação real com cada SGBD.
O fluxo básico de trabalho com database/sql segue três etapas principais:
1. Conexão: abrir e configurar o pool de conexões
2. Execução: enviar queries e comandos
3. Resultados: processar linhas retornadas ou verificar alterações
2. Configuração e abertura de conexão
Para começar, importamos o driver específico (com o underscore para ativar seu registro automático) e o pacote database/sql:
import (
"database/sql"
_ "github.com/lib/pq" // driver PostgreSQL
)
A função sql.Open() não estabelece uma conexão imediata — ela apenas cria um pool gerenciado. A conexão real ocorre quando necessário (primeira query ou Ping()):
db, err := sql.Open("postgres", "host=localhost port=5432 user=admin dbname=meubanco sslmode=disable")
if err != nil {
log.Fatal("Erro ao abrir pool:", err)
}
defer db.Close()
O pool pode ser configurado para otimizar desempenho:
db.SetMaxOpenConns(25) // máximo de conexões abertas simultâneas
db.SetMaxIdleConns(5) // máximo de conexões ociosas no pool
db.SetConnMaxLifetime(5 * time.Minute) // tempo máximo de vida de uma conexão
Para verificar se o banco está acessível:
if err := db.Ping(); err != nil {
log.Fatal("Banco indisponível:", err)
}
fmt.Println("Conectado com sucesso!")
3. Execução de consultas: Query e QueryRow
Múltiplas linhas com Query()
Para consultas que retornam várias linhas, usamos db.Query() e iteramos com Rows.Next():
rows, err := db.Query("SELECT id, nome, email FROM usuarios WHERE ativo = $1", true)
if err != nil {
log.Fatal(err)
}
defer rows.Close() // essencial para liberar recursos
for rows.Next() {
var id int
var nome, email string
if err := rows.Scan(&id, &nome, &email); err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s (%s)\n", id, nome, email)
}
// Verificar erro após iteração
if err := rows.Err(); err != nil {
log.Fatal(err)
}
Linha única com QueryRow()
Quando esperamos apenas um resultado, QueryRow() é mais direto:
var nome, email string
err := db.QueryRow("SELECT nome, email FROM usuarios WHERE id = $1", 42).Scan(&nome, &email)
if err == sql.ErrNoRows {
fmt.Println("Usuário não encontrado")
} else if err != nil {
log.Fatal(err)
}
fmt.Printf("Nome: %s, Email: %s\n", nome, email)
4. Execução de comandos: Exec
Para INSERT, UPDATE, DELETE (operações que não retornam linhas), usamos db.Exec():
result, err := db.Exec("INSERT INTO usuarios (nome, email) VALUES ($1, $2)", "João Silva", "joao@email.com")
if err != nil {
log.Fatal(err)
}
linhasAfetadas, _ := result.RowsAffected()
fmt.Printf("%d linha(s) inserida(s)\n", linhasAfetadas)
// LastInsertId() depende do driver
if id, err := result.LastInsertId(); err == nil {
fmt.Printf("ID gerado: %d\n", id)
}
Atenção: LastInsertId() não é suportado por todos os drivers (ex.: PostgreSQL) — nesses casos, use RETURNING com QueryRow().
5. Parâmetros e prepared statements
O uso de placeholders previne SQL injection automaticamente. Cada driver usa seu próprio estilo:
- PostgreSQL: $1, $2, ...
- MySQL: ?
- SQLite: ? ou $1
Prepared statements trazem benefícios de performance (query compilada uma vez) e segurança:
stmt, err := db.Prepare("INSERT INTO produtos (nome, preco, categoria) VALUES ($1, $2, $3)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// Reutilizar o statement
produtos := []struct{ nome string; preco float64; categoria string }{
{"Teclado", 89.90, "Periféricos"},
{"Mouse", 45.50, "Periféricos"},
{"Monitor", 1299.00, "Monitores"},
}
for _, p := range produtos {
if _, err := stmt.Exec(p.nome, p.preco, p.categoria); err != nil {
log.Fatal(err)
}
}
6. Transações com Begin e Tx
Transações garantem atomicidade: todas as operações são confirmadas ou revertidas juntas:
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
defer tx.Rollback() // seguro: se houver panic ou erro, faz rollback
// Operações dentro da transação
_, err = tx.Exec("UPDATE contas SET saldo = saldo - $1 WHERE id = $2", 100.00, 1)
if err != nil {
return // defer tx.Rollback() será executado
}
_, err = tx.Exec("UPDATE contas SET saldo = saldo + $1 WHERE id = $2", 100.00, 2)
if err != nil {
return // rollback automático
}
// Confirmar tudo
if err := tx.Commit(); err != nil {
log.Fatal("Erro ao commitar:", err)
}
O padrão defer tx.Rollback() combinado com tx.Commit() explícito é seguro: se Commit() for chamado com sucesso, o Rollback() posterior não tem efeito.
7. Tratamento de erros e boas práticas
Erros comuns e como tratá-los:
// sql.ErrNoRows: nenhum resultado encontrado
err := db.QueryRow("SELECT * FROM usuarios WHERE id = $1", 999).Scan(...)
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("Registro não existe")
}
// sql.ErrConnDone: operação em conexão já fechada
// sql.ErrTxDone: operação em transação já finalizada
Boas práticas essenciais:
// Sempre fechar recursos
defer rows.Close()
defer stmt.Close()
defer db.Close()
// Timeout para operações longas
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT ...")
8. Exemplo completo: CRUD básico
Estrutura e funções CRUD com PostgreSQL:
type Usuario struct {
ID int
Nome string
Email string
}
func CriarUsuario(db *sql.DB, u *Usuario) error {
return db.QueryRow(
"INSERT INTO usuarios (nome, email) VALUES ($1, $2) RETURNING id",
u.Nome, u.Email,
).Scan(&u.ID)
}
func ListarUsuarios(db *sql.DB) ([]Usuario, error) {
rows, err := db.Query("SELECT id, nome, email FROM usuarios ORDER BY id")
if err != nil {
return nil, err
}
defer rows.Close()
var usuarios []Usuario
for rows.Next() {
var u Usuario
if err := rows.Scan(&u.ID, &u.Nome, &u.Email); err != nil {
return nil, err
}
usuarios = append(usuarios, u)
}
return usuarios, rows.Err()
}
func AtualizarUsuario(db *sql.DB, u *Usuario) error {
_, err := db.Exec(
"UPDATE usuarios SET nome = $1, email = $2 WHERE id = $3",
u.Nome, u.Email, u.ID,
)
return err
}
func DeletarUsuario(db *sql.DB, id int) error {
_, err := db.Exec("DELETE FROM usuarios WHERE id = $1", id)
return err
}
// Exemplo com transação
func TransferirSaldo(db *sql.DB, deID, paraID int, valor float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("UPDATE contas SET saldo = saldo - $1 WHERE id = $2", valor, deID)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE contas SET saldo = saldo + $1 WHERE id = $2", valor, paraID)
if err != nil {
return err
}
return tx.Commit()
}
O pacote database/sql oferece uma base sólida e portável para trabalhar com bancos de dados relacionais em Go. Dominar seus padrões — prepared statements, transações e tratamento correto de erros — é essencial para construir aplicações robustas e seguras.
Referências
- Documentação oficial do pacote database/sql — Referência completa de tipos, funções e métodos do pacote padrão
- Go database/sql tutorial — Tutorial prático e abrangente sobre o uso de database/sql em Go
- Using SQL Databases in Go - Alex Edwards — Artigo técnico com exemplos práticos de CRUD e boas práticas
- lib/pq - Driver PostgreSQL para Go — Driver PostgreSQL puro em Go, amplamente utilizado com database/sql
- go-sqlite3 - Driver SQLite para Go — Driver SQLite compatível com database/sql, ideal para aplicações embarcadas
- SQL injection prevention in Go — Discussão no Stack Overflow sobre prevenção de SQL injection usando placeholders
- Database/sql transactions best practices — Guia sobre transações com database/sql, incluindo padrões de rollback e commit