Table of Contents
- Go Backend & Databases — Interview Questions
- Table of Contents
- HTTP Server Internals
- REST API Design
- gRPC
- database/sql & Connection Pooling
- Transactions
- Redis & Caching
- SQL Indexing
Go Backend & Databases — Interview Questions¶
Questions covering REST, gRPC, the net/http server, database/sql, connection
pooling, transactions, Redis, caching, and indexing. Difficulty is marked per
question: 🟢 junior · 🟡 mid · 🔴 senior.
Table of Contents¶
- HTTP Server Internals
- REST API Design
- gRPC
- database/sql & Connection Pooling
- Transactions
- Redis & Caching
- SQL Indexing
HTTP Server Internals¶
🟢 What is the http.Handler interface and how does ServeMux fit in?
`http.Handler` is the core server abstraction with one method: `ServeHTTP(w http.ResponseWriter, r *http.Request)`. Anything implementing it can serve requests, and `http.HandlerFunc` adapts a plain function to the interface. `http.ServeMux` is itself a `Handler` that acts as a request router (multiplexer): it matches the request path against registered patterns and dispatches to the matching handler. Since Go 1.22 the default mux supports method and wildcard patterns like `GET /items/{id}`.🟡 How does Go's net/http server handle concurrency?
The `net/http` server accepts connections in a loop and spawns **one goroutine per connection**. Each connection goroutine reads requests and invokes your handler, so handlers run concurrently and must be safe for concurrent use of any shared state. Because goroutines are cheap, this model scales to many thousands of simultaneous connections without a thread pool. The flip side is that a handler that blocks (e.g. on a slow DB call without timeouts) ties up a goroutine, so you should always use context-aware, bounded operations.🔴 How do you implement graceful shutdown of an HTTP server?
Call `srv.Shutdown(ctx)` instead of letting the process die abruptly. `Shutdown` stops accepting new connections, then waits for in-flight requests to finish up to the context deadline, returning early if the context expires. Typically you listen for SIGINT/SIGTERM, then call Shutdown with a timeout context so long-running requests get a grace period but can't hang shutdown forever. Note that `ListenAndServe` returns `http.ErrServerClosed` when Shutdown is called, which you should treat as a clean exit.🟡 Why should you set ReadTimeout and WriteTimeout on http.Server?
The zero-value `http.Server` has **no timeouts**, meaning a slow or malicious client can hold a connection (and its goroutine) open indefinitely — a Slowloris-style denial of service. `ReadTimeout` bounds how long reading the entire request (including body) may take, `WriteTimeout` bounds writing the response, and `IdleTimeout` bounds keep-alive idle time. For finer control over just the headers use `ReadHeaderTimeout`. Setting sensible timeouts is a basic production hardening step.🟡 How does request context cancellation work when a client disconnects?
Every `*http.Request` carries a `Context()` that the server cancels automatically when the client disconnects or the request times out. If you propagate that context into downstream calls (DB queries, outbound HTTP, etc.), they will be canceled promptly when the client goes away, freeing resources instead of doing wasted work. This is why you should thread `r.Context()` through your whole call stack rather than using `context.Background()` inside handlers.🔴 What is middleware in Go HTTP servers and how is it typically composed?
Middleware is a function that wraps an `http.Handler` to add cross-cutting behavior — logging, auth, metrics, recovery, compression — and returns a new `http.Handler`. Because the wrapping function has the signature `func(http.Handler) http.Handler`, you can chain them so each runs before/after the next, forming an onion. Composition is just nested calls (or a small helper that folds a slice), and order matters: recovery should be outermost so it catches panics from inner layers.REST API Design¶
🟢 What makes an API RESTful, and how do HTTP methods map to operations?
REST models server state as **resources** addressed by URLs, manipulated through a uniform set of HTTP methods, and ideally stateless between requests. The common mapping is: `GET` to read (safe, idempotent), `POST` to create, `PUT` to replace/upsert (idempotent), `PATCH` to partially update, and `DELETE` to remove (idempotent). Status codes communicate outcome: 200/201/204 for success, 400/404/409 for client errors, 5xx for server errors. Good REST APIs use nouns for paths (`/users/123/orders`) and reserve verbs for the method, not the URL.🟡 What does idempotency mean and why does it matter for REST and retries?
An operation is idempotent if performing it multiple times has the same effect as performing it once. `GET`, `PUT`, and `DELETE` are expected to be idempotent, while `POST` generally is not (two POSTs create two resources). This matters because networks are unreliable: clients and proxies retry failed requests, and idempotent operations are safe to retry without side effects. For non-idempotent operations you can add an **idempotency key** (a client-supplied unique header) so the server deduplicates retries — payment APIs do this universally.🟡 How would you design pagination for a REST list endpoint?
Two main strategies exist. **Offset/limit** (`?offset=40&limit=20`) is simple but degrades on large offsets (the DB must scan and skip rows) and can skip/duplicate items if data changes between pages. **Cursor (keyset) pagination** uses a stable sort key — `WHERE id > $last_id ORDER BY id LIMIT 20` — which stays fast at any depth and is consistent under concurrent inserts, at the cost of not supporting random page jumps. For large or high-write datasets, prefer cursor pagination and return an opaque `next_cursor` token.gRPC¶
🟢 What is gRPC and what does it use under the hood?
gRPC is a high-performance RPC framework that uses **Protocol Buffers** as its interface definition language and serialization format, and **HTTP/2** as its transport. You define services and messages in a `.proto` file, and a code generator produces strongly-typed client and server stubs in many languages. Protobuf's binary encoding is compact and fast compared to JSON, and HTTP/2 enables multiplexing many calls over one connection. The contract-first `.proto` gives you schema enforcement and easy cross-language interop.🟡 What are the four types of gRPC service methods?
gRPC supports four call patterns. **Unary** is a single request and single response, like a normal function call. **Server streaming** sends one request and receives a stream of responses (e.g. subscribing to updates). **Client streaming** sends a stream of requests and gets one response (e.g. uploading chunks). **Bidirectional streaming** lets both sides send streams independently over the same connection (e.g. chat). All of these ride on HTTP/2 streams, which is what makes multiplexed streaming natural.🔴 How do deadlines and context propagation work in gRPC?
gRPC is deeply integrated with Go's `context`: every client call takes a context, and a **deadline** set with `context.WithTimeout` is transmitted over the wire to the server. The server sees the remaining time in its handler's context and should propagate that same context to its own downstream calls, so a deadline flows through an entire chain of services. If the deadline is exceeded, the call is canceled end to end and returns `codes.DeadlineExceeded`, avoiding wasted work on results nobody is waiting for. Always set deadlines — an unbounded gRPC call is a reliability hazard.🟡 When would you choose gRPC over REST/JSON, and what are the tradeoffs?
Choose gRPC for internal service-to-service communication where you want low latency, compact binary payloads, strict schemas, code-generated clients, and streaming — it shines in microservice meshes. Choose REST/JSON for public-facing APIs, browser clients (gRPC needs gRPC-Web plus a proxy), and easy human debuggability with curl. gRPC's downsides are that payloads aren't human-readable, tooling/observability is heavier, and proto evolution requires discipline. Many systems do both: gRPC internally, a REST/JSON gateway at the edge.database/sql & Connection Pooling¶
🟢 Is sql.DB a single connection? Explain what it actually represents.
No — `sql.DB` is a **connection pool**, not a single connection, and it is safe for concurrent use by many goroutines. You typically open it once at program startup and keep it for the lifetime of the application rather than opening one per request. The pool lazily creates connections as needed (`Open` doesn't actually connect; call `Ping` to verify), reuses idle ones, and hands them out per query. Treating it like a single connection or opening/closing it repeatedly is a common and costly mistake.🟡 What do SetMaxOpenConns, SetMaxIdleConns, and SetConnMaxLifetime control?
`SetMaxOpenConns(n)` caps the total open connections (in-use + idle); when the limit is hit, additional queries block until a connection frees up. `SetMaxIdleConns(n)` caps how many idle connections are kept ready for reuse (default is just **2**, which often causes churn under load). `SetConnMaxLifetime(d)` forces connections to be recycled after a maximum age, and `SetConnMaxIdleTime(d)` closes connections that have sat idle too long. Tuning these together prevents both connection exhaustion and the overhead of constantly reopening connections.🔴 Why is SetConnMaxLifetime important behind a load balancer or proxy?
Long-lived pooled connections can become stale: a database failover, a rolling deploy, or a TCP load balancer / proxy that silently drops idle connections can leave the pool holding dead sockets, causing intermittent errors on the next query. `SetConnMaxLifetime` bounds connection age so the pool periodically rotates connections, naturally rebalancing across new backend instances and shedding stale ones. A common value is a few minutes — short enough to recover from topology changes, long enough to avoid excessive reconnection. It also helps DB-side resource cleanup since servers often cap connection age too.🟢 Why must you always close sql.Rows, and what's the idiomatic pattern?
Each open `*sql.Rows` holds a connection from the pool until it is fully drained or explicitly closed; forgetting to close it leaks that connection, and under load the pool exhausts and queries hang. The idiomatic pattern is to `defer rows.Close()` immediately after a successful `Query`, loop with `rows.Next()`, scan, and then check `rows.Err()` after the loop to catch iteration errors. Note that `Close` is safe to call multiple times and `Next` auto-closes on EOF, but the explicit defer protects against early returns/errors.🟡 When should you use QueryRow versus Query, and how are errors handled?
Use `QueryRow` when you expect at most one row — it returns a `*sql.Row` and you call `Scan` directly on it. Crucially, `QueryRow` defers its error to `Scan`, so a no-rows result surfaces as `sql.ErrNoRows` from `Scan`, which you should check explicitly. Use `Query` for multiple rows. `QueryRow` conveniently manages the underlying connection for you (you don't call Close), whereas `Query` requires you to close the returned rows.🟢 How do parameterized queries prevent SQL injection?
Parameterized (prepared) queries send the SQL text and the argument values **separately** to the database, so user input is always treated as data, never as executable SQL — there is no way for a value to "break out" and inject commands. In `database/sql` you use placeholders (`?` for MySQL, `$1` for Postgres) and pass args as additional parameters; the driver/DB binds them safely. Never build SQL by string-concatenating user input with `fmt.Sprintf` — that is the classic injection vulnerability.🟡 What are prepared statements in database/sql and when do they help?
`db.Prepare` (or `tx.Prepare`) creates a `*sql.Stmt` that sends the SQL to the database once for parsing/planning, then lets you execute it many times with different arguments, saving repeated parse overhead. They also enforce parameterized binding, so they are inherently injection-safe. They help most for hot queries run in a tight loop; for one-off queries the convenience methods (`Query`/`Exec`) are fine and internally use a prepared statement anyway. Remember to `Close()` the statement, and be aware that with a pool a prepared statement may be re-prepared on different connections.🟡 Why prefer the *Context methods like QueryContext over Query?
The context-aware methods (`QueryContext`, `ExecContext`, `QueryRowContext`, `BeginTx`) let the database operation be **canceled** when the context is canceled — for instance when an HTTP client disconnects or a deadline passes. This frees the pooled connection and tells the driver to cancel the in-flight query instead of waiting for a slow result no one needs. In any request-scoped code you should thread `r.Context()` into these methods; the non-context variants are essentially `context.Background()` and can't be canceled.Transactions¶
🟢 How do you run a transaction in database/sql and why is the deferred Rollback pattern used?
You start with `db.BeginTx(ctx, opts)`, run your statements on the returned `*sql.Tx`, and finish with `Commit`. The idiomatic safety net is `defer tx.Rollback()` right after Begin: if you return early due to an error (or a panic), the rollback fires and releases the transaction; if you reach `Commit` first, the later `Rollback` is a harmless no-op (it returns `sql.ErrTxDone`, which you ignore). This guarantees you never leak an open transaction holding locks and a connection.tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, "UPDATE accounts SET bal = bal - $1 WHERE id=$2", amt, from); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, "UPDATE accounts SET bal = bal + $1 WHERE id=$2", amt, to); err != nil {
return err
}
return tx.Commit()