Go Admin

Como Usar database/sql em Go: Conexão, Queries e Boas Práticas Nativas em Produção Já leu

Fundamentos do Package database/sql O package é a abstração padrão da linguagem Go para trabalhar com bancos de dados relacionais. Diferentemente de outras linguagens que oferecem múltiplas formas de acesso, Go padronizou uma interface uniforme que funciona com qualquer banco suportado por um driver específico. Isso significa que você aprende uma única API e consegue trocar entre PostgreSQL, MySQL, SQLite ou outro banco praticamente sem alterar seu código de aplicação. A filosofia do é ser uma camada fina e eficiente. Ela não abstrai completamente as peculiaridades do banco — você ainda escreve SQL — mas oferece gerenciamento de conexões, prepared statements, transações e tratamento de erros de forma segura e idiomática. Antes de começar, você precisa escolher um driver. Os mais comuns são (PostgreSQL), (MySQL) e (SQLite). Instalação e Setup Inicial Para começar, instale o driver do seu banco preferido. Vamos usar PostgreSQL como exemplo: Agora, configure a conexão básica: Preste atenção em um detalhe crucial: não testa a conexão

Fundamentos do Package database/sql

O package database/sql é a abstração padrão da linguagem Go para trabalhar com bancos de dados relacionais. Diferentemente de outras linguagens que oferecem múltiplas formas de acesso, Go padronizou uma interface uniforme que funciona com qualquer banco suportado por um driver específico. Isso significa que você aprende uma única API e consegue trocar entre PostgreSQL, MySQL, SQLite ou outro banco praticamente sem alterar seu código de aplicação.

A filosofia do database/sql é ser uma camada fina e eficiente. Ela não abstrai completamente as peculiaridades do banco — você ainda escreve SQL — mas oferece gerenciamento de conexões, prepared statements, transações e tratamento de erros de forma segura e idiomática. Antes de começar, você precisa escolher um driver. Os mais comuns são github.com/lib/pq (PostgreSQL), github.com/go-sql-driver/mysql (MySQL) e github.com/mattn/go-sqlite3 (SQLite).

Instalação e Setup Inicial

Para começar, instale o driver do seu banco preferido. Vamos usar PostgreSQL como exemplo:

go get github.com/lib/pq

Agora, configure a conexão básica:

package main

import (
    "database/sql"
    "log"

    _ "github.com/lib/pq"
)

func main() {
    // String de conexão: user=username password=secret dbname=mydb host=localhost port=5432 sslmode=disable
    dsn := "user=postgres password=yourpassword dbname=testdb host=localhost port=5432 sslmode=disable"

    db, err := sql.Open("postgres", dsn)
    if err != nil {
        log.Fatalf("Erro ao abrir conexão: %v", err)
    }
    defer db.Close()

    // Verifica se a conexão está realmente funcionando
    err = db.Ping()
    if err != nil {
        log.Fatalf("Erro ao fazer ping no banco: %v", err)
    }

    log.Println("Conexão estabelecida com sucesso!")
}

Preste atenção em um detalhe crucial: sql.Open() não testa a conexão imediatamente. Ele apenas cria um pool de conexões. Use Ping() para verificar se tudo está funcionando. Além disso, note o underscore antes do import do driver — isso é necessário porque o driver precisa ser registrado, mas você não usa diretamente seus símbolos exportados.

Pool de Conexões e Configuração

O database/sql mantém um pool de conexões reutilizáveis. Isso é muito mais eficiente do que abrir e fechar conexões a cada query. No entanto, você deve configurar o pool adequadamente para sua aplicação:

func setupDB(dsn string) (*sql.DB, error) {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, err
    }

    // Máximo de conexões abertas simultaneamente
    db.SetMaxOpenConns(25)

    // Máximo de conexões ociosas mantidas no pool
    db.SetMaxIdleConns(5)

    // Tempo máximo que uma conexão pode viver
    db.SetConnMaxLifetime(5 * time.Minute)

    // Tempo máximo que uma conexão pode ficar ociosa antes de ser fechada
    db.SetConnMaxIdleTime(10 * time.Minute)

    if err := db.Ping(); err != nil {
        return nil, err
    }

    return db, nil
}

Essas configurações dependem do seu workload. Uma API web com muitas requisições simultâneas pode usar MaxOpenConns maior (25-100), enquanto uma aplicação de background simples pode usar 5-10. A regra prática: comece conservador e monitore o uso antes de aumentar.

