Consultas e prepared statements
Nesta aula, você aprenderá a executar consultas SQL em Go usando database/sql, diferenciando Query e QueryRow, entendendo o processo de Scan, utilizando prepared statements para segurança e performance, e prevenindo SQL injection.
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)
// ... scanLembre-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
- Go database/sql - Querying
- Prepared statements no Go
- Evitando SQL injection
- Documentação oficial do pacote database/sql
- Como consultar dados com Go
- Como modificar dados com Go
Exercícios
- Escreva uma função que receba um
*sql.DBe um ID, e retorne o nome do usuário correspondente usandoQueryRoweScan. Trate o caso de nenhuma linha encontrada. - Dado o seguinte código vulnerável, reescreva-o usando prepared statements:
- Explique a diferença entre
QueryeQueryRowe quando usar cada um. - Escreva um exemplo de como usar um prepared statement para inserir múltiplos usuários em um loop, garantindo eficiência.
- Por que prepared statements ajudam a prevenir SQL injection? Explique o mecanismo.
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
}func getUser(db *sql.DB, id string) (*User, error) {
query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", id)
row := db.QueryRow(query)
// ... scan
}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
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).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)
}
}