Bem-vindo à aula sobre consultas e prepared statements em Go! Nesta etapa do curso, você vai dominar a interação com bancos de dados relacionais usando o pacote padrão database/sql. Vamos explorar como executar consultas, ler resultados e, principalmente, como fazê-lo de forma segura e eficiente.

O pacote database/sql fornece uma interface genérica para SQL em Go, funcionando com diversos drivers (PostgreSQL, MySQL, SQLite, etc.). A chave para escrever código robusto é entender as diferenças entre os métodos Query e QueryRow, como o Scan funciona e por que prepared statements são essenciais.

Query vs QueryRow

No pacote database/sql, temos dois métodos principais para executar consultas que retornam dados: Query e QueryRow. Ambos são métodos do tipo *sql.DB (ou *sql.Tx) e retornam resultados, mas com propósitos distintos.

Query é usado quando você espera que a consulta retorne zero ou mais linhas. Ele retorna um *sql.Rows, que é um cursor sobre o conjunto de resultados. Você deve iterar sobre as linhas usando Next() e Scan() para extrair os valores. Já QueryRow é uma conveniência para consultas que retornam no máximo uma linha, como buscar um registro por ID ou fazer um COUNT. Ele retorna um *sql.Row, que não exige iteração: você chama Scan diretamente na linha retornada.

Exemplo prático:

// Usando Query para múltiplas linhas
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("ID: %d, Nome: %s\n", id, nome)
}
if err = rows.Err(); err != nil {
    log.Fatal(err)
}

// Usando QueryRow para uma única linha
var total int
err = db.QueryRow("SELECT COUNT(*) FROM usuarios").Scan(&total)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Total de usuários: %d\n", total)

Observe que com Query você precisa fechar as linhas (defer rows.Close()) para liberar a conexão, enquanto com QueryRow o fechamento é automático após o Scan.

Scan

O método Scan é responsável por copiar os valores da linha atual para as variáveis que você fornece como argumentos. Ele converte automaticamente os tipos SQL para tipos Go, sempre que possível. Por exemplo, colunas do tipo INTEGER podem ser escaneadas em int, int64, etc.; TEXT em string; BOOLEAN em bool.

É crucial passar ponteiros para as variáveis de destino, pois o Scan precisa escrever os valores nelas. A ordem dos argumentos deve corresponder à ordem das colunas no resultado da consulta.

Exemplo com vários tipos:

var (
    id        int
    nome      string
    email     string
    criadoEm  time.Time
    ativo     bool
)

err := db.QueryRow(
    "SELECT id, nome, email, criado_em, ativo FROM usuarios WHERE id = ?", 1,
).Scan(&id, &nome, &email, &criadoEm, &ativo)
if err != nil {
    if err == sql.ErrNoRows {
        fmt.Println("Nenhum usuário encontrado")
    } else {
        log.Fatal(err)
    }
}
fmt.Printf("ID: %d, Nome: %s, Email: %s, Criado em: %s, Ativo: %t\n", id, nome, email, criadoEm, ativo)

Se houver colunas que você não deseja escanear, basta usar sql.NullString, sql.NullInt64, etc., para lidar com valores nulos. Por exemplo, se uma coluna email pode ser nula, declare email sql.NullString e verifique email.Valid.

Prepared statements

Prepared statements (declarações preparadas) são uma técnica em que a consulta SQL é enviada ao banco de dados uma única vez, com placeholders para os parâmetros, e depois executada múltiplas vezes com diferentes valores. No Go, você pode criar um prepared statement usando db.Prepare() e depois executá-lo com Query, QueryRow ou Exec.

As vantagens são: segurança contra SQL injection, performance (o banco compila o plano de execução uma vez e reutiliza), e clareza no código. O driver substitui os placeholders (geralmente ? para MySQL/SQLite, $1, $2 para PostgreSQL) com os valores passados.

Exemplo:

stmt, err := db.Prepare("SELECT id, nome FROM usuarios WHERE ativo = ?")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

// Executa várias vezes com diferentes parâmetros
for _, ativo := range []bool{true, false} {
    rows, err := stmt.Query(ativo)
    if err != nil {
        log.Fatal(err)
    }
    // processar rows...
    rows.Close()
}

Você também pode usar prepared statements com Exec para comandos INSERT, UPDATE, DELETE. Note que o statement deve ser fechado com Close() quando não for mais necessário.

SQL injection

SQL injection é uma vulnerabilidade de segurança onde um atacante insere código SQL malicioso em uma consulta, manipulando os parâmetros. Isso pode levar à exfiltração de dados, modificação não autorizada ou até exclusão de tabelas. A forma mais comum de ocorrer é quando você concatena strings diretamente na construção da query.

