Database Operations in Go
Go provides excellent support for database operations through thedatabase/sql package. This chapter covers SQL databases, ORMs, and NoSQL solutions.
The database/sql Package
Thedatabase/sql package provides a generic interface for SQL databases. Think of database/sql as a universal remote control: it provides the same buttons (Query, Exec, Begin) regardless of which TV (database) you are talking to. The driver is the infrared translator that converts your button presses into the specific protocol your database understands. The package manages a connection pool, handles reconnection, and provides a consistent API regardless of which database you use. You need a driver for your specific database — the driver registers itself via an init function (this is one of the good uses of init and blank imports).
Setting Up a Connection
Basic CRUD Operations
Inserting Data
Querying Single Row
Querying Multiple Rows
Updating Data
Deleting Data
Transactions
Transactions are like an “all-or-nothing” switch: either every operation within the transaction succeeds and is committed together, or any failure rolls everything back as if nothing happened. In Go, the pattern is todefer tx.Rollback() immediately after beginning the transaction. If Commit() is called successfully, the deferred Rollback() becomes a no-op. If any error occurs, the deferred Rollback() cleans up.
Basic Transaction
Transaction Helper
Prepared Statements
Using Prepared Statements
Handling NULL Values
Using sql.Null Types
Using Pointers
Query Builder Pattern
GORM ORM
GORM is the most popular ORM for Go.Setup
CRUD with GORM
Relationships with GORM
GORM Transactions
sqlx Package
sqlx extends database/sql with helpful features.Redis
Redis Caching Pattern
MongoDB
Interview Questions
What's the difference between Query and Exec?
What's the difference between Query and Exec?
Query/QueryContext: Returns rows, use for SELECTExec/ExecContext: Returns Result (LastInsertId, RowsAffected), use for INSERT/UPDATE/DELETE
Why is it important to close rows?
Why is it important to close rows?
- Connection leaks (connection stays open)
- Resource exhaustion (max connections reached)
- Memory leaks
defer rows.Close() immediately after QueryContext.How do you prevent SQL injection?
How do you prevent SQL injection?
- Use parameterized queries (
$1,?placeholders) - Never concatenate user input into SQL strings
- Use prepared statements
- Use an ORM with proper escaping
What are connection pool settings and why are they important?
What are connection pool settings and why are they important?
MaxOpenConns: Max simultaneous connections (prevents overwhelming DB)MaxIdleConns: Connections kept in pool (reduces connection overhead)ConnMaxLifetime: How long a connection can be reusedConnMaxIdleTime: How long idle connections stay open
Summary
Interview Deep-Dive
Explain how database/sql connection pooling works in Go. What do MaxOpenConns, MaxIdleConns, and ConnMaxLifetime actually do, and what happens if you misconfigure them?
Explain how database/sql connection pooling works in Go. What do MaxOpenConns, MaxIdleConns, and ConnMaxLifetime actually do, and what happens if you misconfigure them?
database/sqlmaintains a pool of database connections internally. When you callQueryContextorExecContext, it grabs a connection from the pool (or opens a new one), executes the query, and returns the connection to the pool.MaxOpenConnslimits the total number of connections (both in-use and idle). If all connections are in use and a new query arrives, it blocks until one is returned. Set this too high, and you overwhelm the database server. Set it too low, and your application blocks during traffic spikes.MaxIdleConnscontrols how many connections stay open in the pool when not in use. Idle connections avoid TCP handshake overhead. Set too high, and you waste database resources. Set too low, and you repeatedly open/close connections.ConnMaxLifetimeforces connections to be closed and replaced after a duration. This prevents stale connections from database firewalls, DNS changes, and connection-level state accumulation. A typical value is 5 minutes.- Misconfiguration example: leaving defaults (
MaxOpenConns = 0, meaning unlimited) in a service handling 10,000 concurrent requests. The pool opens 10,000 connections, PostgreSQL rejects them (default max is 100), and you get a flood of “too many connections” errors.
rows.Close() after a QueryContext. What happens?The connection is never returned to the pool. It is “in use” forever. As more queries leak connections, the pool reaches MaxOpenConns, and subsequent queries block indefinitely — classic connection pool exhaustion. The fix is always defer rows.Close() immediately after QueryContext.Walk me through how you would implement the repository pattern for a Go service, and how does this help with testing?
Walk me through how you would implement the repository pattern for a Go service, and how does this help with testing?
- Define a repository interface at the service layer:
type UserRepository interface { GetByID(ctx context.Context, id int64) (*User, error); Create(ctx context.Context, user *User) error }. - The concrete implementation uses actual SQL in a
postgrespackage. The service constructor takes the interface:func NewUserService(repo UserRepository) *UserService. - For testing, a mock implements the same interface with in-memory operations. Integration tests inject the real PostgreSQL implementation against a test database.
- Key design decisions: the interface uses domain types (
*User), not database types (sql.NullString). Error types are domain-specific (ErrNotFound). Context propagates as the first parameter. - The pattern also makes caching transparent: a
CachedUserRepowraps aUserRepository, checks Redis first, falls back to the wrapped repo. Same interface, invisible to the service.
sql.Open does not actually connect to the database. Why was it designed this way?sql.Open creates the pool configuration but opens no TCP connections. The pool is lazy, creating connections on demand. This allows the application to start before the database is ready (common in container orchestration). To verify connectivity, call db.PingContext(ctx) after sql.Open.Your service needs to insert an order with 50 line items atomically. Show me the transaction pattern and explain the `defer tx.Rollback()` idiom.
Your service needs to insert an order with 50 line items atomically. Show me the transaction pattern and explain the `defer tx.Rollback()` idiom.
- Start with
tx, err := db.BeginTx(ctx, nil). Thendefer tx.Rollback()immediately — rollback on an already-committed transaction is a no-op, so this is safe. Execute queries withtx.ExecContext. If any step fails, return the error (deferred rollback fires). On success, calltx.Commit(). - This idiom handles all failure paths: early returns, panics, and context cancellation. You never need manual rollback in error branches.
- If context is cancelled mid-transaction, the current query fails. The deferred rollback sends ROLLBACK to the database. If the connection was lost, the database server rolls back after its idle timeout.
- For 50 line items: use batch inserts rather than 50 individual INSERTs. This reduces round trips from 50 to 1.
- Edge case: if
tx.Commit()fails (network error), the transaction might or might not have committed server-side. For critical operations, use idempotency keys.
QueryContext and ExecContext?QueryContext returns *sql.Rows for SELECT queries. ExecContext returns sql.Result (with LastInsertId() and RowsAffected()) for INSERT, UPDATE, DELETE. Using QueryContext for an INSERT works but you must still call rows.Close() or you leak a connection. Using ExecContext for a SELECT discards the data. Rule: QueryContext for reading, ExecContext for writing.