Operações com Banco de Dados
Lição 27: Operações com Banco de Dados
Analogia do Mundo Real
Imagine que você administra uma biblioteca —
- O banco de dados é o armazém da biblioteca, onde todos os livros (dados) são armazenados ordenadamente nas prateleiras.
- A interface
database/sqlé como o manual do bibliotecário, especificando procedimentos padrão para "como estocar livros, como encontrar livros, como emprestar livros" — se o armazém usa prateleiras de madeira (SQLite) ou prateleiras de aço (MySQL), o manual funciona universalmente. - Drivers são chaves específicas para diferentes armazéns — a chave SQLite abre um pequeno armazém local, a chave MySQL abre um grande armazém remoto.
- Instruções preparadas são como formulários de empréstimo pré-impressos — você só precisa preencher o nome e o título do livro a cada vez, tornando-o rápido e seguro.
- Transações são como operações atômicas de "pegar emprestado e devolver" — ou todas completam ou todas revertem, evitando o constrangimento de "pegou emprestado mas não registrou".
Após esta lição, você poderá trabalhar com bancos de dados com confiança usando Go.
Conceitos Fundamentais
| Conceito | Descrição |
|---|---|
database/sql |
Biblioteca padrão Go, define uma interface unificada de operação com banco de dados |
| Driver | Um pacote de terceiros implementando a interface database/sql/driver |
sql.Open() |
Cria um pool de conexões de banco de dados (não estabelece conexões imediatamente) |
sql.DB |
Objeto de pool de conexões de banco de dados, thread-safe |
Query / QueryRow |
Executa instruções de consulta, retorna conjuntos de resultados |
Exec |
Executa instruções não-consulta (INSERT/UPDATE/DELETE) |
Prepare |
Instruções preparadas, melhora o desempenho de execução repetida |
Tx |
Objeto de transação, suporta Commit e Rollback |
| Pool de conexões | Reutiliza conexões, evitando o custo de abrir/fechar conexões frequentemente |
Sintaxe Básica e Uso
1. Importando Drivers
package main
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // Driver SQLite (importação anônima, apenas registra o driver)
)
_, porque eles só precisam executar a função init() para se registrarem no database/sql — não chamamos funções do driver diretamente.
2. Abrindo um Banco de Dados
db, err := sql.Open("sqlite3", "./mydb.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Verificar se a conexão está disponível
if err = db.Ping(); err != nil {
log.Fatal("Não foi possível conectar ao banco de dados:", err)
}
sql.Open() não estabelece conexões imediatamente — ele apenas inicializa a configuração do pool de conexões. Use Ping() para tentar uma conexão real.
3. Criando Tabelas
createTableSQL := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER DEFAULT 0
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatal("Falha ao criar tabela:", err)
}
4. Operações CRUD
// ---- INSERT ----
result, err := db.Exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
"Alice", "alice@example.com", 25)
if err != nil {
log.Fatal(err)
}
id, _ := result.LastInsertId() // Obter ID auto-incrementado
fmt.Println("Inserido com sucesso, ID:", id)
// ---- SELECT ----
var name, email string
var age int
err = db.QueryRow("SELECT name, email, age FROM users WHERE id = ?", id).
Scan(&name, &email, &age)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Resultado da consulta: %s, %s, %d anos\n", name, email, age)
// ---- UPDATE ----
_, err = db.Exec("UPDATE users SET age = ? WHERE id = ?", 26, id)
if err != nil {
log.Fatal(err)
}
// ---- DELETE ----
_, err = db.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
log.Fatal(err)
}
5. Instruções Preparadas
stmt, err := db.Prepare("INSERT INTO users (name, email, age) VALUES (?, ?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// O mesmo stmt pode ser reutilizado múltiplas vezes para melhor desempenho
stmt.Exec("Bob", "bob@example.com", 30)
stmt.Exec("Charlie", "charlie@example.com", 28)
6. Tratamento de Transações
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// Executar múltiplas instruções dentro de uma transação
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
if err != nil {
tx.Rollback() // Reverter em caso de erro
log.Fatal(err)
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
if err = tx.Commit(); err != nil { // Confirmar em caso de sucesso
log.Fatal(err)
}
fmt.Println("Transação confirmada com sucesso")
7. Configuração do Pool de Conexões
db.SetMaxOpenConns(25) // Máximo de conexões abertas
db.SetMaxIdleConns(10) // Máximo de conexões inativas
db.SetConnMaxLifetime(5 * time.Minute) // Tempo máximo de vida da conexão
db.SetConnMaxIdleTime(3 * time.Minute) // Tempo máximo de vida da conexão inativa
MaxOpenConns muito baixo causa enfileiramento de requisições; defini-lo muito alto esgota os recursos do banco de dados.
Prática Hands-On
Exemplo: Gerenciamento de Usuários SQLite (Dificuldade ⭐)
Um exemplo completo de gerenciamento de usuários demonstrando operações CRUD básicas.
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3" // Driver SQLite
)
// Estrutura User
type User struct {
ID int
Name string
Email string
Age int
}
func main() {
// Abrir (ou criar) banco de dados SQLite
db, err := sql.Open("sqlite3", "./users.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Criar tabela
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER DEFAULT 0
)
`)
if err != nil {
log.Fatal("Falha ao criar tabela:", err)
}
// Inserir usuário
result, err := db.Exec(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
"Alice", "alice@example.com", 25,
)
if err != nil {
log.Fatal("Falha na inserção:", err)
}
id, _ := result.LastInsertId()
fmt.Printf("Usuário inserido com sucesso, ID: %d\n", id)
// Consultar usuário único
var u User
err = db.QueryRow("SELECT id, name, email, age FROM users WHERE id = ?", id).
Scan(&u.ID, &u.Name, &u.Email, &u.Age)
if err != nil {
log.Fatal("Falha na consulta:", err)
}
fmt.Printf("Resultado da consulta: %+v\n", u)
// Atualizar idade do usuário
_, err = db.Exec("UPDATE users SET age = ? WHERE id = ?", 26, id)
if err != nil {
log.Fatal("Falha na atualização:", err)
}
fmt.Println("Atualizado com sucesso")
// Consultar todos os usuários
rows, err := db.Query("SELECT id, name, email, age FROM users")
if err != nil {
log.Fatal("Falha ao consultar todos:", err)
}
defer rows.Close()
fmt.Println("\nTodos os usuários:")
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age); err != nil {
log.Fatal(err)
}
fmt.Printf(" ID:%d Nome:%s Email:%s Idade:%d\n",
user.ID, user.Name, user.Email, user.Age)
}
// Excluir usuário
_, err = db.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
log.Fatal("Falha na exclusão:", err)
}
fmt.Println("Usuário excluído com sucesso")
}
Usuário inserido com sucesso, ID: 1
Resultado da consulta: {ID:1 Name:Alice Email:alice@example.com Age:25}
Atualizado com sucesso
Todos os usuários:
ID:1 Nome:Alice Email:alice@example.com Idade:26
Usuário excluído com sucesso
Exemplo: Encapsulamento CRUD + Instruções Preparadas (Dificuldade ⭐⭐)
Encapsula operações de banco de dados como métodos de estrutura, usando instruções preparadas para melhorar o desempenho.
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
// Modelo User
type User struct {
ID int
Name string
Email string
Age int
}
// UserDAO objeto de acesso a dados do usuário
type UserDAO struct {
db *sql.DB
insertStmt *sql.Stmt
getByID *sql.Stmt
updateStmt *sql.Stmt
deleteStmt *sql.Stmt
}
// NewUserDAO cria um DAO e inicializa instruções preparadas
func NewUserDAO(db *sql.DB) (*UserDAO, error) {
dao := &UserDAO{db: db}
// Inicializar tabela
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER DEFAULT 0
)
`)
if err != nil {
return nil, err
}
// Preparar todas as instruções comumente usadas
dao.insertStmt, err = db.Prepare(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)")
if err != nil {
return nil, err
}
dao.getByID, err = db.Prepare(
"SELECT id, name, email, age FROM users WHERE id = ?")
if err != nil {
return nil, err
}
dao.updateStmt, err = db.Prepare(
"UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?")
if err != nil {
return nil, err
}
dao.deleteStmt, err = db.Prepare(
"DELETE FROM users WHERE id = ?")
if err != nil {
return nil, err
}
return dao, nil
}
// Close fecha todas as instruções preparadas
func (d *UserDAO) Close() {
d.insertStmt.Close()
d.getByID.Close()
d.updateStmt.Close()
d.deleteStmt.Close()
}
// Create insere um novo usuário
func (d *UserDAO) Create(u *User) error {
result, err := d.insertStmt.Exec(u.Name, u.Email, u.Age)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
u.ID = int(id)
return nil
}
// GetByID consulta um usuário por ID
func (d *UserDAO) GetByID(id int) (*User, error) {
u := &User{}
err := d.getByID.QueryRow(id).Scan(&u.ID, &u.Name, &u.Email, &u.Age)
if err != nil {
return nil, err
}
return u, nil
}
// Update atualiza informações do usuário
func (d *UserDAO) Update(u *User) error {
_, err := d.updateStmt.Exec(u.Name, u.Email, u.Age, u.ID)
return err
}
// Delete exclui um usuário
func (d *UserDAO) Delete(id int) error {
_, err := d.deleteStmt.Exec(id)
return err
}
// ListAll consulta todos os usuários
func (d *UserDAO) ListAll() ([]User, error) {
rows, err := d.db.Query("SELECT id, name, email, age FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
func main() {
db, err := sql.Open("sqlite3", ":memory:") // Banco de dados em memória para demonstração
if err != nil {
log.Fatal(err)
}
defer db.Close()
dao, err := NewUserDAO(db)
if err != nil {
log.Fatal(err)
}
defer dao.Close()
// Inserção em lote
users := []User{
{Name: "Alice", Email: "alice@example.com", Age: 25},
{Name: "Bob", Email: "bob@example.com", Age: 30},
{Name: "Charlie", Email: "charlie@example.com", Age: 28},
}
for i := range users {
if err := dao.Create(&users[i]); err != nil {
log.Printf("Falha na inserção: %v", err)
continue
}
fmt.Printf("Inserido: %s (ID=%d)\n", users[i].Name, users[i].ID)
}
// Consultar e modificar
u, err := dao.GetByID(1)
if err != nil {
log.Fatal("Falha na consulta:", err)
}
fmt.Printf("\nConsulta ID=1: %+v\n", u)
u.Age = 26
if err := dao.Update(u); err != nil {
log.Fatal("Falha na atualização:", err)
}
fmt.Printf("Após atualização: %+v\n", u)
// Listar todos
all, _ := dao.ListAll()
fmt.Println("\nTodos os usuários:")
for _, user := range all {
fmt.Printf(" [%d] %s | %s | %d anos\n",
user.ID, user.Name, user.Email, user.Age)
}
// Excluir
dao.Delete(2)
fmt.Println("\nApós excluir ID=2:")
all, _ = dao.ListAll()
for _, user := range all {
fmt.Printf(" [%d] %s | %s | %d anos\n",
user.ID, user.Name, user.Email, user.Age)
}
}
Inserido: Alice (ID=1)
Inserido: Bob (ID=2)
Inserido: Charlie (ID=3)
Consulta ID=1: {ID:1 Name:Alice Email:alice@example.com Age:25}
Após atualização: {ID:1 Name:Alice Email:alice@example.com Age:26}
Todos os usuários:
[1] Alice | alice@example.com | 26 anos
[2] Bob | bob@example.com | 30 anos
[3] Charlie | charlie@example.com | 28 anos
Após excluir ID=2:
[1] Alice | alice@example.com | 26 anos
[3] Charlie | charlie@example.com | 28 anos
Exemplo: Transferência com Transação + Configuração do Pool de Conexões (Dificuldade ⭐⭐⭐)
Simula um cenário de transferência bancária, demonstrando garantias de atomicidade de transações e configuração do pool de conexões.
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
)
// Account conta bancária
type Account struct {
ID int
Name string
Balance float64
}
func initDB(db *sql.DB) error {
// Criar tabela
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
balance REAL NOT NULL DEFAULT 0
)
`)
return err
}
// Transfer realiza uma transferência (operação de transação)
// Transfere valor de fromID para toID
func Transfer(db *sql.DB, fromID, toID int, amount float64) error {
// Iniciar transação
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("falha ao iniciar transação: %w", err)
}
// Garantir que a transação confirma ou reverte
defer func() {
if err != nil {
tx.Rollback()
}
}()
// Verificar saldo da conta de origem
var fromBalance float64
err = tx.QueryRow("SELECT balance FROM accounts WHERE id = ?", fromID).Scan(&fromBalance)
if err != nil {
return fmt.Errorf("falha ao consultar conta de origem: %w", err)
}
if fromBalance < amount {
return fmt.Errorf("saldo insuficiente: atual %.2f, necessário %.2f", fromBalance, amount)
}
// Deduzir da conta de origem
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
if err != nil {
return fmt.Errorf("falha na dedução: %w", err)
}
// Adicionar à conta de destino
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
if err != nil {
return fmt.Errorf("falha no crédito: %w", err)
}
// Simular lógica de negócio: registrar log de operação (na mesma transação)
_, err = tx.Exec(
"INSERT INTO transfer_log (from_id, to_id, amount, created_at) VALUES (?, ?, ?, ?)",
fromID, toID, amount, time.Now().Format(time.RFC3339),
)
if err != nil {
// Se a tabela de log não existir, ignorar erro (apenas para demonstração)
fmt.Printf(" [Aviso] Falha ao registrar log de transferência: %v\n", err)
}
// Confirmar transação
if err = tx.Commit(); err != nil {
return fmt.Errorf("falha ao confirmar transação: %w", err)
}
return nil
}
// PrintBalances imprime todos os saldos das contas
func PrintBalances(db *sql.DB) {
rows, err := db.Query("SELECT id, name, balance FROM accounts ORDER BY id")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
fmt.Println(" Saldos das contas:")
for rows.Next() {
var a Account
if err := rows.Scan(&a.ID, &a.Name, &a.Balance); err != nil {
log.Fatal(err)
}
fmt.Printf(" [%d] %-6s Saldo: %.2f\n", a.ID, a.Name, a.Balance)
}
}
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Configurar pool de conexões
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(3 * time.Minute)
fmt.Println("✓ Pool de conexões configurado:")
fmt.Println(" Máximo de conexões: 10")
fmt.Println(" Máximo inativas: 5")
fmt.Println(" Tempo de vida da conexão: 5 minutos")
fmt.Println()
// Inicializar tabelas
if err := initDB(db); err != nil {
log.Fatal(err)
}
// Criar tabela de log de transferência (opcional)
db.Exec(`CREATE TABLE IF NOT EXISTS transfer_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_id INTEGER, to_id INTEGER,
amount REAL, created_at TEXT
)`)
// Inserir contas iniciais
_, err = db.Exec("INSERT INTO accounts (name, balance) VALUES (?, ?), (?, ?)",
"Alice", 1000.00, "Bob", 500.00)
if err != nil {
log.Fatal("Falha ao inicializar contas:", err)
}
fmt.Println("=== Estado Inicial ===")
PrintBalances(db)
fmt.Println()
// Transferência normal: Alice -> Bob 300
fmt.Println("=== Alice -> Bob Transferência 300 ===")
if err := Transfer(db, 1, 2, 300); err != nil {
fmt.Printf("Falha na transferência: %v\n", err)
} else {
fmt.Println("Transferência bem-sucedida!")
}
PrintBalances(db)
fmt.Println()
// Transferência com saldo insuficiente: Bob -> Alice 900
fmt.Println("=== Bob -> Alice Transferência 900 ===")
if err := Transfer(db, 2, 1, 900); err != nil {
fmt.Printf("Falha na transferência: %v\n", err)
} else {
fmt.Println("Transferência bem-sucedida!")
}
PrintBalances(db)
fmt.Println()
// Outra transferência normal
fmt.Println("=== Bob -> Alice Transferência 100 ===")
if err := Transfer(db, 2, 1, 100); err != nil {
fmt.Printf("Falha na transferência: %v\n", err)
} else {
fmt.Println("Transferência bem-sucedida!")
}
PrintBalances(db)
}
✓ Pool de conexões configurado:
Máximo de conexões: 10
Máximo inativas: 5
Tempo de vida da conexão: 5 minutos
=== Estado Inicial ===
Saldos das contas:
[1] Alice Saldo: 1000.00
[2] Bob Saldo: 500.00
=== Alice -> Bob Transferência 300 ===
Transferência bem-sucedida!
Saldos das contas:
[1] Alice Saldo: 700.00
[2] Bob Saldo: 800.00
=== Bob -> Alice Transferência 900 ===
Falha na transferência: saldo insuficiente: atual 800.00, necessário 900.00
Saldos das contas:
[1] Alice Saldo: 700.00
[2] Bob Saldo: 800.00
=== Bob -> Alice Transferência 100 ===
Transferência bem-sucedida!
Saldos das contas:
[1] Alice Saldo: 800.00
[2] Bob Saldo: 700.00
Cenários do Mundo Real
Cenário 1: Registro e Login de Usuário (MySQL)
Usa MySQL para armazenar informações do usuário, implementando registro e verificação de senha.
package main
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql" // Driver MySQL
)
// hashPassword hash simples de senha (em produção deve usar bcrypt)
func hashPassword(password string) string {
h := sha256.Sum256([]byte(password))
return hex.EncodeToString(h[:])
}
// UserRepository repositório de usuários
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(dsn string) (*UserRepository, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// Configurar pool de conexões
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(10 * time.Minute)
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("falha na conexão com banco de dados: %w", err)
}
return &UserRepository{db: db}, nil
}
func (r *UserRepository) Close() error {
return r.db.Close()
}
// Register registra um novo usuário
func (r *UserRepository) Register(username, password, email string) (int64, error) {
hashedPwd := hashPassword(password)
result, err := r.db.Exec(
"INSERT INTO users (username, password_hash, email, created_at) VALUES (?, ?, ?, ?)",
username, hashedPwd, email, time.Now(),
)
if err != nil {
return 0, fmt.Errorf("falha no registro: %w", err)
}
return result.LastInsertId()
}
// Login verifica o login do usuário
func (r *UserRepository) Login(username, password string) (int, error) {
hashedPwd := hashPassword(password)
var id int
var storedHash string
err := r.db.QueryRow(
"SELECT id, password_hash FROM users WHERE username = ?",
username,
).Scan(&id, &storedHash)
if err == sql.ErrNoRows {
return 0, fmt.Errorf("usuário não existe")
}
if err != nil {
return 0, fmt.Errorf("falha na consulta: %w", err)
}
if storedHash != hashedPwd {
return 0, fmt.Errorf("senha incorreta")
}
// Atualizar último login
_, _ = r.db.Exec(
"UPDATE users SET last_login = ? WHERE id = ?",
time.Now(), id,
)
return id, nil
}
func main() {
// Formato DSN: username:password@tcp(host:port)/dbname?parseTime=true
dsn := "root:password@tcp(127.0.0.1:3306)/myapp?parseTime=true"
repo, err := NewUserRepository(dsn)
if err != nil {
log.Fatal("Falha na inicialização:", err)
}
defer repo.Close()
// Registrar
id, err := repo.Register("alice", "mypassword123", "alice@example.com")
if err != nil {
log.Fatal("Falha no registro:", err)
}
fmt.Printf("Registro bem-sucedido, ID do usuário: %d\n", id)
// Login
userID, err := repo.Login("alice", "mypassword123")
if err != nil {
fmt.Printf("Falha no login: %v\n", err)
} else {
fmt.Printf("Login bem-sucedido, ID do usuário: %d\n", userID)
}
// Senha errada
_, err = repo.Login("alice", "wrongpassword")
if err != nil {
fmt.Printf("Falha no login: %v\n", err)
}
}
Cenário 2: Consultas Paginadas e Inserção em Lote
Lida eficientemente com consultas paginadas e inserções em lote para grandes quantidades de dados.
package main
import (
"database/sql"
"fmt"
"log"
"strings"
_ "github.com/mattn/go-sqlite3"
)
// Product produto
type Product struct {
ID int
Name string
Price float64
Stock int
}
// PageResult resultado da paginação
type PageResult struct {
Items []Product
Total int
Page int
PageSize int
TotalPages int
}
// ProductStore armazenamento de produtos
type ProductStore struct {
db *sql.DB
}
func NewProductStore(db *sql.DB) (*ProductStore, error) {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER DEFAULT 0
)
`)
if err != nil {
return nil, err
}
return &ProductStore{db: db}, nil
}
// BatchInsert inserção em lote (usa transação para melhor desempenho)
func (s *ProductStore) BatchInsert(products []Product) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
stmt, err := tx.Prepare("INSERT INTO products (name, price, stock) VALUES (?, ?, ?)")
if err != nil {
tx.Rollback()
return err
}
defer stmt.Close()
for _, p := range products {
_, err := stmt.Exec(p.Name, p.Price, p.Stock)
if err != nil {
tx.Rollback()
return fmt.Errorf("falha ao inserir %s: %w", p.Name, err)
}
}
return tx.Commit()
}
// Paginate consulta paginada
func (s *ProductStore) Paginate(page, pageSize int) (*PageResult, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
// Consultar contagem total
var total int
err := s.db.QueryRow("SELECT COUNT(*) FROM products").Scan(&total)
if err != nil {
return nil, err
}
// Calcular total de páginas
totalPages := total / pageSize
if total%pageSize > 0 {
totalPages++
}
// Consultar dados da página atual
offset := (page - 1) * pageSize
rows, err := s.db.Query(
"SELECT id, name, price, stock FROM products ORDER BY id LIMIT ? OFFSET ?",
pageSize, offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock); err != nil {
return nil, err
}
items = append(items, p)
}
return &PageResult{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// Search busca difusa por nome
func (s *ProductStore) Search(keyword string, page, pageSize int) (*PageResult, error) {
likePattern := "%" + keyword + "%"
var total int
err := s.db.QueryRow(
"SELECT COUNT(*) FROM products WHERE name LIKE ?", likePattern,
).Scan(&total)
if err != nil {
return nil, err
}
totalPages := total / pageSize
if total%pageSize > 0 {
totalPages++
}
offset := (page - 1) * pageSize
rows, err := s.db.Query(
"SELECT id, name, price, stock FROM products WHERE name LIKE ? ORDER BY id LIMIT ? OFFSET ?",
likePattern, pageSize, offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock); err != nil {
return nil, err
}
items = append(items, p)
}
return &PageResult{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
func printPage(result *PageResult) {
fmt.Printf(" Página %d/%d (total %d itens)\n",
result.Page, result.TotalPages, result.Total)
for _, p := range result.Items {
fmt.Printf(" [%d] %-10s Preço: %6.2f Estoque: %d\n",
p.ID, p.Name, p.Price, p.Stock)
}
}
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
store, err := NewProductStore(db)
if err != nil {
log.Fatal(err)
}
// Inserir 50 produtos em lote
var products []Product
names := []string{"Teclado", "Mouse", "Monitor", "Fone", "Caixa de Som",
"Webcam", "Pen Drive", "HD", "RAM", "Placa Mãe"}
for i := 0; i < 50; i++ {
products = append(products, Product{
Name: fmt.Sprintf("%s-%02d", names[i%len(names)], i+1),
Price: float64(50 + i*10),
Stock: 100 - i,
})
}
if err := store.BatchInsert(products); err != nil {
log.Fatal("Falha na inserção em lote:", err)
}
fmt.Printf("✓ %d produtos inseridos em lote com sucesso\n\n", len(products))
// Consulta paginada
fmt.Println("=== Consulta Paginada (10 por página) ===")
for page := 1; page <= 3; page++ {
result, err := store.Paginate(page, 10)
if err != nil {
log.Fatal(err)
}
printPage(result)
fmt.Println()
}
// Busca
fmt.Println("=== Busca 'Teclado' ===")
result, err := store.Search("Teclado", 1, 10)
if err != nil {
log.Fatal(err)
}
printPage(result)
}
✓ 50 produtos inseridos em lote com sucesso
=== Consulta Paginada (10 por página) ===
Página 1/5 (total 50 itens)
[1] Teclado-01 Preço: 50.00 Estoque: 100
[2] Mouse-02 Preço: 60.00 Estoque: 99
...
=== Busca 'Teclado' ===
Página 1/1 (total 5 itens)
[1] Teclado-01 Preço: 50.00 Estoque: 100
[11] Teclado-11 Preço: 150.00 Estoque: 90
[21] Teclado-21 Preço: 250.00 Estoque: 80
[31] Teclado-31 Preço: 350.00 Estoque: 70
[41] Teclado-41 Preço: 450.00 Estoque: 60
❓ Perguntas Frequentes
P1: Por que sql.Open() reporta unknown driver?
O driver não foi importado corretamente. Certifique-se de usar importação anônima:
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // Esta linha é obrigatória
)
_ significa apenas executar a função init() do pacote (registrando o driver), sem referenciar diretamente o nome do pacote. Esquecer de importar o driver é o erro mais comum de iniciante.
P2: Por que QueryRow().Scan() reporta erro quando não há dados?
Quando uma consulta não tem linhas correspondentes, Scan() retorna sql.ErrNoRows. Precisa ser tratado separadamente:
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 999).Scan(&name)
if err == sql.ErrNoRows {
fmt.Println("Usuário não existe")
} else if err != nil {
log.Fatal("Erro na consulta:", err)
}
P3: Quando db.Close() deve ser chamado?
sql.DB é um pool de conexões e deve permanecer aberto durante todo o ciclo de vida do programa, não fechado após cada operação. Tipicamente use defer db.Close() no início da função main():
func main() {
db, err := sql.Open("sqlite3", "./mydb.db")
if err != nil {
log.Fatal(err)
}
defer db.Close() // Fechado apenas quando o programa sai
// ... usar db para várias operações
}
P4: Como prevenir injeção de SQL?
Nunca use concatenação de strings para construir instruções SQL. Sempre use consultas parametrizadas:
// ❌ Perigoso! Risco de injeção SQL
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput)
// ✓ Seguro: usar placeholders de parâmetros
db.Query("SELECT * FROM users WHERE name = ?", userInput)
O placeholder ? é automaticamente escapado pelo driver, e o banco de dados o trata como dados puros em vez de código SQL.
📖 Resumo
Nesta lição aprendemos:
| Tópico | Pontos-Chave |
|---|---|
database/sql |
Interface unificada da biblioteca padrão Go, suporta qualquer driver de banco de dados |
| Importação de driver | Deve usar importação anônima _, apenas registra o driver |
sql.Open |
Cria pool de conexões, não conecta imediatamente; use Ping() para verificar |
| CRUD | Exec para operações de escrita, Query/QueryRow para leitura de dados |
| Instruções preparadas | Prepare melhora o desempenho, previne injeção SQL |
| Transações | Begin→Exec→Commit/Rollback, garante atomicidade |
| Pool de conexões | SetMaxOpenConns/SetMaxIdleConns e outros métodos de ajuste |
Scan |
Mapeia colunas do banco de dados para variáveis Go |
sql.ErrNoRows |
Erro especial quando a consulta não retorna resultados, precisa de tratamento separado |
Com essas habilidades, você tem a capacidade completa de operar bancos de dados relacionais em projetos Go.
📝 Exercícios
Exercício 1: CRUD Básico (Aquecimento)
Crie um banco de dados SQLite, crie uma tabela books (campos: id, title, author, price, published_year), e complete as seguintes operações:
- Inserir pelo menos 5 livros
- Consultar livros com preço maior que 50
- Atualizar o preço de um livro
- Excluir livros publicados antes de 2000
Exercício 2: Transações e Tratamento de Erros
Baseado no Exercício 1, implemente uma função TransferBook: transfira todos os livros de um autor para outro (use transações para garantir atomicidade). Requisitos:
- Se o autor original não existir, reverta e retorne um erro
- Após atualização bem-sucedida, imprima o número de linhas afetadas
- Use
defer+recoverpara prevenir que panics causem pendurações de transação
Exercício 3: Desafio Completo — Sistema Mini de Gerenciamento de Livros
Construa um sistema de gerenciamento de livros de linha de comando com os seguintes requisitos:
- Suportar operações CRUD e lista paginada
- Usar padrão DAO para encapsular operações de banco de dados
- Suportar busca difusa por título e filtragem por faixa de preço
- Usar instruções preparadas para otimizar consultas repetidas
- Configurar parâmetros adequados do pool de conexões
- Usar tratamento de erros estruturado em vez de
log.Fatal
Próxima Lição: LiÇÃO 28: Implantação do Projeto — Aprenda como compilar, empacotar e implantar projetos Go em servidores.



