Go Backend Developer Top 100 Interview Questions
100 Go backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the language core, goroutines and channels, context, the standard HTTP and database packages, testing, profiling, performance and system design, from the basics to production incidents.
Questions 41-60 of 100
Q41.How do you limit concurrency with a buffered channel used as a semaphore?
A buffered channel with capacity N can act as a semaphore: a goroutine sends a value to acquire a slot and receives a value to release it. When N goroutines hold slots, the next send blocks until one releases, so at most N run the protected section at once.
When to use it:
- Limiting calls to a database or external service to its safe concurrency.
- Bounding parallel file processing or image resizing.
- Starting one goroutine per item while still capping how many run at the same time.
Details:
- Use
chan struct{}, which carries no data. - Release with
defer, so slots return even on errors or panics. - Acquire inside a
selectwithctx.Done(), so waiting goroutines can give up when the request is cancelled. - Unlike a worker pool, this approach still creates one goroutine per item; for millions of items, a fixed pool reading from a channel uses less memory.
The extended Go packages also offer a weighted semaphore, useful when tasks need different amounts of capacity.
Example
Ten goroutines share a semaphore of three slots; the highest number running at once is three.
package main import ( "fmt" "sync" "sync/atomic" "time" ) func main() { sem := make(chan struct{}, 3) var active, peak atomic.Int32 var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() n := active.Add(1) for { p := peak.Load() if n <= p || peak.CompareAndSwap(p, n) { break } } time.Sleep(20 * time.Millisecond) active.Add(-1) }() } wg.Wait() fmt.Println(peak.Load()) // 3 }Q42.How do you implement rate limiting in Go with the standard library?
For a simple fixed rate, a
time.Tickerreleases one action per tick. For bursts with an average limit, a token bucket allows short bursts up to a capacity while refilling tokens at a steady rate. Both can be built from the standard library in a few lines.Ticker approach: each operation waits for the next tick, which spaces calls evenly, such as one call every 100 milliseconds for 10 per second. It does not allow bursts, and the ticker must be stopped when no longer needed.
Token bucket approach:
- Store the token count, capacity, refill rate and last refill time, protected by a mutex.
- On each request, add tokens for the elapsed time, up to the capacity.
- Allow the request if at least one token is available, and subtract it; otherwise reject or wait.
Design questions:
- Per client or global: a map of buckets keyed by client ID, with cleanup of idle entries.
- Reject or wait: APIs usually reject with status 429; outgoing calls usually wait.
- Distributed limits: buckets in memory work per process; a shared limit across many instances needs a shared store.
The extended Go packages include a production-ready token bucket limiter with waiting and burst support.
Q43.What causes deadlocks in Go, and how do you diagnose them?
A deadlock occurs when goroutines wait for each other in a cycle, so none can proceed. If every goroutine in the program is blocked, the runtime detects it and crashes with "all goroutines are asleep - deadlock!"; partial deadlocks, where only some goroutines are stuck, are not detected and silently stall work.
Common causes:
- Channels: sending on an unbuffered channel with no receiver, or two goroutines each waiting to receive from the other.
- Locks in different orders: goroutine A holds lock 1 and waits for lock 2, while goroutine B holds lock 2 and waits for lock 1.
- Re-locking: calling a method that locks a mutex from code already holding it; Go mutexes are not reentrant.
- Waiting on a WaitGroup whose counter never reaches zero, because a goroutine returned without
Done.
How to diagnose:
- Collect a goroutine dump, through the profiling endpoint or by sending a quit signal, and look for many goroutines blocked on the same lock or channel.
- Find the cycle in the stack traces.
Prevention: consistent lock ordering, short critical sections, timeouts on waits and simple ownership of channels.
Q44.How does `sync.Pool` work, and when does it help?
A
sync.Poolkeeps a set of temporary objects that can be reused, reducing allocations for objects created and discarded at high rates, such as buffers used to encode responses. GoroutinesGetan object, use it andPutit back.How it behaves:
- If the pool is empty,
Getcalls theNewfunction to create an object. - The runtime may drop pooled objects at any garbage collection, so a pool is a cache, not a guaranteed store.
- Each processor has its own local cache, so access is fast under concurrency.
When it helps:
- Hot paths that allocate many short-lived, similar-sized objects, such as byte buffers per request.
- When profiling shows allocation or garbage collection as a significant cost.
Pitfalls:
- Reset objects before reuse, such as
buf.Reset(), or data from one request leaks into another. - Do not pool very large objects that are only occasionally needed; they keep memory high.
- Do not store objects with external resources, such as open connections; use a proper connection pool.
- Measure: for cheap objects, a pool can be slower than allocating.
- If the pool is empty,
Q45.How do timers and tickers work, and how can they cause problems?
A
time.Timerfires once after a duration, sending the time on its channel; atime.Tickerfires repeatedly at an interval. Both are used withselectfor timeouts, periodic work and delayed actions.Timer Ticker Fires once after the duration Fires repeatedly at the interval time.NewTimer(d),time.After(d),time.AfterFunc(d, f)time.NewTicker(d)Reuse with Resetfor repeated timeoutsMust be stopped with Stopwhen no longer neededBest for timeouts and delays Best for periodic jobs, such as flushing metrics Problems to avoid:
- Forgotten tickers: a ticker that is never stopped keeps running; always
defer ticker.Stop(). time.Afterin hot loops: each call creates a new timer; reuse one timer or use a context deadline.- Slow handlers: if work takes longer than the ticker interval, ticks are dropped rather than queued, so periodic work silently runs less often.
- Drift: periodic jobs that must run at wall-clock times should compute the next run time instead of relying on intervals.
Since Go 1.23, unreferenced timers and tickers can be garbage collected even if not stopped, but stopping them is still the clearest code.
- Forgotten tickers: a ticker that is never stopped keeps running; always
Q46.How do you share configuration safely across goroutines when it can be reloaded?
Store the configuration as an immutable snapshot behind an
atomic.Pointer. Readers load the current pointer without locking; a reload builds a complete new snapshot and swaps the pointer in one step. Readers see either the old or the new configuration, never a mix.Rules:
- Never modify a published snapshot; build a new one for every change.
- Validate the new configuration fully before swapping, so a bad file never reaches handlers.
- Load once per operation: a handler should load the pointer once and use that snapshot throughout, so it stays consistent during the request.
- Log the reload with a version or hash, so behavior can be tied to a configuration version.
Alternatives:
- A
sync.RWMutexaround a struct works, but every reader pays for locking, and holding the read lock during slow work blocks reloads. - Restarting the process on configuration changes is simpler and often good enough when restarts are cheap.
This pattern also suits other read-heavy, rarely changing data, such as routing tables or feature flag sets.
Example
Readers always get a complete snapshot; the reload swaps the whole configuration at once.
package main import ( "fmt" "sync/atomic" "time" ) type Config struct { Timeout time.Duration Features map[string]bool } var current atomic.Pointer[Config] func describe() string { c := current.Load() return fmt.Sprint(c.Timeout, " search=", c.Features["search"]) } func main() { current.Store(&Config{Timeout: 2 * time.Second, Features: map[string]bool{"search": true}}) fmt.Println(describe()) // 2s search=true next := &Config{Timeout: 5 * time.Second, Features: map[string]bool{"search": false}} current.Store(next) fmt.Println(describe()) // 5s search=false }Q47.What does `GOMAXPROCS` control, and how should you set it in containers?
GOMAXPROCSsets how many operating system threads can execute Go code at the same time, which is the number of the scheduler's processors. By default it matches the number of logical cores available to the process.Why containers matter: a container may be limited to two cores by a processor quota while running on a 64-core machine. Older Go versions set
GOMAXPROCSto 64 there, so the program ran 64 threads of Go code inside a two-core quota, got throttled by the kernel in short bursts and saw high tail latency.What to do:
- Use a recent Go version: since Go 1.25, the runtime reads the container's processor limit and sets
GOMAXPROCSto match, updating it if the limit changes. - On older versions, set
GOMAXPROCSexplicitly from the quota through an environment variable, rounded down, with a minimum of one. - Check throttling metrics: frequent throttling with low average processor use is the classic sign of a mismatch.
Other notes: goroutines blocked in system calls do not count against the limit, and raising
GOMAXPROCSabove the available cores rarely helps.Example
A service runs in a container limited to 2 cores on a 32-core host, using an older Go version.
GOMAXPROCSdefaults to 32, so up to 32 threads run Go code in parallel.- The quota allows 2 cores' worth of time per scheduling period, such as 200 ms of processor time per 100 ms period.
- Thirty-two busy threads use the 200 ms in about 6 ms, then the container is throttled for the remaining 94 ms.
- Requests caught in that pause see roughly 94 ms of extra latency; setting
GOMAXPROCS=2spreads the work evenly and removes the pauses.
- Use a recent Go version: since Go 1.25, the runtime reads the container's processor limit and sets
Q48.How do you profile a Go service with `pprof`?
Go's profiler is built into the runtime and the standard library. Import
net/http/pprofon an internal-only port, and you can collect processor, memory, goroutine, blocking and mutex profiles from a running service, then analyze them withgo tool pprof.Profiles and what they show:
- CPU: where processor time goes, sampled over a period such as 30 seconds.
- Heap: memory in use and total allocations by call site.
- Goroutine: stacks of all goroutines, which reveals leaks and deadlocks.
- Block and mutex: where goroutines wait on channels and locks, enabled with sampling rates.
Workflow:
- Reproduce the problem under realistic load, in production or a load test.
- Collect a profile by pointing
go tool pprofat the service's/debug/pprof/profileendpoint withseconds=30, from inside the network. - Look at the top functions and the flame graph view.
- Fix the largest cost, then measure again.
Safety: never expose profiling endpoints publicly; they reveal internals and cost resources. For tests,
go test -cpuprofileand-memprofileproduce the same data from benchmarks.Q49.How do you find a goroutine leak in production?
Confirm the leak with metrics, collect goroutine profiles at two points in time and compare the stacks that keep growing. The growing stack shows exactly where goroutines are stuck.
- Confirm the trend: the goroutine count, from runtime metrics, rises steadily while traffic is flat, often with memory rising too.
- Collect a goroutine profile from the profiling endpoint, which groups goroutines by identical stack and counts them.
- Collect another later and compare: a stack whose count keeps growing is the leak.
- Read the top frame: usually a channel send or receive, a
select, a lock or a network read without a deadline. - Trace it to the code that started the goroutine, and find the missing exit path.
Typical fixes:
- Buffer result channels so senders can finish after a timeout.
- Add
ctx.Done()cases to blocking selects. - Set deadlines on network calls and close response bodies.
- Stop tickers and close channels that end loops.
Add a regression test that checks the goroutine count after the code runs, and keep the goroutine count on a dashboard with an alert.
Q50.How do you use the execution tracer to understand latency in a Go program?
The execution tracer records detailed events from the runtime: when goroutines start, block and resume, garbage collection phases, system calls and processor use. Where a CPU profile shows where time is spent computing, a trace shows why a request waited.
How to collect a trace:
- From a running service: the profiling endpoint's trace handler, for a few seconds.
- From tests:
go test -trace trace.out. - From code:
runtime/tracewithtrace.Startandtrace.Stop. - Recent Go versions also support a flight recorder that keeps the last few seconds in memory, so you can save a trace right after a slow request happens.
What to look for in
go tool trace:- Garbage collection pauses and assist time slowing request goroutines.
- Scheduler latency: goroutines that were runnable but waited for a processor, which suggests overloaded processors or a low
GOMAXPROCS. - Blocking: time waiting on channels, locks, network or system calls.
- Uneven processor use: some processors idle while others are busy.
Annotate your code with tasks and regions for important operations, so the trace viewer shows request-level timelines.
Q51.What happens when an HTTP request reaches a Go server built on `net/http`?
The server accepts the connection and starts a goroutine for it. That goroutine reads and parses each request, finds the handler through the router and calls it with a response writer and the request. Every request runs concurrently, in its own goroutine, with a context that is cancelled when the client disconnects.
sequenceDiagram participant C as Client participant S as http.Server participant G as Connection goroutine participant M as ServeMux router participant H as Handler C->>S: Open connection S->>G: Start goroutine for the connection C->>G: Send request G->>M: Parsed request M->>H: ServeHTTP with writer and request H-->>C: Status, headers and bodyWhat this means for your code:
- Handlers must be safe for concurrent use: shared state needs locks or other synchronization.
- Use the request context: pass
r.Context()to database and outgoing calls, so work stops when the client goes away. - Headers are sent with the first write: set headers and the status before writing the body.
- Panics in handlers are recovered by the server for that connection, but the response is lost; add middleware that returns a proper 500 and logs the stack.
- Keep-alive connections serve several requests in sequence on the same goroutine.
Q52.How do you write HTTP handlers and middleware with the standard library?
A handler is anything with a
ServeHTTP(http.ResponseWriter, *http.Request)method;http.HandlerFuncturns an ordinary function into one. Middleware is a function that takes a handler and returns a new handler that adds behavior before or after calling the original.Typical middleware:
- Logging and metrics with the response status and duration.
- Authentication and authorization checks that stop the chain early.
- Recovering from panics and returning a 500 response.
- Adding request IDs and timeouts to the context.
Writing it well:
- Order matters: the outermost middleware runs first, so recovery and logging usually wrap everything.
- Capture the status code by wrapping the response writer, since the standard writer does not expose it.
- Stop early correctly: write the error response and
returnwithout calling the next handler. - Keep it composable: each middleware does one thing, and a small helper can chain them.
Testing:
httptest.NewRequestandhttptest.NewRecorderexercise handlers and middleware in memory, without opening network ports.Example
A logging middleware wraps an authentication middleware; the wrong key stops the chain with a 401.
package main import ( "fmt" "net/http" "net/http/httptest" "strings" ) type statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } func logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) fmt.Printf("%s %s -> %d\n", r.Method, r.URL.Path, rec.status) }) } func requireKey(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-API-Key") != "secret" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } func main() { hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello") }) handler := logging(requireKey(hello)) for _, key := range []string{"secret", "wrong"} { req := httptest.NewRequest("GET", "/hello", nil) req.Header.Set("X-API-Key", key) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) fmt.Println(rec.Code, strings.TrimSpace(rec.Body.String())) } } // Output: // GET /hello -> 200 // 200 hello // GET /hello -> 401 // 401 unauthorizedQ53.How do method and path patterns work in the standard `ServeMux`?
Since Go 1.22, the standard router supports HTTP methods and path variables in its patterns, which covers the needs of many services without a third-party router.
Pattern features:
- Methods:
"GET /orders/{id}"matches only GET, and also HEAD for GET patterns. - Wildcards:
{id}matches one path segment; read it withr.PathValue("id"). - Remaining path:
{path...}matches the rest of the path, including slashes. - Exact match:
{$}matches only the path itself, such as"/{$}"for the root alone. - Hosts: patterns can start with a host name to route by domain.
How conflicts are resolved: the most specific pattern wins, so
/orders/latestbeats/orders/{id}for that exact path. Registering two patterns that are equally specific and overlap causes a panic at registration, which catches mistakes at startup.Automatic responses:
- A path that matches but with the wrong method returns 405 Method Not Allowed, with an
Allowheader. - A path with no match returns 404.
Validate path values yourself, such as parsing an ID as a number, since patterns only match text.
Example
Five requests show path values, a remaining-path wildcard and automatic 405 and 404 responses.
package main import ( "fmt" "net/http" "net/http/httptest" "strings" ) func main() { mux := http.NewServeMux() mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "order %s", r.PathValue("id")) }) mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) fmt.Fprint(w, "created") }) mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "file %s", r.PathValue("path")) }) for _, target := range []string{"GET /orders/42", "POST /orders", "DELETE /orders/42", "GET /files/a/b.txt", "GET /missing"} { method, path, _ := strings.Cut(target, " ") rec := httptest.NewRecorder() mux.ServeHTTP(rec, httptest.NewRequest(method, path, nil)) fmt.Println(rec.Code, strings.TrimSpace(rec.Body.String())) } } // Output: // 200 order 42 // 201 created // 405 Method Not Allowed // 200 file a/b.txt // 404 404 page not found- Methods:
Q54.How do you set HTTP server timeouts correctly in Go?
The zero-value
http.Serverhas no timeouts at all, so a slow or malicious client can hold connections and goroutines forever. Set explicit timeouts for every phase of a connection.ReadHeaderTimeoutReadTimeoutWriteTimeoutIdleTimeoutTime allowed to read the request headers Time allowed to read the whole request, including the body Time from the end of reading the headers until the response is written Time a kept-alive connection may sit idle between requests Protects against clients that send headers very slowly Protects against slow uploads, but also limits legitimate large ones Caps how long a handler's response can take Frees resources held by idle connections Typical: a few seconds Typical: depends on the largest expected body Typical: slightly above the slowest handler Typical: longer than the load balancer's idle timeout Practical advice:
- Always set
ReadHeaderTimeout; it is the most important protection. - Use per-request deadlines inside handlers, through the context, for business timeouts, and keep
WriteTimeoutas a safety net. - For streaming or long uploads, set wide server timeouts and use
http.ResponseControllerto adjust deadlines for the specific request. - Align
IdleTimeoutwith proxies in front of the service, so the proxy never reuses a connection the server has just closed.
- Always set
Q55.How do you shut down a Go HTTP server gracefully with `http.Server.Shutdown`?
Catch the termination signal, stop accepting new connections and let in-flight requests finish within a time limit, then close resources such as database pools.
http.Server.Shutdowndoes the middle part: it closes listeners, closes idle connections and waits for active ones to finish.sequenceDiagram participant O as Orchestrator participant M as main participant S as http.Server participant R as In-flight requests O->>M: Termination signal M->>M: Mark not ready, wait briefly for load balancers M->>S: Shutdown with a 20-second context S->>S: Stop accepting, close idle connections R-->>S: Finish S-->>M: Shutdown returns M->>M: Close database pool, flush logs, exitSteps in code:
- Run
ListenAndServein a goroutine; it returnshttp.ErrServerClosedafter shutdown begins, which is not an error. - Use
signal.NotifyContextto get a context cancelled on termination signals. - Call
Shutdownwith a timeout context; if it expires, force-close remaining connections withClose. - Stop background workers through their own contexts, and wait for them.
Watch for: long-lived connections such as WebSockets, which
Shutdowndoes not wait for; register cleanup withRegisterOnShutdownto close them.- Run
Q56.How do you decode and validate JSON request bodies safely?
Limit the body size, decode into a struct that describes exactly what you accept, reject unknown fields and validate values before using them. Each step closes a common hole: memory exhaustion, silently ignored typos, and invalid data reaching business logic.
- Limit size: wrap the body with
http.MaxBytesReader, so huge bodies fail with an error instead of filling memory. - Decode strictly:
json.NewDecoderwithDisallowUnknownFields, so misspelled or unexpected fields are rejected. - Reject trailing data: after decoding, check that nothing else follows the object.
- Validate: required fields, ranges, formats and allowed values, collecting clear messages.
- Respond with 400 or 413 and a message that names the problem, without echoing internal details.
Type considerations:
- Use pointers or custom types when you need to tell a missing field from a zero value.
- Use
json.Numberor strings for very large or precise numbers, such as money in minor units. - Decode directly from the request body; there is no need to read it all into memory first.
Example
A strict decoder accepts a valid body and rejects unknown fields, wrong types, oversized bodies and missing values.
package main import ( "encoding/json" "errors" "fmt" "io" "net/http" "strings" ) type CreateOrder struct { CustomerID string `json:"customer_id"` Items int `json:"items"` } func decode(body string) (CreateOrder, error) { r := http.MaxBytesReader(nil, io.NopCloser(strings.NewReader(body)), 64) dec := json.NewDecoder(r) dec.DisallowUnknownFields() var req CreateOrder if err := dec.Decode(&req); err != nil { return req, err } if dec.More() { return req, errors.New("body must contain a single object") } if req.CustomerID == "" || req.Items < 1 { return req, errors.New("customer_id and a positive items count are required") } return req, nil } func main() { bodies := []string{ `{"customer_id":"c-1","items":2}`, `{"customer_id":"c-1","items":2,"admin":true}`, `{"customer_id":"c-1","items":"two"}`, `{"customer_id":"` + strings.Repeat("x", 80) + `","items":1}`, `{"customer_id":"","items":0}`, } for _, b := range bodies { req, err := decode(b) if err != nil { fmt.Println("error:", err) continue } fmt.Printf("ok: %+v\n", req) } } // Output: // ok: {CustomerID:c-1 Items:2} // error: json: unknown field "admin" // error: json: cannot unmarshal string into Go struct field CreateOrder.items of type int // error: http: request body too large // error: customer_id and a positive items count are required- Limit size: wrap the body with
Q57.How do struct tags control JSON encoding in Go?
Struct tags are string annotations on fields that packages read through reflection. The
jsontag sets the field's name in JSON and options that control when it is included.Common options:
- Rename:
json:"created_at"maps the field to that key. - Skip empty values:
omitemptyleaves out zero numbers, empty strings, nil pointers and empty slices and maps. - Skip zero values:
omitzero, added in Go 1.24, leaves out values that are zero, including structs such astime.Time, whichomitemptynever omits. - Ignore:
json:"-"never encodes or decodes the field, useful for internal data such as password hashes. - Encode as string:
json:",string"writes a number as a JSON string, for clients that cannot handle large integers.
Rules to remember:
- Only exported fields, starting with an uppercase letter, are encoded.
- Decoding matches keys to field names case-insensitively, which can accept unexpected spellings.
omitemptyon a number hides a legitimate zero; use a pointer when zero is a meaningful value.- Implement
MarshalJSONandUnmarshalJSONfor full control over a type's format.
Example
Hidden, empty and zero fields are left out, and decoding matches a key regardless of letter case.
package main import ( "encoding/json" "fmt" "time" ) type Product struct { ID int `json:"id"` Name string `json:"name"` Price float64 `json:"price,omitempty"` Internal string `json:"-"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at,omitzero"` } func main() { p := Product{ID: 7, Name: "Lamp", Internal: "cost-sheet-12", CreatedAt: time.Date(2026, 3, 1, 9, 30, 0, 0, time.UTC)} out, _ := json.Marshal(p) fmt.Println(string(out)) var back Product _ = json.Unmarshal([]byte(`{"id":8,"NAME":"Desk"}`), &back) fmt.Println(back.ID, back.Name) } // Output: // {"id":7,"name":"Lamp","created_at":"2026-03-01T09:30:00Z"} // 8 Desk- Rename:
Q58.How do you make outgoing HTTP calls reliably with the standard client?
Use a shared, configured
http.Clientwith timeouts, pass a context on every request, always read and close response bodies and check the status code yourself. The default client has no timeout, so a stuck server can block a goroutine forever.Client configuration:
- One client per destination or purpose, reused across requests, so its transport can pool connections.
- Overall timeout:
Client.Timeoutcaps the whole exchange, including reading the body. - Transport settings: dial and TLS handshake timeouts,
MaxIdleConnsPerHostlarge enough for your concurrency, andIdleConnTimeoutbelow the server's keep-alive timeout.
Per request:
http.NewRequestWithContext(ctx, ...)so the call stops when the incoming request is cancelled.defer resp.Body.Close()after checking the error, and read the body to the end, even when discarding it, so the connection can be reused.- Check
resp.StatusCode; non-2xx responses are not returned as errors. - Limit how much of the body you read with
io.LimitReader.
Retries: retry only idempotent requests and temporary failures, with backoff and jitter, within the context's deadline.
Q59.How do you use `database/sql` correctly?
database/sqlprovides a connection pool and a common interface over database drivers. A single*sql.DBis safe for concurrent use and should be created once at startup and shared; it is a pool, not a single connection.SetMaxOpenConnsSetMaxIdleConnsSetConnMaxLifetimeSetConnMaxIdleTimeCaps total connections from this process Keeps this many unused connections ready Retires connections after this age Closes connections idle for this long Protects the database from too many clients Avoids reconnecting on every burst Helps with load balancers and server-side limits Frees resources during quiet periods Usage rules:
- Pass contexts:
QueryContext,QueryRowContextandExecContext, so slow queries stop when the request is cancelled. - Close rows:
defer rows.Close(), iterate withrows.Next(), then checkrows.Err(); unclosed rows hold a connection. - Use placeholders for every value, never string formatting.
- Size the pool from the database's connection limit divided by the number of instances.
- Check
sql.ErrNoRowsfromQueryRow().Scanto detect missing records.
Monitor
db.Stats(), especially wait counts and wait duration, which show when the pool is too small.- Pass contexts:
Q60.How do you run transactions with `database/sql`?
Start a transaction with
BeginTx, run every statement on the returned*sql.Tx, commit if all succeed and roll back otherwise. A deferred rollback is the simplest way to guarantee cleanup: after a successful commit, the rollback does nothing.Pattern:
tx, err := db.BeginTx(ctx, nil), checking the error.defer tx.Rollback()immediately afterwards.- Run statements with
tx.ExecContextandtx.QueryContext, not withdb, or they run outside the transaction on another connection. return tx.Commit()at the end.
Good practices:
- Wrap it in a helper that takes a function, so every transaction follows the same pattern and callers cannot forget the rollback.
- Keep transactions short: no network calls to other services inside, since locks and the connection stay held.
- Set the isolation level in
sql.TxOptionswhen the default is not enough, and retry the whole function on serialization failures. - Do not use the transaction from several goroutines at once; a transaction is tied to one connection.
Avoid side effects inside retried transactions, such as sending emails, because the function may run more than once.