Work with Databases
Problem
Database integration in Go requires careful connection management, transaction handling, and proper error handling. The standard library is low-level, leading to repetitive boilerplate code.
// Problematic approach - connection leak
db, _ := sql.Open("postgres", connStr)
rows, _ := db.Query("SELECT * FROM users")
// Connection never closed, no error handlingThis guide shows practical techniques for robust database integration in Go applications.
Solution
1. Database Connection Setup
Use database/sql with proper connection pooling.
Basic connection:
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/lib/pq" // PostgreSQL driver
)
type User struct {
ID int
Username string
Email string
}
func setupDatabase() (*sql.DB, error) {
connStr := "host=localhost port=5432 user=myuser password=mypass dbname=mydb sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Configure connection pool
db.SetMaxOpenConns(25) // Maximum open connections
db.SetMaxIdleConns(5) // Maximum idle connections
db.SetConnMaxLifetime(5 * time.Minute) // Connection lifetime
db.SetConnMaxIdleTime(10 * time.Minute) // Idle connection timeout
// Verify connection
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
// Query single row
func getUserByID(db *sql.DB, id int) (*User, error) {
query := "SELECT id, username, email FROM users WHERE id = $1"
var user User
err := db.QueryRow(query, id).Scan(&user.ID, &user.Username, &user.Email)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user not found: %d", id)
}
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
return &user, nil
}
// Query multiple rows
func getAllUsers(db *sql.DB) ([]User, error) {
query := "SELECT id, username, email FROM users"
rows, err := db.Query(query)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Username, &user.Email); err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows error: %w", err)
}
return users, nil
}
// Insert
func createUser(db *sql.DB, username, email string) (int, error) {
query := "INSERT INTO users (username, email) VALUES ($1, $2) RETURNING id"
var id int
err := db.QueryRow(query, username, email).Scan(&id)
if err != nil {
return 0, fmt.Errorf("insert failed: %w", err)
}
return id, nil
}2. Transaction Handling
Manage transactions for data consistency.
Transaction basics:
func transferFunds(db *sql.DB, fromID, toID int, amount float64) error {
// Begin transaction
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback() // Rollback if not committed
// Debit from source account
debitQuery := "UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1"
result, err := tx.Exec(debitQuery, amount, fromID)
if err != nil {
return fmt.Errorf("debit failed: %w", err)
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("insufficient funds or account not found")
}
// Credit to destination account
creditQuery := "UPDATE accounts SET balance = balance + $1 WHERE id = $2"
_, err = tx.Exec(creditQuery, amount, toID)
if err != nil {
return fmt.Errorf("credit failed: %w", err)
}
// Commit transaction
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit failed: %w", err)
}
return nil
}
// Transaction with context
func transferFundsWithContext(ctx context.Context, db *sql.DB, fromID, toID int, amount float64) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
return err
}
defer tx.Rollback()
// Operations with context
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, toID)
if err != nil {
return err
}
return tx.Commit()
}3. Using sqlx for Simplicity
sqlx extends database/sql with struct scanning.
sqlx basics:
import "github.com/jmoiron/sqlx"
func setupSqlx() (*sqlx.DB, error) {
db, err := sqlx.Connect("postgres", connStr)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
return db, nil
}
type User struct {
ID int `db:"id"`
Username string `db:"username"`
Email string `db:"email"`
}
// Get single row
func getUserSqlx(db *sqlx.DB, id int) (*User, error) {
var user User
err := db.Get(&user, "SELECT id, username, email FROM users WHERE id = $1", id)
if err != nil {
return nil, err
}
return &user, nil
}
// Get multiple rows
func getAllUsersSqlx(db *sqlx.DB) ([]User, error) {
var users []User
err := db.Select(&users, "SELECT id, username, email FROM users")
if err != nil {
return nil, err
}
return users, nil
}
// Named queries
func getUserByEmailSqlx(db *sqlx.DB, email string) (*User, error) {
var user User
query := "SELECT * FROM users WHERE email = :email"
rows, err := db.NamedQuery(query, map[string]interface{}{"email": email})
if err != nil {
return nil, err
}
defer rows.Close()
if rows.Next() {
if err := rows.StructScan(&user); err != nil {
return nil, err
}
}
return &user, nil
}4. Prepared Statements
Improve performance with prepared statements.
Prepared statements:
func batchInsert(db *sql.DB, users []User) error {
stmt, err := db.Prepare("INSERT INTO users (username, email) VALUES ($1, $2)")
if err != nil {
return err
}
defer stmt.Close()
for _, user := range users {
_, err := stmt.Exec(user.Username, user.Email)
if err != nil {
return err
}
}
return nil
}
// Prepared statement with transaction
func batchInsertTx(db *sql.DB, users []User) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO users (username, email) VALUES ($1, $2)")
if err != nil {
return err
}
defer stmt.Close()
for _, user := range users {
_, err := stmt.Exec(user.Username, user.Email)
if err != nil {
return err
}
}
return tx.Commit()
}How It Works
Database Connection Lifecycle
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[sql.Open] --> B[Connection Pool Created]
B --> C{Query Requested}
C -->|Available Conn| D[Use Existing Connection]
C -->|No Conn| E{Pool at Max?}
E -->|No| F[Create New Connection]
E -->|Yes| G[Wait for Connection]
F --> D
G --> D
D --> H[Execute Query]
H --> I[Return Connection to Pool]
I --> J{Idle Timeout?}
J -->|Yes| K[Close Connection]
J -->|No| L[Keep in Pool]
style A fill:#0173B2,stroke:#000000,color:#FFFFFF
style B fill:#DE8F05,stroke:#000000,color:#FFFFFF
style C fill:#029E73,stroke:#000000,color:#FFFFFF
style H fill:#CC78BC,stroke:#000000,color:#FFFFFF
Key concepts:
- Connection Pool: Reusable database connections managed by database/sql
- Prepared Statements: Pre-compiled SQL for performance and SQL injection prevention
- Transactions: ACID guarantees for data consistency
- Context: Timeout and cancellation support for queries
Variations
Connection Health Checks
func healthCheck(db *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return fmt.Errorf("database unhealthy: %w", err)
}
return nil
}Batch Operations
func batchInsertEfficient(db *sql.DB, users []User) error {
valueStrings := []string{}
valueArgs := []interface{}{}
for i, user := range users {
valueStrings = append(valueStrings, fmt.Sprintf("($%d, $%d)", i*2+1, i*2+2))
valueArgs = append(valueArgs, user.Username, user.Email)
}
query := fmt.Sprintf("INSERT INTO users (username, email) VALUES %s",
strings.Join(valueStrings, ","))
_, err := db.Exec(query, valueArgs...)
return err
}NULL Handling
import "database/sql"
type User struct {
ID int
Username string
Email sql.NullString // Handles NULL values
Age sql.NullInt64
}
func getUserWithNulls(db *sql.DB, id int) (*User, error) {
var user User
err := db.QueryRow(
"SELECT id, username, email, age FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Username, &user.Email, &user.Age)
if err != nil {
return nil, err
}
return &user, nil
}Common Pitfalls
Pitfall 1: Not Closing Rows
Always close result sets:
// Bad
rows, _ := db.Query("SELECT * FROM users")
// Forgot to close - connection leak
// Good
rows, err := db.Query("SELECT * FROM users")
if err != nil {
return err
}
defer rows.Close()Pitfall 2: SQL Injection
Use parameters, not string concatenation:
// Bad - SQL injection
username := userInput
query := "SELECT * FROM users WHERE username = '" + username + "'"
// Good - parameterized
query := "SELECT * FROM users WHERE username = $1"
db.Query(query, username)Pitfall 3: Ignoring Errors
Check all errors:
// Bad
result, _ := db.Exec(query)
rows, _ := result.RowsAffected()
// Good
result, err := db.Exec(query)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return errors.New("no rows affected")
}Pitfall 4: Not Using Transactions
Use transactions for related operations:
// Bad - no atomicity
db.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
db.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
// Second update may fail, leaving inconsistent state
// Good - atomic
tx, _ := db.Begin()
tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
tx.Commit()Related Patterns
Related Tutorial: See Intermediate Tutorial - Database for database fundamentals and Beginner Tutorial - SQL Basics for SQL introduction.
Related How-To: See Handle Errors Gracefully for database error handling and Use Context Effectively for context with queries.
Related Cookbook: See Cookbook recipes “Database Connection Pool”, “Transaction Patterns”, and “SQL Query Optimization” for copy-paste ready database code.
Related Explanation: See Best Practices - Database for database design principles.
Further Reading
- database/sql Tutorial - Official database/sql guide
- sqlx Documentation - sqlx library guide
- Go Database Best Practices - Alex Edwards’ guide
- Connection Pooling - Pool management guide