Exemplo vulnerável:

// NUNCA faça isso!
id := r.URL.Query().Get("id")
query := fmt.Sprintf("SELECT * FROM usuarios WHERE id = %s", id)
rows, _ := db.Query(query)

Se o usuário passar 1 OR 1=1, a query vira SELECT * FROM usuarios WHERE id = 1 OR 1=1, retornando todos os registros. Ou pior, 1; DROP TABLE usuarios; --.

A solução é usar parameterized queries (consultas parametrizadas) com placeholders. Tanto db.Query quanto db.QueryRow, db.Exec e prepared statements aceitam parâmetros adicionais que são tratados de forma segura pelo driver: eles são escapados e nunca interpretados como SQL.

Exemplo seguro:

// Seguro: usa placeholder
id := r.URL.Query().Get("id")
row := db.QueryRow("SELECT * FROM usuarios WHERE id = ?", id)
// ... scan

Lembre-se: nunca concatene valores diretamente no SQL. Sempre utilize placeholders, independentemente da origem do dado (usuário, API, arquivo, etc.).

Boas práticas e observações finais

Além de usar prepared statements, considere estas boas práticas: sempre feche os recursos (rows, stmt) com defer para evitar vazamento de conexões; trate erros adequadamente, especialmente sql.ErrNoRows; use o padrão de repositório para isolar a lógica de acesso a dados; e configure o pool de conexões corretamente (SetMaxOpenConns, SetMaxIdleConns).

Outra dica: para consultas que são executadas muitas vezes, crie os prepared statements uma vez e reutilize-os, em vez de preparar a cada execução.

Referências

Exercícios

  1. Escreva uma função que receba um *sql.DB e um ID, e retorne o nome do usuário correspondente usando QueryRow e Scan. Trate o caso de nenhuma linha encontrada.
  2. ✓ Resposta:
    func getUserName(db *sql.DB, id int) (string, error) {
        var nome string
        err := db.QueryRow("SELECT nome FROM usuarios WHERE id = ?", id).Scan(&nome)
        if err != nil {
            if err == sql.ErrNoRows {
                return "", fmt.Errorf("usuário %d não encontrado", id)
            }
            return "", err
        }
        return nome, nil
    }
  3. Dado o seguinte código vulnerável, reescreva-o usando prepared statements:
  4. func getUser(db *sql.DB, id string) (*User, error) {
        query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", id)
        row := db.QueryRow(query)
        // ... scan
    }

    ✓ Resposta:
    stmt, err := db.Prepare("SELECT * FROM users WHERE id = ?")
    if err != nil {
        return nil, err
    }
    defer stmt.Close()
    
    var u User
    if err := stmt.QueryRow(id).Scan(&u.ID, &u.Nome, &u.Email); err != nil {
        return nil, err
    }
    return &u, nil
    
  5. Explique a diferença entre Query e QueryRow e quando usar cada um.
  6. ✓ Resposta: Query retorna um *sql.Rows que pode conter múltiplas linhas; você deve iterar com Next(). QueryRow é para consultas que retornam no máximo uma linha; ele retorna *sql.Row e você chama Scan diretamente. Use Query quando esperar várias linhas (ex.: listagem) e QueryRow quando esperar uma única linha (ex.: busca por chave primária, agregações).
  7. Escreva um exemplo de como usar um prepared statement para inserir múltiplos usuários em um loop, garantindo eficiência.
  8. ✓ Resposta:
    stmt, err := db.Prepare("INSERT INTO usuarios (nome, email) VALUES (?, ?)")
    if err != nil {
        log.Fatal(err)
    }
    defer stmt.Close()
    
    usuarios := []struct{ Nome, Email string }{
        {"Alice", "alice@example.com"},
        {"Bob", "bob@example.com"},
    }
    
    for _, u := range usuarios {
        if _, err := stmt.Exec(u.Nome, u.Email); err != nil {
            log.Fatal(err)
        }
    }
    
  9. Por que prepared statements ajudam a prevenir SQL injection? Explique o mecanismo.
  10. ✓ Resposta: Prepared statements separam a estrutura SQL dos dados. O SQL é enviado ao banco com placeholders, e os valores são passados separadamente. O driver escapa os valores ou usa protocolos binários, garantindo que eles sejam tratados como dados, não como parte do SQL. Assim, qualquer tentativa de injeção é neutralizada porque o conteúdo do parâmetro não é interpretado como código SQL.