Executando Queries e Scanning de Resultados

Existem três padrões principais para executar queries em Go: Query() para múltiplas linhas, QueryRow() para uma única linha, e Exec() para operações que não retornam dados (INSERT, UPDATE, DELETE).

Query para Múltiplas Linhas

Quando você espera vários registros, use Query(). Ele retorna um Rows que você itera:

type User struct {
    ID    int
    Name  string
    Email string
    Age   int
}

func getUsers(db *sql.DB) ([]User, error) {
    query := `SELECT id, name, email, age FROM users WHERE age > $1`
    rows, err := db.Query(query, 18)
    if err != nil {
        return nil, err
    }
    defer rows.Close() // CRUCIAL: sempre feche rows

    var users []User

    for rows.Next() {
        var user User
        err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
        if err != nil {
            return nil, err
        }
        users = append(users, user)
    }

    // Sempre verifique erro após o loop
    if err = rows.Err(); err != nil {
        return nil, err
    }

    return users, nil
}

Note que Scan() mapeia as colunas na ordem exata da query. Se você selecionar name, email, id, precisa passar para Scan() nessa ordem. Fechar rows é obrigatório — caso contrário, a conexão não retorna ao pool.

QueryRow para Uma Única Linha

Quando você sabe que a query retornará apenas um registro (ou nenhum), use QueryRow():

func getUserByID(db *sql.DB, id int) (*User, error) {
    user := &User{}
    query := `SELECT id, name, email, age FROM users WHERE id = $1`

    err := db.QueryRow(query, id).Scan(&user.ID, &user.Name, &user.Email, &user.Age)
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("usuário não encontrado")
        }
        return nil, err
    }

    return user, nil
}

QueryRow() é mais direto que Query() quando você sabe o resultado tem no máximo uma linha. O erro sql.ErrNoRows é especial — significa que a query foi executada, mas nenhuma linha foi encontrada. Trate isso explicitamente.

Exec para Operações sem Retorno

Para INSERT, UPDATE e DELETE, use Exec():

func createUser(db *sql.DB, name, email string, age int) (int64, error) {
    query := `INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING id`

    var id int64
    err := db.QueryRow(query, name, email, age).Scan(&id)
    if err != nil {
        return 0, err
    }

    return id, nil
}

func updateUser(db *sql.DB, id int, name string) error {
    query := `UPDATE users SET name = $1 WHERE id = $2`

    result, err := db.Exec(query, name, id)
    if err != nil {
        return err
    }

    rowsAffected, err := result.RowsAffected()
    if err != nil {
        return err
    }

    if rowsAffected == 0 {
        return fmt.Errorf("nenhuma linha foi atualizada")
    }

    return nil
}

func deleteUser(db *sql.DB, id int) error {
    query := `DELETE FROM users WHERE id = $1`

    _, err := db.Exec(query, id)
    return err
}

Observe que Exec() retorna um Result que oferece RowsAffected() e LastInsertId(). O LastInsertId() nem sempre funciona em todos os bancos — PostgreSQL, por exemplo, não o suporta diretamente. Use RETURNING em PostgreSQL como mostrado acima.

Transações e Prepared Statements

Transações garantem que múltiplas operações ocorrem atomicamente — ou todas são executadas, ou nenhuma é. Prepared statements protegem contra SQL injection e melhoram performance quando você executa a mesma query várias vezes.

Transações com Rollback e Commit

Uma transação agrupa múltiplas operações que devem suceder ou falhar juntas:

func transferMoney(db *sql.DB, fromID, toID int, amount decimal.Decimal) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }

    // Se algo der errado, faz rollback automaticamente
    defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()

    // Subtrai da conta origem
    query1 := `UPDATE accounts SET balance = balance - $1 WHERE id = $2`
    _, err = tx.Exec(query1, amount, fromID)
    if err != nil {
        return err
    }

    // Adiciona na conta destino
    query2 := `UPDATE accounts SET balance = balance + $1 WHERE id = $2`
    _, err = tx.Exec(query2, amount, toID)
    if err != nil {
        return err
    }

    // Insere registro de auditoria
    query3 := `INSERT INTO transactions (from_id, to_id, amount) VALUES ($1, $2, $3)`
    _, err = tx.Exec(query3, fromID, toID, amount)
    if err != nil {
        return err
    }

    // Se tudo correu bem, commit
    if err = tx.Commit(); err != nil {
        return err
    }

    return nil
}

