Go

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

14 min de leitura

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

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

<h2>Fundamentos do Package database/sql</h2>

<p>O package <code>database/sql</code> é 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.</p>

<p>A filosofia do <code>database/sql</code> é 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 <code>github.com/lib/pq</code> (PostgreSQL), <code>github.com/go-sql-driver/mysql</code> (MySQL) e <code>github.com/mattn/go-sqlite3</code> (SQLite).</p>

<h3>Instalação e Setup Inicial</h3>

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

<pre><code class="language-bash">go get github.com/lib/pq</code></pre>

<p>Agora, configure a conexão básica:</p>

<pre><code class="language-go">package main

import (

&quot;database/sql&quot;

&quot;log&quot;

_ &quot;github.com/lib/pq&quot;

)

func main() {

// String de conexão: user=username password=secret dbname=mydb host=localhost port=5432 sslmode=disable

dsn := &quot;user=postgres password=yourpassword dbname=testdb host=localhost port=5432 sslmode=disable&quot;

db, err := sql.Open(&quot;postgres&quot;, dsn)

if err != nil {

log.Fatalf(&quot;Erro ao abrir conexão: %v&quot;, err)

}

defer db.Close()

// Verifica se a conexão está realmente funcionando

err = db.Ping()

if err != nil {

log.Fatalf(&quot;Erro ao fazer ping no banco: %v&quot;, err)

}

log.Println(&quot;Conexão estabelecida com sucesso!&quot;)

}</code></pre>

<p>Preste atenção em um detalhe crucial: <code>sql.Open()</code> não testa a conexão imediatamente. Ele apenas cria um pool de conexões. Use <code>Ping()</code> 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.</p>

<h3>Pool de Conexões e Configuração</h3>

<p>O <code>database/sql</code> 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:</p>

<pre><code class="language-go">func setupDB(dsn string) (*sql.DB, error) {

db, err := sql.Open(&quot;postgres&quot;, 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

}</code></pre>

<p>Essas configurações dependem do seu workload. Uma API web com muitas requisições simultâneas pode usar <code>MaxOpenConns</code> 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.</p>

<h2>Executando Queries e Scanning de Resultados</h2>

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

<h3>Query para Múltiplas Linhas</h3>

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

<pre><code class="language-go">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 &gt; $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(&amp;user.ID, &amp;user.Name, &amp;user.Email, &amp;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

}</code></pre>

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

<h3>QueryRow para Uma Única Linha</h3>

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

<pre><code class="language-go">func getUserByID(db sql.DB, id int) (User, error) {

user := &amp;User{}

query := SELECT id, name, email, age FROM users WHERE id = $1

err := db.QueryRow(query, id).Scan(&amp;user.ID, &amp;user.Name, &amp;user.Email, &amp;user.Age)

if err != nil {

if err == sql.ErrNoRows {

return nil, fmt.Errorf(&quot;usuário não encontrado&quot;)

}

return nil, err

}

return user, nil

}</code></pre>

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

<h3>Exec para Operações sem Retorno</h3>

<p>Para INSERT, UPDATE e DELETE, use <code>Exec()</code>:</p>

<pre><code class="language-go">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(&amp;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(&quot;nenhuma linha foi atualizada&quot;)

}

return nil

}

func deleteUser(db *sql.DB, id int) error {

query := DELETE FROM users WHERE id = $1

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

return err

}</code></pre>

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

<h2>Transações e Prepared Statements</h2>

<p>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.</p>

<h3>Transações com Rollback e Commit</h3>

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

<pre><code class="language-go">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

}</code></pre>

<p>Se qualquer <code>Exec()</code> falhar, o <code>defer</code> 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.</p>

<h3>Prepared Statements para Queries Repetidas</h3>

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

<pre><code class="language-go">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

}</code></pre>

<p>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.</p>

<h2>Boas Práticas e Tratamento de Erros</h2>

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

<h3>Sempre Feche Recursos</h3>

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

<pre><code class="language-go"></code></pre>

<h3>Use Context para Timeouts</h3>

<p>Em APIs e sistemas com deadline, passe <code>context</code> para queries:</p>

<pre><code class="language-go">func getUserWithTimeout(db sql.DB, id int, timeout time.Duration) (User, error) {

ctx, cancel := context.WithTimeout(context.Background(), timeout)

defer cancel()

user := &amp;User{}

query := SELECT id, name, email, age FROM users WHERE id = $1

err := db.QueryRowContext(ctx, query, id).Scan(&amp;user.ID, &amp;user.Name, &amp;user.Email, &amp;user.Age)

if err != nil {

return nil, err

}

return user, nil

}</code></pre>

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

<h3>Validação de Parâmetros</h3>

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

<pre><code class="language-go"></code></pre>

<h3>Logging e Monitoramento</h3>

<p>Log queries lentas e erros de conexão:</p>

<pre><code class="language-go">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 &gt; 100*time.Millisecond {

log.Printf(&quot;Query lenta (%v): %s&quot;, duration, query)

}

if err != nil {

log.Printf(&quot;Erro na query: %v&quot;, err)

}

return result, err

}</code></pre>

<h3>Tratamento Específico de Erros</h3>

<p>Diferentes erros demandam diferentes ações:</p>

<pre><code class="language-go">func safeQueryUser(db sql.DB, id int) (User, error) {

user := &amp;User{}

err := db.QueryRow(&quot;SELECT id, name FROM users WHERE id = $1&quot;, id).

Scan(&amp;user.ID, &amp;user.Name)

if err != nil {

if err == sql.ErrNoRows {

return nil, fmt.Errorf(&quot;usuário não existe&quot;)

}

if strings.Contains(err.Error(), &quot;connection refused&quot;) {

return nil, fmt.Errorf(&quot;banco de dados indisponível&quot;)

}

return nil, fmt.Errorf(&quot;erro inesperado: %w&quot;, err)

}

return user, nil

}</code></pre>

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

<h2>Conclusão</h2>

<p>Você aprendeu que o <code>database/sql</code> é 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 — <code>Query()</code>, <code>QueryRow()</code> e <code>Exec()</code> — 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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://pkg.go.dev/database/sql" target="_blank" rel="noopener noreferrer">The Go Programming Language - database/sql</a></li>

<li><a href="https://pkg.go.dev/github.com/lib/pq" target="_blank" rel="noopener noreferrer">PostgreSQL Driver for Go</a></li>

<li><a href="https://go.dev/doc/database/sql-insights" target="_blank" rel="noopener noreferrer">Using the database/sql Package in Go</a></li>

<li><a href="https://www.apress.com/gp/book/9781484268384" target="_blank" rel="noopener noreferrer">Go Web Development with Gorilla</a></li>

<li><a href="https://go.dev/doc/effective_go#errors" target="_blank" rel="noopener noreferrer">Effective Go - Error Handling</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Guia Completo de Pacote time em Go: Datas, Durações, Timers e Tickers
Guia Completo de Pacote time em Go: Datas, Durações, Timers e Tickers

Introdução ao Pacote time em Go O pacote é um dos pilares fundamentais da pro...

O que Todo Dev Deve Saber sobre Documentação de APIs Go com Swagger e swaggo
O que Todo Dev Deve Saber sobre Documentação de APIs Go com Swagger e swaggo

Entendendo APIs e a Importância da Documentação Uma API (Application Programm...

O que Todo Dev Deve Saber sobre Type Switch em Go: Discriminando Tipos em Tempo de Execução
O que Todo Dev Deve Saber sobre Type Switch em Go: Discriminando Tipos em Tempo de Execução

O que é Type Switch e Por que Usar Type switch é um mecanismo em Go que permi...