Back to skills
SKILL.md
Data
ASecurityGo database integration patterns using sqlx, pgx, and migration tools like golang-migrate
- 74 stars
- 0 votes
- 0 copies
- 1 view
- Added June 6, 2026
Works with
Security analysis
100/100npx -y skills add bobmatnyc/claude-mpm-skills --skill golang-database-patterns --agent claude-codeAre you the author of Data?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/bobmatnyc-data)---
name: golang-database-patterns
description: "Go database integration patterns using sqlx, pgx, and migration tools like golang-migrate"
user-invocable: false
disable-model-invocation: true
version: 1.0.0
category: toolchain
author: Claude MPM Team
license: MIT
progressive_disclosure:
entry_point:
summary: "Master Go database patterns with sqlx, pgx, and golang-migrate for type-safe queries, migrations, and production-ready data access layers"
when_to_use: "Building CRUD operations, managing schema evolution, implementing repository patterns, handling transactions, optimizing connection pools, testing database code"
quick_start: "1. Choose library (database/sql, sqlx, pgx) 2. Set up connection pooling 3. Implement repository pattern 4. Handle transactions with context 5. Manage migrations with golang-migrate"
token_estimate:
entry: 150
full: 4800
context_limit: 700
tags:
- database
- golang
- sqlx
- pgx
- migrations
- repository-pattern
- sql
requires_tools: []
---
# Go Database Patterns
## Overview
Go's database ecosystem provides multiple layers of abstraction for SQL database integration. From the standard library's `database/sql` to enhanced libraries like `sqlx` and PostgreSQL-optimized `pgx`, developers can choose the right tool for their performance and ergonomics needs.
**Key Features:**
- ๐ **database/sql**: Standard interface for any SQL database
- ๐ **sqlx**: Convenience methods with struct scanning and named queries
- ๐ **pgx**: PostgreSQL-native driver with maximum performance
- ๐ฆ **Repository Pattern**: Interface-based data access for testability
- ๐ **Migrations**: Schema versioning with golang-migrate
- โก **Connection Pooling**: Production-ready connection management
- ๐ **Transaction Safety**: Context-aware transaction handling
## When to Use This Skill
Activate this skill when:
- Building CRUD operations with type safety
- Implementing data access layers for web services
- Managing database schema evolution across environments
- Optimizing database connection pooling for production
- Testing database code with mock repositories
- Handling concurrent database access patterns
- Migrating from ORMs to SQL-first approaches
- Integrating PostgreSQL-specific features (COPY, LISTEN/NOTIFY)
## Core Database Libraries
### Decision Tree: Choosing Your Database Library
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ What database are you using? โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโดโโโโโโโโโโโ
โ โ
PostgreSQL Other SQL DB
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโ Use database/sql
โ Need max perf? โ + sqlx for convenience
โโโโโโโฌโโโโโโโโโโโโ
โ
โโโโดโโโ
Yes No
โ โ
pgx sqlx + pq driver
```
**Use database/sql when:**
- Working with any SQL database (MySQL, SQLite, PostgreSQL, etc.)
- Need database portability
- Want standard library stability with no dependencies
**Use sqlx when:**
- Want convenience methods (Get, Select, StructScan)
- Need named parameter queries
- Using IN clause expansion
- Prefer less boilerplate than database/sql
**Use pgx when:**
- PostgreSQL-only application
- Need maximum performance (30-50% faster than lib/pq)
- Want advanced PostgreSQL features (COPY, LISTEN/NOTIFY, prepared statement caching)
- Building high-throughput systems
### database/sql: The Standard Foundation
**Core Concepts:**
```go
package main
import (
"context"
"database/sql"
"time"
_ "github.com/lib/pq" // PostgreSQL driver
)
func setupDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
// Connection pooling configuration
db.SetMaxOpenConns(25) // Max open connections
db.SetMaxIdleConns(5) // Max idle connections
db.SetConnMaxLifetime(5 * time.Minute) // Max connection lifetime
db.SetConnMaxIdleTime(1 * time.Minute) // Max idle time
// Verify connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, err
}
return db, nil
}
```
**Key Patterns:**
```go
// Query single row
func GetUserByID(ctx context.Context, db *sql.DB, id int) (*User, error) {
var user User
query := `SELECT id, name, email, created_at FROM users WHERE id = $1`
err := db.QueryRowContext(ctx, query, id).Scan(
&user.ID, &user.Name, &user.Email, &user.CreatedAt,
)
if err == sql.ErrNoRows {
return nil, ErrUserNotFound // Custom error
}
if err != nil {
return nil, fmt.Errorf("query user: %w", err)
}
return &user, nil
}
// Query multiple rows
func ListActiveUsers(ctx context.Context, db *sql.DB) ([]User, error) {
query := `SELECT id, name, email, created_at FROM users WHERE active = true`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query users: %w", err)
}
defer rows.Close() // CRITICAL: Always close rows
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt); err != nil {
return nil, fmt.Errorf("scan user: %w", err)
}
users = append(users, user)
}
// Check for errors during iteration
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate users: %w", err)
}
return users, nil
}
```
### sqlx: Ergonomic Extensions
**Installation:**
```bash
go get github.com/jmoiron/sqlx
```
**Core Features:**
```go
package main
import (
"context"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
CreatedAt time.Time `db:"created_at"`
}
// Get single struct
func GetUserByID(ctx context.Context, db *sqlx.DB, id int) (*User, error) {
var user User
query := `SELECT id, name, email, created_at FROM users WHERE id = $1`
err := db.GetContext(ctx, &user, query, id)
if err == sql.ErrNoRows {
return nil, ErrUserNotFound
}
return &user, err
}
// Select multiple structs
func ListUsers(ctx context.Context, db *sqlx.DB, limit int) ([]User, error) {
var users []User
query := `SELECT id, name, email, created_at FROM users LIMIT $1`
err := db.SelectContext(ctx, &users, query, limit)
return users, err
}
// Named queries
func FindUsersByName(ctx context.Context, db *sqlx.DB, name string) ([]User, error) {
var users []User
query := `SELECT * FROM users WHERE name LIKE :name || '%'`
nstmt, err := db.PrepareNamedContext(ctx, query)
if err != nil {
return nil, err
}
defer nstmt.Close()
err = nstmt.SelectContext(ctx, &users, map[string]interface{}{"name": name})
return users, err
}
// IN clause expansion
func GetUsersByIDs(ctx context.Context, db *sqlx.DB, ids []int) ([]User, error) {
var users []User
query, args, err := sqlx.In(`SELECT * FROM users WHERE id IN (?)`, ids)
if err != nil {
return nil, err
}
// Rebind for PostgreSQL ($1, $2, ...) vs MySQL (?, ?, ...)
query = db.Rebind(query)
err = db.SelectContext(ctx, &users, query, args...)
return users, err
}
```
### pgx: PostgreSQL-Native Performance
**Installation:**
```bash
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool
```
**Connection Pool Setup:**
```go
package main
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
func setupPgxPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
// Connection pool tuning
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = 1 * time.Hour
config.MaxConnIdleTime = 30 * time.Minute
config.HealthCheckPeriod = 1 * time.Minute
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("create pool: %w", err)
}
// Verify connectivity
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("ping: %w", err)
}
return pool, nil
}
```
**Query Patterns:**
```go
// Query single row
func GetUser(ctx context.Context, pool *pgxpool.Pool, id int) (*User, error) {
var user User
query := `SELECT id, name, email, created_at FROM users WHERE id = $1`
err := pool.QueryRow(ctx, query, id).Scan(
&user.ID, &user.Name, &user.Email, &user.CreatedAt,
)
if err == pgx.ErrNoRows {
return nil, ErrUserNotFound
}
return &user, err
}
// Batch operations (pgx-specific optimization)
func BatchInsertUsers(ctx context.Context, pool *pgxpool.Pool, users []User) error {
batch := &pgx.Batch{}
query := `INSERT INTO users (name, email) VALUES ($1, $2)`
for _, user := range users {
batch.Queue(query, user.Name, user.Email)
}
results := pool.SendBatch(ctx, batch)
defer results.Close()
for range users {
_, err := results.Exec()
if err != nil {
return fmt.Errorf("batch insert: %w", err)
}
}
return nil
}
// COPY for bulk inserts (10x faster than INSERT)
func BulkCopyUsers(ctx context.Context, pool *pgxpool.Pool, users []User) error {
_, err := pool.CopyFrom(
ctx,
pgx.Identifier{"users"},
[]string{"name", "email"},
pgx.CopyFromSlice(len(users), func(i int) ([]interface{}, error) {
return []interface{}{users[i].Name, users[i].Email}, nil
}),
)
return err
}
```
## Repository Pattern Implementation
### Interface-Based Design
```go
package repository
import (
"context"
"database/sql"
)
// UserRepository defines data access interface
type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id int) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id int) error
List(ctx context.Context, filters ListFilters) ([]User, error)
}
// PostgresUserRepository implements UserRepository
type PostgresUserRepository struct {
db *sqlx.DB
}
func NewPostgresUserRepository(db *sqlx.DB) *PostgresUserRepository {
return &PostgresUserRepository{db: db}
}
func (r *PostgresUserRepository) Create(ctx context.Context, user *User) error {
query := `
INSERT INTO users (name, email, password_hash)
VALUES ($1, $2, $3)
RETURNING id, created_at
`
err := r.db.QueryRowContext(
ctx, query,
user.Name, user.Email, user.PasswordHash,
).Scan(&user.ID, &user.CreatedAt)
if err != nil {
return fmt.Errorf("insert user: %w", err)
}
return nil
}
func (r *PostgresUserRepository) GetByID(ctx context.Context, id int) (*User, error) {
var user User
query := `SELECT id, name, email, created_at, updated_at FROM users WHERE id = $1`
err := r.db.GetContext(ctx, &user, query, id)
if err == sql.ErrNoRows {
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
return &user, nil
}
func (r *PostgresUserRepository) Update(ctx context.Context, user *User) error {
query := `
UPDATE users
SET name = $1, email = $2, updated_at = NOW()
WHERE id = $3
RETURNING updated_at
`
err := r.db.QueryRowContext(
ctx, query,
user.Name, user.Email, user.ID,
).Scan(&user.UpdatedAt)
if err == sql.ErrNoRows {
return ErrUserNotFound
}
return err
}
func (r *PostgresUserRepository) Delete(ctx context.Context, id int) error {
query := `DELETE FROM users WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("delete user: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return ErrUserNotFound
}
return nil
}
```
### Testing with Mock Repository
```go
package repository_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockUserRepository for testing
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) GetByID(ctx context.Context, id int) (*User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func TestUserService_GetUser(t *testing.T) {
mockRepo := new(MockUserRepository)
service := NewUserService(mockRepo)
expectedUser := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}
mockRepo.On("GetByID", mock.Anything, 1).Return(expectedUser, nil)
user, err := service.GetUser(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
mockRepo.AssertExpectations(t)
}
```
## Transaction Handling
### Basic Transaction Pattern
```go
func (r *PostgresUserRepository) UpdateWithHistory(ctx context.Context, user *User) error {
tx, err := r.db.BeginTxx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback() // Safe to call even after commit
// Update user
query := `UPDATE users SET name = $1, email = $2 WHERE id = $3`
_, err = tx.ExecContext(ctx, query, user.Name, user.Email, user.ID)
if err != nil {
return fmt.Errorf("update user: %w", err)
}
// Insert history record
historyQuery := `INSERT INTO user_history (user_id, name, email, changed_at) VALUES ($1, $2, $3, NOW())`
_, err = tx.ExecContext(ctx, historyQuery, user.ID, user.Name, user.Email)
if err != nil {
return fmt.Errorf("insert history: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit tx: %w", err)
}
return nil
}
```
### Transaction Isolation Levels
```go
func (r *PostgresUserRepository) TransferBalance(ctx context.Context, fromID, toID int, amount float64) error {
// Use serializable isolation for financial transactions
txOpts := &sql.TxOptions{
Isolation: sql.LevelSerializable,
ReadOnly: false,
}
tx, err := r.db.BeginTxx(ctx, txOpts)
if err != nil {
return err
}
defer tx.Rollback()
// Deduct from sender
_, err = tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance - $1 WHERE user_id = $2 AND balance >= $1`,
amount, fromID,
)
if err != nil {
return fmt.Errorf("deduct balance: %w", err)
}
// Add to receiver
_, err = tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance + $1 WHERE user_id = $2`,
amount, toID,
)
if err != nil {
return fmt.Errorf("add balance: %w", err)
}
return tx.Commit()
}
```
### Retry Logic for Serialization Failures
```go
func WithRetry(ctx context.Context, maxRetries int, fn func() error) error {
for i := 0; i < maxRetries; i++ {
err := fn()
if err == nil {
return nil
}
// Check for serialization error (PostgreSQL error code 40001)
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "40001" {
// Exponential backoff
time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
continue
}
return err // Non-retryable error
}
return fmt.Errorf("max retries exceeded")
}
// Usage
err := WithRetry(ctx, 3, func() error {
return r.TransferBalance(ctx, fromID, toID, amount)
})
```
## Database Migrations
### Decision Tree: Migration Tools
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Migration tool selection โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโดโโโโโโโโโโโ
โ โ
Simple SQL Complex logic
migrations (Go code needed)
โ โ
โผ โผ
golang-migrate goose
(SQL only) (Go + SQL migrations)
```
**Use golang-migrate when:**
- Pure SQL migrations (no custom Go logic)
- Need CLI tool for manual migrations
- Want clean separation of schema and application
- Industry standard (most popular)
**Use goose when:**
- Need Go code in migrations (data transformations)
- Want flexibility of both SQL and Go
- Need custom migration logic
**Use sql-migrate when:**
- Using sqlx already
- Want embedded migrations in binary
- Need programmatic migration control
### golang-migrate Setup
**Installation:**
```bash
# CLI tool
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Library
go get -u github.com/golang-migrate/migrate/v4
go get -u github.com/golang-migrate/migrate/v4/database/postgres
go get -u github.com/golang-migrate/migrate/v4/source/file
```
**Migration Files:**
```bash
# Create migration
migrate create -ext sql -dir migrations -seq create_users_table
# Generates:
# migrations/000001_create_users_table.up.sql
# migrations/000001_create_users_table.down.sql
```
**000001_create_users_table.up.sql:**
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
```
**000001_create_users_table.down.sql:**
```sql
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;
```
**Programmatic Migration:**
```go
package main
import (
"fmt"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func runMigrations(databaseURL, migrationsPath string) error {
m, err := migrate.New(
fmt.Sprintf("file://%s", migrationsPath),
databaseURL,
)
if err != nil {
return fmt.Errorf("create migrate instance: %w", err)
}
defer m.Close()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("run migrations: %w", err)
}
version, dirty, err := m.Version()
if err != nil {
return err
}
fmt.Printf("Migration complete. Version: %d, Dirty: %v\n", version, dirty)
return nil
}
```
**CLI Usage:**
```bash
# Apply all up migrations
migrate -path migrations -database "postgres://user:pass@localhost:5432/db?sslmode=disable" up
# Rollback one migration
migrate -path migrations -database $DATABASE_URL down 1
# Go to specific version
migrate -path migrations -database $DATABASE_URL goto 5
# Check current version
migrate -path migrations -database $DATABASE_URL version
```
## NULL Handling
### Using sql.Null* Types
```go
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
Phone sql.NullString `db:"phone"` // Nullable string
Age sql.NullInt64 `db:"age"` // Nullable int
UpdatedAt sql.NullTime `db:"updated_at"` // Nullable timestamp
}
func (r *PostgresUserRepository) GetUser(ctx context.Context, id int) (*User, error) {
var user User
err := r.db.GetContext(ctx, &user, `SELECT * FROM users WHERE id = $1`, id)
if err != nil {
return nil, err
}
// Access nullable fields
if user.Phone.Valid {
fmt.Println("Phone:", user.Phone.String)
}
return &user, nil
}
// Setting NULL values
func (r *PostgresUserRepository) UpdatePhone(ctx context.Context, userID int, phone *string) error {
var nullPhone sql.NullString
if phone != nil {
nullPhone = sql.NullString{String: *phone, Valid: true}
}
// If phone is nil, nullPhone.Valid is false, SQL writes NULL
query := `UPDATE users SET phone = $1 WHERE id = $2`
_, err := r.db.ExecContext(ctx, query, nullPhone, userID)
return err
}
```
### Custom Nullable Types (Preferred Pattern)
```go
// Custom nullable type with JSON marshaling
type NullString struct {
sql.NullString
}
func (ns NullString) MarshalJSON() ([]byte, error) {
if !ns.Valid {
return []byte("null"), nil
}
return json.Marshal(ns.String)
}
func (ns *NullString) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
ns.Valid = false
return nil
}
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
ns.String = s
ns.Valid = true
return nil
}
```
## Anti-Patterns to Avoid
### โ N+1 Query Problem
**Wrong:**
```go
func GetUsersWithPosts(ctx context.Context, db *sqlx.DB) ([]UserWithPosts, error) {
var users []User
db.SelectContext(ctx, &users, `SELECT * FROM users`)
for i, user := range users {
var posts []Post
// N+1: One query per user!
db.SelectContext(ctx, &posts, `SELECT * FROM posts WHERE user_id = $1`, user.ID)
users[i].Posts = posts
}
return users, nil
}
```
**Correct:**
```go
func GetUsersWithPosts(ctx context.Context, db *sqlx.DB) ([]UserWithPosts, error) {
// Single query with JOIN
query := `
SELECT u.id, u.name, p.id as post_id, p.title, p.content
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
ORDER BY u.id
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
usersMap := make(map[int]*UserWithPosts)
for rows.Next() {
var userID int
var userName string
var postID sql.NullInt64
var title, content sql.NullString
rows.Scan(&userID, &userName, &postID, &title, &content)
if _, exists := usersMap[userID]; !exists {
usersMap[userID] = &UserWithPosts{ID: userID, Name: userName}
}
if postID.Valid {
usersMap[userID].Posts = append(usersMap[userID].Posts, Post{
ID: int(postID.Int64),
Title: title.String,
Content: content.String,
})
}
}
result := make([]UserWithPosts, 0, len(usersMap))
for _, user := range usersMap {
result = append(result, *user)
}
return result, nil
}
```
### โ Missing Connection Pool Configuration
**Wrong:**
```go
db, _ := sql.Open("postgres", dsn)
// Uses defaults: unlimited connections, no timeouts
```
**Correct:**
```go
db, _ := sql.Open("postgres", dsn)
// Production-ready pool settings
db.SetMaxOpenConns(25) // Limit total connections
db.SetMaxIdleConns(5) // Limit idle connections
db.SetConnMaxLifetime(5 * time.Minute) // Recycle old connections
db.SetConnMaxIdleTime(1 * time.Minute) // Close idle connections
```
### โ Ignoring Context Cancellation
**Wrong:**
```go
func SlowQuery(db *sql.DB) error {
// No context - query runs until completion even if client disconnects
rows, err := db.Query("SELECT * FROM huge_table")
// ...
}
```
**Correct:**
```go
func SlowQuery(ctx context.Context, db *sql.DB) error {
// Context cancellation propagates to database
rows, err := db.QueryContext(ctx, "SELECT * FROM huge_table")
// If ctx is canceled, query is terminated
}
```
### โ Not Closing Rows
**Wrong:**
```go
func GetUsers(db *sql.DB) ([]User, error) {
rows, _ := db.Query("SELECT * FROM users")
// Missing rows.Close() - connection leak!
var users []User
for rows.Next() {
// ...
}
return users, nil
}
```
**Correct:**
```go
func GetUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT * FROM users")
if err != nil {
return nil, err
}
defer rows.Close() // CRITICAL: Always defer Close
var users []User
for rows.Next() {
// ...
}
return users, rows.Err() // Check for iteration errors
}
```
### โ SQL Injection Vulnerability
**Wrong:**
```go
func FindUser(db *sql.DB, email string) (*User, error) {
// NEVER concatenate user input into SQL!
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
// Vulnerable to: ' OR '1'='1
row := db.QueryRow(query)
// ...
}
```
**Correct:**
```go
func FindUser(db *sql.DB, email string) (*User, error) {
// Use parameterized queries
query := "SELECT * FROM users WHERE email = $1"
row := db.QueryRow(query, email) // Safe
// ...
}
```
### โ Ignoring Transaction Errors
**Wrong:**
```go
func UpdateUser(db *sql.DB, user *User) error {
tx, _ := db.Begin()
tx.Exec("UPDATE users SET name = $1 WHERE id = $2", user.Name, user.ID)
tx.Commit() // Ignores errors - data may not be committed!
return nil
}
```
**Correct:**
```go
func UpdateUser(db *sql.DB, user *User) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // Rollback if commit not reached
_, err = tx.Exec("UPDATE users SET name = $1 WHERE id = $2", user.Name, user.ID)
if err != nil {
return err
}
return tx.Commit() // Check commit error
}
```
## Connection Pooling Best Practices
### Tuning Parameters
```go
func OptimizeDatabasePool(db *sql.DB, config PoolConfig) {
// MaxOpenConns: Total connections (in-use + idle)
// Rule of thumb: (CPU cores * 2) + disk spindles
// Cloud databases often limit connections (e.g., AWS RDS: 100-5000)
db.SetMaxOpenConns(config.MaxOpen) // Example: 25 for small app
// MaxIdleConns: Idle connections ready for reuse
// Should be lower than MaxOpenConns
// Too low: frequent reconnections (slow)
// Too high: wasted resources
db.SetMaxIdleConns(config.MaxIdle) // Example: 5-10
// ConnMaxLifetime: Maximum age of any connection
// Prevents stale connections to load balancers
// Recommended: 5-15 minutes
db.SetConnMaxLifetime(config.MaxLifetime)
// ConnMaxIdleTime: Close idle connections after this duration
// Saves resources during low traffic
// Recommended: 1-5 minutes
db.SetConnMaxIdleTime(config.MaxIdleTime)
}
type PoolConfig struct {
MaxOpen int
MaxIdle int
MaxLifetime time.Duration
MaxIdleTime time.Duration
}
// Example configurations
var (
// Development: Low resource usage
DevConfig = PoolConfig{
MaxOpen: 10,
MaxIdle: 2,
MaxLifetime: 10 * time.Minute,
MaxIdleTime: 2 * time.Minute,
}
// Production: High throughput
ProdConfig = PoolConfig{
MaxOpen: 25,
MaxIdle: 10,
MaxLifetime: 5 * time.Minute,
MaxIdleTime: 1 * time.Minute,
}
// High-traffic API: Maximum performance
HighTrafficConfig = PoolConfig{
MaxOpen: 50,
MaxIdle: 20,
MaxLifetime: 5 * time.Minute,
MaxIdleTime: 30 * time.Second,
}
)
```
### Monitoring Connection Pool
```go
func MonitorConnectionPool(db *sql.DB) {
stats := db.Stats()
fmt.Printf("Connection Pool Stats:\n")
fmt.Printf(" Open Connections: %d\n", stats.OpenConnections)
fmt.Printf(" In Use: %d\n", stats.InUse)
fmt.Printf(" Idle: %d\n", stats.Idle)
fmt.Printf(" Wait Count: %d\n", stats.WaitCount) // Queries waited for connection
fmt.Printf(" Wait Duration: %s\n", stats.WaitDuration) // Total wait time
fmt.Printf(" Max Idle Closed: %d\n", stats.MaxIdleClosed) // Connections closed due to idle
fmt.Printf(" Max Lifetime Closed: %d\n", stats.MaxLifetimeClosed)
// Alert if too many waits (need more connections)
if stats.WaitCount > 100 {
fmt.Println("WARNING: High wait count - consider increasing MaxOpenConns")
}
// Alert if many idle closures (pool too large)
if stats.MaxIdleClosed > 1000 {
fmt.Println("INFO: Many idle closures - consider reducing MaxIdleConns")
}
}
```
## Testing Database Code
### Using sqlmock for Unit Tests
**Installation:**
```bash
go get github.com/DATA-DOG/go-sqlmock
```
**Example:**
```go
package repository_test
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
)
func TestGetUserByID(t *testing.T) {
// Create mock database
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
sqlxDB := sqlx.NewDb(db, "postgres")
repo := NewPostgresUserRepository(sqlxDB)
// Expected query and result
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
AddRow(1, "Alice", "alice@example.com")
mock.ExpectQuery("SELECT (.+) FROM users WHERE id = \\$1").
WithArgs(1).
WillReturnRows(rows)
// Execute
user, err := repo.GetByID(context.Background(), 1)
// Assert
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
assert.Equal(t, "alice@example.com", user.Email)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestGetUserByID_NotFound(t *testing.T) {
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
sqlxDB := sqlx.NewDb(db, "postgres")
repo := NewPostgresUserRepository(sqlxDB)
mock.ExpectQuery("SELECT (.+) FROM users WHERE id = \\$1").
WithArgs(999).
WillReturnError(sql.ErrNoRows)
user, err := repo.GetByID(context.Background(), 999)
assert.Nil(t, user)
assert.ErrorIs(t, err, ErrUserNotFound)
assert.NoError(t, mock.ExpectationsWereMet())
}
```
### Integration Tests with Real Database
```go
// +build integration
package repository_test
import (
"context"
"testing"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type UserRepositoryIntegrationSuite struct {
suite.Suite
db *sqlx.DB
repo *PostgresUserRepository
}
func (s *UserRepositoryIntegrationSuite) SetupSuite() {
// Connect to test database
db, err := sqlx.Connect("postgres", "postgres://test:test@localhost/testdb?sslmode=disable")
s.Require().NoError(err)
s.db = db
s.repo = NewPostgresUserRepository(db)
}
func (s *UserRepositoryIntegrationSuite) TearDownSuite() {
s.db.Close()
}
func (s *UserRepositoryIntegrationSuite) SetupTest() {
// Clean database before each test
_, err := s.db.Exec("TRUNCATE users RESTART IDENTITY CASCADE")
s.Require().NoError(err)
}
func (s *UserRepositoryIntegrationSuite) TestCreateUser() {
user := &User{Name: "Alice", Email: "alice@example.com"}
err := s.repo.Create(context.Background(), user)
s.NoError(err)
s.NotZero(user.ID)
s.NotZero(user.CreatedAt)
}
func (s *UserRepositoryIntegrationSuite) TestGetUserByID() {
// Insert test data
user := &User{Name: "Bob", Email: "bob@example.com"}
s.repo.Create(context.Background(), user)
// Test retrieval
retrieved, err := s.repo.GetByID(context.Background(), user.ID)
s.NoError(err)
s.Equal(user.Name, retrieved.Name)
s.Equal(user.Email, retrieved.Email)
}
func TestUserRepositoryIntegration(t *testing.T) {
suite.Run(t, new(UserRepositoryIntegrationSuite))
}
```
**Run integration tests:**
```bash
# Skip integration tests by default
go test ./...
# Run only integration tests
go test -tags=integration ./...
```
## Resources and Further Reading
### Official Documentation
- [database/sql Tutorial](https://go.dev/doc/database/sql) - Official Go database guide (2024)
- [sqlx Documentation](https://jmoiron.github.io/sqlx/) - Illustrated guide to sqlx
- [pgx Documentation](https://github.com/jackc/pgx) - PostgreSQL driver and toolkit
- [golang-migrate](https://github.com/golang-migrate/migrate) - Database migration tool
### Best Practices
- [Go Database Best Practices](https://www.alexedwards.net/blog/organising-database-access) - Alex Edwards (2024)
- [Connection Pool Tuning](https://www.alexedwards.net/blog/configuring-sqldb) - Production configuration guide
- [Repository Pattern in Go](https://threedots.tech/post/repository-pattern-in-go/) - Three Dots Labs
### Migration Tools
- [golang-migrate CLI](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate) - Command-line tool
- [goose](https://github.com/pressly/goose) - Alternative with Go and SQL migrations
- [sql-migrate](https://github.com/rubenv/sql-migrate) - Migration tool with sqlx integration
### Testing
- [go-sqlmock](https://github.com/DATA-DOG/go-sqlmock) - SQL mock for unit tests
- [testcontainers-go](https://github.com/testcontainers/testcontainers-go) - Docker containers for integration tests
- [dockertest](https://github.com/ory/dockertest) - Ephemeral databases for testing
### Advanced Topics
- [Handling Database Errors](https://www.alexedwards.net/blog/working-with-database-nulls) - NULL handling patterns
- [PostgreSQL LISTEN/NOTIFY with pgx](https://github.com/jackc/pgx/wiki/PG-Notify-Example) - Real-time notifications
- [Query Builders](https://github.com/Masterminds/squirrel) - Dynamic SQL generation
- [GORM](https://gorm.io/) - Full-featured ORM (alternative approach)
## Summary
Go database patterns prioritize simplicity, type safety, and performance:
**Library Selection:**
- Start with `database/sql` for portability
- Add `sqlx` for convenience and reduced boilerplate
- Use `pgx` for PostgreSQL-specific high-performance applications
**Core Patterns:**
- Repository pattern for testable data access layers
- Context-aware queries for cancellation and timeouts
- Proper transaction handling with defer rollback
- Connection pooling tuned for production workloads
**Migration Strategy:**
- Use `golang-migrate` for version-controlled schema evolution
- Separate up/down migrations for safe rollbacks
- Run migrations programmatically or via CLI
**Avoid Common Pitfalls:**
- N+1 queries (use JOINs or batching)
- Missing connection pool configuration
- SQL injection (always use parameterized queries)
- Not closing rows (defer rows.Close())
- Ignoring context cancellation
**Testing:**
- Unit tests with sqlmock for business logic
- Integration tests with real databases for critical paths
- Repository interfaces for dependency injection
By following these patterns, you'll build robust, performant, and maintainable database layers in Go.
Attribution
Comments
Loading commentsโฆ
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.