database/sql Usage (Correct Patterns)
database/sql: Correct Usage Patterns in Production
The database/sql package is deceptively simple. Its API hides a connection pool and concurrency coordination layer. Misusing it leads to connection leaks, transaction inconsistencies, and hard-to-debug production incidents.
This lesson focuses on correct usage patterns — not just API calls.
Opening the Database
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
Important: sql.Open does not immediately connect to the database. It validates arguments and prepares the pool.
To verify connectivity:
if err := db.Ping(); err != nil {
log.Fatal(err)
}
Query vs QueryRow vs Exec
Query (Multiple Rows)
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil {
return err
}
defer rows.Close()
Always close rows. Not closing rows leaks connections.
QueryRow (Single Row)
row := db.QueryRowContext(ctx, "SELECT id FROM users WHERE id = $1", id)
var userID int
err := row.Scan(&userID)
if errors.Is(err, sql.ErrNoRows) {
return notFound
}
QueryRow does not require Close().
Exec (No Rows Returned)
res, err := db.ExecContext(ctx, "UPDATE users SET name = $1 WHERE id = $2", name, id)
Use Exec for INSERT/UPDATE/DELETE without result sets.
Rows Iteration Pattern
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return err
}
}
if err := rows.Err(); err != nil {
return err
}
Always check rows.Err() after iteration.
NULL Handling
Scanning NULL into non-pointer types causes errors.
Unsafe
var name string rows.Scan(&name)
Safe Using sql.NullString
var name sql.NullString
rows.Scan(&name)
if name.Valid {
fmt.Println(name.String)
}
Alternatively use pointers:
var name *string
Common Bug: Forgotten rows.Close()
If rows are not closed, the connection remains in use. Under load, pool exhaustion occurs.
Always:
defer rows.Close()
Context Is Mandatory
Always use Context variants.
db.QueryContext(ctx, ...) db.ExecContext(ctx, ...)
If request is canceled, DB query is interrupted.
Error Classification
sql.ErrNoRows
if errors.Is(err, sql.ErrNoRows) {
return notFound
}
Driver-Specific Errors
Inspect error types carefully for constraint violations.
Never compare error strings.
Prepared Statements
Single Use (Prefer This)
Let driver handle preparation internally.
db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
Explicit Prepare (Advanced Use)
stmt, err := db.PrepareContext(ctx, query)
if err != nil {
return err
}
defer stmt.Close()
Avoid preparing inside loops repeatedly.
Transactions
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
Always defer Rollback. Commit only if successful.
if err := tx.Commit(); err != nil {
return err
}
Never mix db.Query and tx.Query inside the same transaction scope.
Common Transaction Pitfalls
- Long-running transactions
- Performing network calls inside transaction
- Not handling commit errors
- Ignoring serialization conflicts
Scanning Performance
Scan is reflection-heavy. For high-volume reads:
- Select only required columns
- Avoid SELECT *
- Use indexes properly
Connection Leak Detection
stats := db.Stats() fmt.Println(stats.InUse)
If InUse remains high under low traffic, investigate leaks.
Graceful Shutdown Considerations
During shutdown:
- Stop accepting new HTTP requests
- Wait for active DB queries
- Call db.Close() last
Testing SQL Usage
Use integration tests with real DB when possible.
go test -run=Integration ./...
Mocking sql.DB rarely captures real behavior.
Common Anti-Patterns
- Ignoring rows.Err()
- Forgetting rows.Close()
- Using string concatenation for SQL (SQL injection risk)
- Long transactions spanning multiple layers
- Comparing error messages directly
Operational Checklist
- Context used everywhere
- rows.Close() always deferred
- rows.Err() checked
- NULL handled explicitly
- Transactions short-lived
- No SQL string concatenation
- Pool stats monitored
Final Perspective
database/sql is powerful but unforgiving when misused. Correct resource handling, explicit context usage, and disciplined transaction management separate stable production systems from those that degrade silently under load. Treat SQL interactions as critical resource operations — not just function calls.