Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

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.

100 questions

Questions 41-60 of 100

  1. 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:

    1. Use chan struct{}, which carries no data.
    2. Release with defer, so slots return even on errors or panics.
    3. Acquire inside a select with ctx.Done(), so waiting goroutines can give up when the request is cancelled.
    4. 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
    }
  2. Q42.How do you implement rate limiting in Go with the standard library?

    For a simple fixed rate, a time.Ticker releases 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:

    1. Store the token count, capacity, refill rate and last refill time, protected by a mutex.
    2. On each request, add tokens for the elapsed time, up to the capacity.
    3. 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.

  3. 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:

    1. 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.
    2. Find the cycle in the stack traces.

    Prevention: consistent lock ordering, short critical sections, timeouts on waits and simple ownership of channels.

  4. Q44.How does `sync.Pool` work, and when does it help?

    A sync.Pool keeps 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. Goroutines Get an object, use it and Put it back.

    How it behaves:

    • If the pool is empty, Get calls the New function 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:

    1. Hot paths that allocate many short-lived, similar-sized objects, such as byte buffers per request.
    2. 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.
  5. Q45.How do timers and tickers work, and how can they cause problems?

    A time.Timer fires once after a duration, sending the time on its channel; a time.Ticker fires repeatedly at an interval. Both are used with select for timeouts, periodic work and delayed actions.

    TimerTicker
    Fires once after the durationFires repeatedly at the interval
    time.NewTimer(d), time.After(d), time.AfterFunc(d, f)time.NewTicker(d)
    Reuse with Reset for repeated timeoutsMust be stopped with Stop when no longer needed
    Best for timeouts and delaysBest for periodic jobs, such as flushing metrics

    Problems to avoid:

    1. Forgotten tickers: a ticker that is never stopped keeps running; always defer ticker.Stop().
    2. time.After in hot loops: each call creates a new timer; reuse one timer or use a context deadline.
    3. Slow handlers: if work takes longer than the ticker interval, ticks are dropped rather than queued, so periodic work silently runs less often.
    4. 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.

  6. 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:

    1. Never modify a published snapshot; build a new one for every change.
    2. Validate the new configuration fully before swapping, so a bad file never reaches handlers.
    3. Load once per operation: a handler should load the pointer once and use that snapshot throughout, so it stays consistent during the request.
    4. Log the reload with a version or hash, so behavior can be tied to a configuration version.

    Alternatives:

    • A sync.RWMutex around 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
    }
  7. Q47.What does `GOMAXPROCS` control, and how should you set it in containers?

    GOMAXPROCS sets 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 GOMAXPROCS to 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:

    1. Use a recent Go version: since Go 1.25, the runtime reads the container's processor limit and sets GOMAXPROCS to match, updating it if the limit changes.
    2. On older versions, set GOMAXPROCS explicitly from the quota through an environment variable, rounded down, with a minimum of one.
    3. 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 GOMAXPROCS above 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.

    1. GOMAXPROCS defaults to 32, so up to 32 threads run Go code in parallel.
    2. The quota allows 2 cores' worth of time per scheduling period, such as 200 ms of processor time per 100 ms period.
    3. Thirty-two busy threads use the 200 ms in about 6 ms, then the container is throttled for the remaining 94 ms.
    4. Requests caught in that pause see roughly 94 ms of extra latency; setting GOMAXPROCS=2 spreads the work evenly and removes the pauses.
  8. 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/pprof on an internal-only port, and you can collect processor, memory, goroutine, blocking and mutex profiles from a running service, then analyze them with go 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:

    1. Reproduce the problem under realistic load, in production or a load test.
    2. Collect a profile by pointing go tool pprof at the service's /debug/pprof/profile endpoint with seconds=30, from inside the network.
    3. Look at the top functions and the flame graph view.
    4. Fix the largest cost, then measure again.

    Safety: never expose profiling endpoints publicly; they reveal internals and cost resources. For tests, go test -cpuprofile and -memprofile produce the same data from benchmarks.

  9. 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.

    1. Confirm the trend: the goroutine count, from runtime metrics, rises steadily while traffic is flat, often with memory rising too.
    2. Collect a goroutine profile from the profiling endpoint, which groups goroutines by identical stack and counts them.
    3. Collect another later and compare: a stack whose count keeps growing is the leak.
    4. Read the top frame: usually a channel send or receive, a select, a lock or a network read without a deadline.
    5. 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.

  10. 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/trace with trace.Start and trace.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:

    1. Garbage collection pauses and assist time slowing request goroutines.
    2. Scheduler latency: goroutines that were runnable but waited for a processor, which suggests overloaded processors or a low GOMAXPROCS.
    3. Blocking: time waiting on channels, locks, network or system calls.
    4. 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.

  11. 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 body

    What this means for your code:

    1. Handlers must be safe for concurrent use: shared state needs locks or other synchronization.
    2. Use the request context: pass r.Context() to database and outgoing calls, so work stops when the client goes away.
    3. Headers are sent with the first write: set headers and the status before writing the body.
    4. 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.
    5. Keep-alive connections serve several requests in sequence on the same goroutine.
  12. 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.HandlerFunc turns 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:

    1. Order matters: the outermost middleware runs first, so recovery and logging usually wrap everything.
    2. Capture the status code by wrapping the response writer, since the standard writer does not expose it.
    3. Stop early correctly: write the error response and return without calling the next handler.
    4. Keep it composable: each middleware does one thing, and a small helper can chain them.

    Testing: httptest.NewRequest and httptest.NewRecorder exercise 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 unauthorized
  13. Q53.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 with r.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/latest beats /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:

    1. A path that matches but with the wrong method returns 405 Method Not Allowed, with an Allow header.
    2. 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
  14. Q54.How do you set HTTP server timeouts correctly in Go?

    The zero-value http.Server has 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.

    ReadHeaderTimeoutReadTimeoutWriteTimeoutIdleTimeout
    Time allowed to read the request headersTime allowed to read the whole request, including the bodyTime from the end of reading the headers until the response is writtenTime a kept-alive connection may sit idle between requests
    Protects against clients that send headers very slowlyProtects against slow uploads, but also limits legitimate large onesCaps how long a handler's response can takeFrees resources held by idle connections
    Typical: a few secondsTypical: depends on the largest expected bodyTypical: slightly above the slowest handlerTypical: longer than the load balancer's idle timeout

    Practical advice:

    1. Always set ReadHeaderTimeout; it is the most important protection.
    2. Use per-request deadlines inside handlers, through the context, for business timeouts, and keep WriteTimeout as a safety net.
    3. For streaming or long uploads, set wide server timeouts and use http.ResponseController to adjust deadlines for the specific request.
    4. Align IdleTimeout with proxies in front of the service, so the proxy never reuses a connection the server has just closed.
  15. 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.Shutdown does 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, exit

    Steps in code:

    1. Run ListenAndServe in a goroutine; it returns http.ErrServerClosed after shutdown begins, which is not an error.
    2. Use signal.NotifyContext to get a context cancelled on termination signals.
    3. Call Shutdown with a timeout context; if it expires, force-close remaining connections with Close.
    4. Stop background workers through their own contexts, and wait for them.

    Watch for: long-lived connections such as WebSockets, which Shutdown does not wait for; register cleanup with RegisterOnShutdown to close them.

  16. 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.

    1. Limit size: wrap the body with http.MaxBytesReader, so huge bodies fail with an error instead of filling memory.
    2. Decode strictly: json.NewDecoder with DisallowUnknownFields, so misspelled or unexpected fields are rejected.
    3. Reject trailing data: after decoding, check that nothing else follows the object.
    4. Validate: required fields, ranges, formats and allowed values, collecting clear messages.
    5. 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.Number or 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
  17. Q57.How do struct tags control JSON encoding in Go?

    Struct tags are string annotations on fields that packages read through reflection. The json tag 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: omitempty leaves 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 as time.Time, which omitempty never 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:

    1. Only exported fields, starting with an uppercase letter, are encoded.
    2. Decoding matches keys to field names case-insensitively, which can accept unexpected spellings.
    3. omitempty on a number hides a legitimate zero; use a pointer when zero is a meaningful value.
    4. Implement MarshalJSON and UnmarshalJSON for 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
  18. Q58.How do you make outgoing HTTP calls reliably with the standard client?

    Use a shared, configured http.Client with 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:

    1. One client per destination or purpose, reused across requests, so its transport can pool connections.
    2. Overall timeout: Client.Timeout caps the whole exchange, including reading the body.
    3. Transport settings: dial and TLS handshake timeouts, MaxIdleConnsPerHost large enough for your concurrency, and IdleConnTimeout below 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.

  19. Q59.How do you use `database/sql` correctly?

    database/sql provides a connection pool and a common interface over database drivers. A single *sql.DB is safe for concurrent use and should be created once at startup and shared; it is a pool, not a single connection.

    SetMaxOpenConnsSetMaxIdleConnsSetConnMaxLifetimeSetConnMaxIdleTime
    Caps total connections from this processKeeps this many unused connections readyRetires connections after this ageCloses connections idle for this long
    Protects the database from too many clientsAvoids reconnecting on every burstHelps with load balancers and server-side limitsFrees resources during quiet periods

    Usage rules:

    1. Pass contexts: QueryContext, QueryRowContext and ExecContext, so slow queries stop when the request is cancelled.
    2. Close rows: defer rows.Close(), iterate with rows.Next(), then check rows.Err(); unclosed rows hold a connection.
    3. Use placeholders for every value, never string formatting.
    4. Size the pool from the database's connection limit divided by the number of instances.
    5. Check sql.ErrNoRows from QueryRow().Scan to detect missing records.

    Monitor db.Stats(), especially wait counts and wait duration, which show when the pool is too small.

  20. 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:

    1. tx, err := db.BeginTx(ctx, nil), checking the error.
    2. defer tx.Rollback() immediately afterwards.
    3. Run statements with tx.ExecContext and tx.QueryContext, not with db, or they run outside the transaction on another connection.
    4. 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.TxOptions when 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.