Frontend Labs
⋯

61. Production database access with database/sql and pgx

61.1 database/sql is a pool, not a connection

db, err := sql.Open("postgres", dsn)
if err != nil {
	return err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(30 * time.Minute)

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
	return err
}

sql.Open validates arguments; it does not prove the database is reachable. Use PingContext during startup.

61.2 Always pass context into database calls

func (s *Store) User(ctx context.Context, id string) (User, error) {
	const q = `select id, email, created_at from users where id = $1`
	var u User
	if err := s.db.QueryRowContext(ctx, q, id).Scan(&u.ID, &u.Email, &u.CreatedAt); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return User{}, ErrUserNotFound
		}
		return User{}, fmt.Errorf("query user %s: %w", id, err)
	}
	return u, nil
}

Context ties DB work to request cancellation and deadlines. Without it, a client can disconnect while the database query continues pointlessly.

61.3 Transactions: defer rollback, check commit

tx, err := db.BeginTx(ctx, nil)
if err != nil {
	return err
}
defer tx.Rollback()

if _, err := tx.ExecContext(ctx, `update accounts set balance = balance - $1 where id = $2`, amount, from); err != nil {
	return err
}
if _, err := tx.ExecContext(ctx, `update accounts set balance = balance + $1 where id = $2`, amount, to); err != nil {
	return err
}
if err := tx.Commit(); err != nil {
	return err
}

The deferred rollback is cleanup for every early return. After Commit, it does nothing useful and its error is normally ignored.

61.4 pgx for PostgreSQL-heavy services

database/sql is portable. pgx is often better when PostgreSQL is the system of record and you want Postgres-specific features, better type support, COPY, notifications, or direct pool control.

pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil {
	return err
}
defer pool.Close()

var email string
if err := pool.QueryRow(ctx, `select email from users where id=$1`, id).Scan(&email); err != nil {
	return err
}

61.5 Migrations are part of the app lifecycle

Use a migration tool (golang-migrate, goose, Atlas, Flyway, Liquibase) and keep migrations in source control:

migrations/
  0001_create_users.up.sql
  0001_create_users.down.sql
  0002_add_last_login.up.sql
  0002_add_last_login.down.sql

Never rely on "the app creates whatever tables it needs" for production schema evolution. Schema changes need review, ordering, rollback planning, and observability just like code changes.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36