Se qualquer Exec() falhar, o defer captura o erro e faz rollback. Sem uma transação, se a transferência de débito funcionasse mas o crédito falhasse, a conta perderia dinheiro.

Prepared Statements para Queries Repetidas

Se você executa a mesma query múltiplas vezes (com parâmetros diferentes), prepare-a uma vez:

func insertManyUsers(db *sql.DB, users []User) error {
    stmt, err := db.Prepare(`INSERT INTO users (name, email, age) VALUES ($1, $2, $3)`)
    if err != nil {
        return err
    }
    defer stmt.Close()

    for _, user := range users {
        _, err := stmt.Exec(user.Name, user.Email, user.Age)
        if err != nil {
            return err
        }
    }

    return nil
}

Prepared statements são compilados uma única vez e reutilizados. Isso é mais rápido e seguro. Use-os sempre que executar a mesma query múltiplas vezes em um loop.

Boas Práticas e Tratamento de Erros

A qualidade do seu código Go depende tanto das patterns que você segue quanto do código em si.

Sempre Feche Recursos

Toda operação que abre um recurso (rows, stmt, tx) deve fechá-lo, mesmo que haja erro:

// ❌ ERRADO - rows nunca é fechado se houver erro
rows, err := db.Query("SELECT * FROM users")
if err != nil {
    return nil, err // rows.Close() não é chamado
}

// ✅ CORRETO - defer garante fechamento
rows, err := db.Query("SELECT * FROM users")
if err != nil {
    return nil, err
}
defer rows.Close()

Use Context para Timeouts

Em APIs e sistemas com deadline, passe context para queries:

func getUserWithTimeout(db *sql.DB, id int, timeout time.Duration) (*User, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    user := &User{}
    query := `SELECT id, name, email, age FROM users WHERE id = $1`

    err := db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name, &user.Email, &user.Age)
    if err != nil {
        return nil, err
    }

    return user, nil
}

Métodos com Context (QueryContext, ExecContext, QueryRowContext) respeitam cancelamento e timeout. Use sempre em APIs web — isso evita que queries lentas travam sua aplicação.

Validação de Parâmetros

SQL injection ocorre quando entrada de usuário é concatenada na query. Use placeholders ($1, $2 no PostgreSQL, ? no MySQL):

// ❌ NUNCA FAÇA ISSO
name := getUserInput()
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name)
db.Query(query)

// ✅ CORRETO - o driver escapa valores automaticamente
name := getUserInput()
db.Query("SELECT * FROM users WHERE name = $1", name)

Logging e Monitoramento

Log queries lentas e erros de conexão:

func execWithLogging(db *sql.DB, query string, args ...interface{}) (sql.Result, error) {
    start := time.Now()

    result, err := db.Exec(query, args...)

    duration := time.Since(start)
    if duration > 100*time.Millisecond {
        log.Printf("Query lenta (%v): %s", duration, query)
    }

    if err != nil {
        log.Printf("Erro na query: %v", err)
    }

    return result, err
}

Tratamento Específico de Erros

Diferentes erros demandam diferentes ações:

func safeQueryUser(db *sql.DB, id int) (*User, error) {
    user := &User{}
    err := db.QueryRow("SELECT id, name FROM users WHERE id = $1", id).
        Scan(&user.ID, &user.Name)

    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("usuário não existe")
        }
        if strings.Contains(err.Error(), "connection refused") {
            return nil, fmt.Errorf("banco de dados indisponível")
        }
        return nil, fmt.Errorf("erro inesperado: %w", err)
    }

    return user, nil
}

Use errors.Is() para erros específicos do driver quando disponível. Sempre envolva erros com contexto usando %w no fmt.Errorf().

Conclusão

Você aprendeu que o database/sql é a interface padrão e unificada do Go para qualquer banco relacional, abstraindo complexity enquanto mantém você no controle do SQL. As três operações principais — Query(), QueryRow() e Exec() — cobrem 99% dos casos, e entender quando usar cada uma é fundamental. Por fim, transações, prepared statements e context são ferramentas que transformam código funcional em código profissional: atômico, seguro e responsivo.

Referências


Artigos relacionados