Database Operations

The Connection Pool That Saved Our Database

It was 2:34 AM when our database monitoring went red. CPU at 98%. Connection count: 1,247 active connections. PostgreSQL was dying.

The culprit was our Go service. Every HTTP request opened a new database connection:

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    // BAD: New connection per request
    db, err := sql.Open("postgres", connectionString)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer db.Close()  // Too late!
    
    var user User
    db.QueryRow("SELECT id, name FROM users WHERE id = $1", userID).Scan(&user.ID, &user.Name)
    
    json.NewEncoder(w).Encode(user)
}

At 500 requests per second, we were creating 500 connections per second. The database couldn't keep up.

The fix was simple - connection pooling:

The result:

  • Connection count dropped to 25 max

  • Database CPU dropped to 12%

  • Response time improved from 847ms to 23ms

  • Zero downtime since

That incident taught me: database connections are precious. This article covers everything I learned.


The database/sql Package

Go's database/sql package provides a generic interface for SQL databases.

Supported Drivers

You need a driver for your specific database:

Opening a Connection

Important: sql.Open() doesn't actually connect - it just validates the connection string. Use Ping() to verify the connection works.


Connection Pooling Configuration

The *sql.DB is a connection pool, not a single connection.

Pool Settings

Tuning Guidelines

MaxOpenConns:

  • Too high: Overwhelms database

  • Too low: Bottlenecks your app

  • Start with: 25-50 for most apps

MaxIdleConns:

  • Keeps connections warm for reuse

  • Start with: 25% of MaxOpenConns

ConnMaxLifetime:

  • Prevents stale connections

  • Start with: 5 minutes

Real Configuration


Querying Data

Single Row Query

Note: PostgreSQL uses $1, $2, $3 for placeholders. MySQL uses ?.

Multiple Rows Query

Critical: Always defer rows.Close() to release the connection back to the pool.

Query with WHERE Clause


Inserting, Updating, Deleting

Insert

Update

Delete


Prepared Statements

Prepared statements are pre-compiled SQL queries. Benefits:

  • Better performance (query is parsed once)

  • Protection against SQL injection

  • Cleaner code for repeated queries

Basic Prepared Statement

When to Use Prepared Statements

Use when:

  • Executing the same query multiple times

  • Complex queries with many parameters

Don't use when:

  • One-off queries

  • Dynamic queries (varying columns)


Transactions

Transactions ensure multiple operations succeed or fail together.

Basic Transaction

Transaction with Context

FOR UPDATE: Locks the row until transaction completes, preventing race conditions.


Scanning Results

Scanning into Struct

Handling NULL Values

NULL types: sql.NullString, sql.NullInt64, sql.NullFloat64, sql.NullBool, sql.NullTime


Working with sqlx

sqlx is a popular library that extends database/sql with useful features.

Installation

Basic Usage

Benefits:

  • Automatically maps columns to struct fields

  • Less boilerplate than database/sql

  • Named parameters support

Named Queries


Working with GORM

GORM is a full-featured ORM for Go.

Installation

Basic Usage

When to Use GORM

Use when:

  • Rapid prototyping

  • CRUD-heavy applications

  • Want automatic migrations

Don't use when:

  • Complex queries

  • Performance is critical

  • Need full SQL control

My preference: Use database/sql or sqlx for production. More control, better performance.


Real Example: Complete User Repository


Your Challenge

Build a complete blog post repository:


Key Takeaways

  1. Connection pooling: *sql.DB is a pool, configure it properly

  2. Always defer rows.Close(): Release connections back to pool

  3. QueryRow vs Query: Single row vs multiple rows

  4. Exec vs Query: Mutations vs reads

  5. Prepared statements: Better performance for repeated queries

  6. Transactions: Ensure atomicity with Begin/Commit/Rollback

  7. NULL handling: Use sql.Null* types

  8. Context: Always use context for cancellation/timeouts


What I Learned

That 2 AM database incident taught me that connection management is critical:

  • Connection pooling reduced connections from 1,247 to 25

  • Proper configuration dropped response time from 847ms to 23ms

  • Prepared statements improved query performance by 40%

  • Zero database incidents in 2 years since implementing pooling

Coming from Python's SQLAlchemy magic and Node's Sequelize complexity, Go's database/sql felt raw but powerful. The repository pattern has served 50,000 queries per second without breaking a sweat.

The time saved debugging connection issues? Countless hours.


Next: Best Practices and Code Organization

In the next article, we'll explore Go best practices and project structure. You'll learn the patterns that turned messy code into maintainable systems.

Last updated