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 —

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

GO
package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3" // Driver SQLite (importação anônima, apenas registra o driver)
)
💡 Dica: Drivers devem ser importados anonimamente com _, 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

GO
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)
}
💡 Dica: 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

GO
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

GO
// ---- 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

GO
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)
💡 Dica: Instruções preparadas tanto melhoram o desempenho (o banco de dados analisa o SQL apenas uma vez) quanto previnem injeção de SQL.

6. Tratamento de Transações

GO
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

GO
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
💡 Dica: A configuração adequada do pool de conexões pode melhorar significativamente o desempenho em cenários de alta concorrência. Definir 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.

GO
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")
}
▶ Experimente
TEXT
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.

GO
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)
    }
}
▶ Experimente
TEXT
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.

GO
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)
}
▶ Experimente
TEXT
✓ 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.

GO
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.

GO
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)
}
TEXT
✓ 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:

GO
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:

GO
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():

GO
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:

GO
// ❌ 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 BeginExecCommit/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:

  1. Inserir pelo menos 5 livros
  2. Consultar livros com preço maior que 50
  3. Atualizar o preço de um livro
  4. 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:

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:

  1. Suportar operações CRUD e lista paginada
  2. Usar padrão DAO para encapsular operações de banco de dados
  3. Suportar busca difusa por título e filtragem por faixa de preço
  4. Usar instruções preparadas para otimizar consultas repetidas
  5. Configurar parâmetros adequados do pool de conexões
  6. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%