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 81-100 of 100

  1. Q81.How would you design a high-throughput event ingestion service in Go?

    Accept events quickly, validate them, buffer them in memory with strict limits and write them to durable storage in batches. Go's goroutines and channels fit this pipeline well, but the design must be explicit about backpressure and data loss.

    flowchart LR
      A["HTTP handlers: validate and accept"] --> B["Bounded channel buffer"]
      B --> C["Batch writers: flush by size or time"]
      C --> D["Durable queue or storage"]
      B -->|Full| E["Reject with 503 and a retry delay"]
      C --> F["Metrics: lag, batch size, failures"]

    Design points:

    1. Fast acceptance: handlers validate, then try a non-blocking send to a bounded channel; when full, they reject with 503, so clients back off instead of the service running out of memory.
    2. Batching: writer goroutines collect events until a batch size or a short time limit, such as 500 events or 100 ms, then write them in one call.
    3. Durability choice: acknowledging before the write is fast but loses buffered events on a crash; acknowledging after the write is safer but slower.
    4. Retries: writers retry failed batches with backoff, and send persistent failures to a dead-letter store.
    5. Graceful shutdown: stop accepting, drain the buffer and flush the final batch before exiting.
    6. Horizontal scaling: stateless instances behind a load balancer.
  2. Q82.How would you design a background job worker service in Go?

    Workers pull jobs from a durable queue, process them with limited concurrency and acknowledge each job only after it succeeds. Failed jobs are retried with backoff, and jobs that keep failing move to a dead-letter queue for investigation.

    flowchart TD
      A["Durable job queue"] --> B["Fetcher goroutine"]
      B --> C["Worker pool with N goroutines"]
      C --> D{"Job succeeded?"}
      D -->|Yes| E["Acknowledge"]
      D -->|No, attempts left| F["Requeue with backoff delay"]
      D -->|No, attempts exhausted| G["Dead-letter queue and alert"]

    Key decisions:

    1. Idempotent jobs: queues usually deliver at least once, so a job may run twice; use idempotency keys or check state before acting.
    2. Visibility timeout: longer than the slowest job, or extended while the job runs, so another worker does not pick it up mid-processing.
    3. Concurrency per job type, so slow jobs, such as report generation, cannot starve quick ones, such as emails.
    4. Context per job with a deadline, so stuck jobs are cancelled.
    5. Panic recovery in each worker goroutine, turning panics into failed attempts.
    6. Graceful shutdown: stop fetching, finish in-flight jobs within a deadline and leave unfinished ones to be redelivered.

    Monitor queue age, job duration, failure rates and dead-letter growth per job type.

  3. Q83.How would you build a reverse proxy or simple API gateway in Go?

    The standard library's httputil.ReverseProxy forwards requests to backends and streams responses back. Around it, you add routing, authentication, rate limits, timeouts and observability, which together make a small gateway.

    flowchart LR
      A["Client"] --> B["Gateway middleware: auth, rate limit, request ID"]
      B --> C["Router by path or host"]
      C --> D["ReverseProxy to orders backend"]
      C --> E["ReverseProxy to search backend"]
      D --> F["Shared transport with pooled connections"]
      E --> F

    Implementation points:

    1. One proxy per backend, using httputil.NewSingleHostReverseProxy or a custom Rewrite function that sets the target and forwarding headers.
    2. A tuned transport: connection pools, dial and response header timeouts, shared across proxies.
    3. Error handling: an ErrorHandler that returns 502 or 504 with a clear body and logs the cause.
    4. Header hygiene: remove hop-by-hop headers, strip incoming internal headers that clients should not set and add forwarding information.
    5. Middleware for authentication, rate limiting, request IDs and metrics per route.
    6. Health-aware routing when there are several backend instances, skipping unhealthy ones.

    Streaming: ReverseProxy streams bodies and supports flushing, so large downloads and server-sent events work without buffering everything in memory.

  4. Q84.How would you design a Go client for a third-party API with strict rate limits?

    Wrap the API in one client that every part of the service shares, and enforce the provider's limits inside it: a rate limiter, a concurrency cap, timeouts, retries that respect the provider's signals and clear errors for callers.

    flowchart LR
      A["Callers across the service"] --> B["Shared API client"]
      B --> C["Token bucket at the allowed rate"]
      C --> D["Concurrency limit"]
      D --> E["HTTP call with timeout"]
      E --> F{"Response"}
      F -->|429 or 503| G["Wait for the suggested delay, then retry"]
      F -->|Success| H["Return typed result"]

    Design points:

    1. Limit below the quota, such as 90% of the allowed rate, leaving room for clock differences and other clients sharing the same key.
    2. Wait or fail: callers pass a context; the limiter waits for a token unless the context expires first.
    3. Respect Retry-After and back off with jitter on rate-limit and server errors.
    4. Cache responses that do not change often, which reduces calls.
    5. Batch requests when the API supports it.
    6. Distributed limits: if many instances share one quota, coordinate through a shared store, or divide the quota per instance.
    7. Typed errors for rate limiting, so callers can decide to degrade gracefully.

    Track calls, rate-limit responses and wait time as metrics.

  5. Q85.How would you implement a generic LRU cache in Go?

    Combine a map from keys to list elements with a doubly linked list ordered by recency, from container/list. The map finds entries in constant time, and moving an element to the front or removing the back is also constant time.

    Operations:

    • Get: look up the element; if present, move it to the front and return its value.
    • Put: update and move an existing element, or push a new one to the front; if the size exceeds capacity, remove the back element and delete its key from the map.

    Design considerations:

    1. Generics: LRU[K comparable, V any] gives type safety without type assertions at call sites.
    2. Concurrency: this version is not safe for concurrent use; wrap operations in a mutex, or shard the cache by key hash to reduce contention.
    3. Capacity by size: for values of very different sizes, track bytes instead of counts.
    4. Expiry: add a timestamp per entry if data can become stale.

    Example

    After reading a, inserting c evicts b, the least recently used key.

    package main
    
    import (
    	"container/list"
    	"fmt"
    )
    
    type entry[K comparable, V any] struct {
    	key   K
    	value V
    }
    
    type LRU[K comparable, V any] struct {
    	capacity int
    	order    *list.List
    	items    map[K]*list.Element
    }
    
    func NewLRU[K comparable, V any](capacity int) *LRU[K, V] {
    	return &LRU[K, V]{capacity: capacity, order: list.New(), items: make(map[K]*list.Element)}
    }
    
    func (c *LRU[K, V]) Get(key K) (V, bool) {
    	if el, ok := c.items[key]; ok {
    		c.order.MoveToFront(el)
    		return el.Value.(*entry[K, V]).value, true
    	}
    	var zero V
    	return zero, false
    }
    
    func (c *LRU[K, V]) Put(key K, value V) {
    	if el, ok := c.items[key]; ok {
    		el.Value.(*entry[K, V]).value = value
    		c.order.MoveToFront(el)
    		return
    	}
    	c.items[key] = c.order.PushFront(&entry[K, V]{key, value})
    	if c.order.Len() > c.capacity {
    		oldest := c.order.Back()
    		c.order.Remove(oldest)
    		delete(c.items, oldest.Value.(*entry[K, V]).key)
    	}
    }
    
    func main() {
    	cache := NewLRU[string, int](2)
    	cache.Put("a", 1)
    	cache.Put("b", 2)
    	cache.Get("a")
    	cache.Put("c", 3)
    
    	_, hasB := cache.Get("b")
    	v, hasA := cache.Get("a")
    	fmt.Println(hasB, hasA, v) // false true 1
    }
  6. Q86.How would you count word frequencies across many lines concurrently in Go?

    Split the input among a few worker goroutines, let each count into its own local map and merge the partial maps at the end. Local maps avoid locking on every word; merging once is cheap.

    Steps:

    1. Send lines to workers through a channel, and close it when done.
    2. Each worker counts words into a private map, then sends the map on a results channel when its input ends.
    3. A coordinator closes the results channel after all workers finish.
    4. Merge the partial maps, then sort by count and word for a stable output.

    Why not one shared map with a mutex: every word would take the lock, so workers would mostly wait for each other. Private maps scale with the number of cores.

    Considerations for real data:

    • Normalize words: lowercase and strip punctuation.
    • For files, read with a bufio.Scanner and send batches of lines rather than single lines, which reduces channel overhead.
    • For very large vocabularies, merge partial results incrementally to limit memory.

    Example

    Three workers count words from four lines; ties in count are ordered by word.

    package main
    
    import (
    	"fmt"
    	"slices"
    	"strings"
    	"sync"
    )
    
    func main() {
    	lines := []string{"go is fast", "go is simple", "simple is good", "Go routines are cheap"}
    	jobs := make(chan string)
    	partials := make(chan map[string]int)
    
    	var wg sync.WaitGroup
    	for w := 0; w < 3; w++ {
    		wg.Add(1)
    		go func() {
    			defer wg.Done()
    			counts := map[string]int{}
    			for line := range jobs {
    				for _, word := range strings.Fields(strings.ToLower(line)) {
    					counts[word]++
    				}
    			}
    			partials <- counts
    		}()
    	}
    	go func() {
    		for _, l := range lines {
    			jobs <- l
    		}
    		close(jobs)
    	}()
    	go func() {
    		wg.Wait()
    		close(partials)
    	}()
    
    	total := map[string]int{}
    	for p := range partials {
    		for w, n := range p {
    			total[w] += n
    		}
    	}
    	words := make([]string, 0, len(total))
    	for w := range total {
    		words = append(words, w)
    	}
    	slices.SortFunc(words, func(a, b string) int {
    		if total[a] != total[b] {
    			return total[b] - total[a]
    		}
    		return strings.Compare(a, b)
    	})
    	for _, w := range words[:3] {
    		fmt.Printf("%s=%d ", w, total[w])
    	}
    	fmt.Println()
    }
    // Output:
    // go=3 is=3 simple=2
  7. Q87.How would you merge several sorted slices efficiently in Go?

    Use a min-heap holding the current smallest element of each slice. Repeatedly pop the smallest element, append it to the output and push the next element from the same slice. With k slices and n elements in total, this takes O(n log k) time.

    Using container/heap:

    1. Define a slice type of items holding the value, the source slice index and the position in that slice.
    2. Implement Len, Less, Swap, Push and Pop for it.
    3. Initialize the heap with the first element of every non-empty slice.
    4. Pop and push until the heap is empty.

    Where it is used:

    • Merging sorted results from several database shards or partitions.
    • Combining sorted log files or event streams by timestamp.
    • External sorting, where sorted chunks on disk are merged.

    Variations: for streams, read the next element lazily from each source instead of holding whole slices, which keeps memory proportional to k rather than n. For only two slices, a simple two-pointer merge is faster.

    Example

    Four sorted slices, one of them empty, merge into one sorted result.

    package main
    
    import (
    	"container/heap"
    	"fmt"
    )
    
    type item struct{ value, list, index int }
    
    type minHeap []item
    
    func (h minHeap) Len() int           { return len(h) }
    func (h minHeap) Less(i, j int) bool { return h[i].value < h[j].value }
    func (h minHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
    func (h *minHeap) Push(x any)        { *h = append(*h, x.(item)) }
    func (h *minHeap) Pop() any {
    	old := *h
    	last := old[len(old)-1]
    	*h = old[:len(old)-1]
    	return last
    }
    
    func mergeSorted(lists [][]int) []int {
    	h := &minHeap{}
    	for i, l := range lists {
    		if len(l) > 0 {
    			heap.Push(h, item{l[0], i, 0})
    		}
    	}
    	var out []int
    	for h.Len() > 0 {
    		it := heap.Pop(h).(item)
    		out = append(out, it.value)
    		if next := it.index + 1; next < len(lists[it.list]) {
    			heap.Push(h, item{lists[it.list][next], it.list, next})
    		}
    	}
    	return out
    }
    
    func main() {
    	fmt.Println(mergeSorted([][]int{{1, 4, 9}, {2, 3, 10}, {}, {5}})) // [1 2 3 4 5 9 10]
    }
  8. Q88.How would you implement retries with exponential backoff that respect a context in Go?

    Call the operation in a loop; after each failure, wait for an exponentially growing delay with random jitter, and stop when the operation succeeds, attempts run out or the context is cancelled. Waiting inside a select with ctx.Done() makes the retry loop stop immediately when the caller gives up.

    Design points:

    1. Exponential delay: base << attempt, capped at a maximum.
    2. Jitter: randomize part of each delay, so many clients do not retry at the same moment.
    3. Only retry retryable errors: pass a function, or check for error types, so validation errors fail immediately.
    4. Wrap the final error with the number of attempts, and keep the last cause inspectable with %w.
    5. Return the context error when cancelled, combined with the last operation error, so callers see both.

    Where it belongs: in clients for remote calls, and only at one layer of the call chain, to avoid multiplying retries.

    Example

    An operation that succeeds on the third attempt, one that always fails, and one stopped by the context deadline.

    package main
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"math/rand/v2"
    	"time"
    )
    
    func retry(ctx context.Context, attempts int, base time.Duration, op func() error) error {
    	var err error
    	for i := 0; i < attempts; i++ {
    		if err = op(); err == nil {
    			return nil
    		}
    		if i == attempts-1 {
    			break
    		}
    		delay := base << i
    		delay = delay/2 + time.Duration(rand.Int64N(int64(delay/2)+1))
    		select {
    		case <-time.After(delay):
    		case <-ctx.Done():
    			return errors.Join(err, ctx.Err())
    		}
    	}
    	return fmt.Errorf("after %d attempts: %w", attempts, err)
    }
    
    func main() {
    	calls := 0
    	err := retry(context.Background(), 5, 10*time.Millisecond, func() error {
    		calls++
    		if calls < 3 {
    			return errors.New("temporary")
    		}
    		return nil
    	})
    	fmt.Println(err, calls) // <nil> 3
    
    	err = retry(context.Background(), 3, 5*time.Millisecond, func() error { return errors.New("down") })
    	fmt.Println(err) // after 3 attempts: down
    
    	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
    	defer cancel()
    	err = retry(ctx, 5, 100*time.Millisecond, func() error { return errors.New("down") })
    	fmt.Println(errors.Is(err, context.DeadlineExceeded)) // true
    }
  9. Q89.How would you implement a thread-safe in-memory cache with expiry in Go?

    Store values with an expiry time in a map protected by a mutex, treat expired entries as missing on reads and remove them periodically so memory does not grow forever. Inject the clock, so expiry can be tested without waiting.

    Operations:

    • Set: store the value with now + ttl.
    • Get: return the value only if it exists and has not expired; expired entries are reported as missing.
    • Cleanup: scan and delete expired entries, run by a background goroutine on a ticker that stops with a context.

    Design considerations:

    1. Lock granularity: a single mutex is simple and usually fast enough; shard by key hash under heavy contention.
    2. Memory bounds: expiry alone does not cap size; add a maximum entry count with eviction for untrusted keys.
    3. Load coalescing: when a missing key is expensive to load, let one goroutine load it while others wait.
    4. Copying values: returning pointers to cached data lets callers modify shared state; store immutable values or return copies.

    Example

    A fake clock moves time forward: the entry expires after its time-to-live, and cleanup removes it.

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    type ttlItem[V any] struct {
    	value   V
    	expires time.Time
    }
    
    type TTLCache[K comparable, V any] struct {
    	mu    sync.Mutex
    	items map[K]ttlItem[V]
    	ttl   time.Duration
    	now   func() time.Time
    }
    
    func NewTTLCache[K comparable, V any](ttl time.Duration, now func() time.Time) *TTLCache[K, V] {
    	return &TTLCache[K, V]{items: make(map[K]ttlItem[V]), ttl: ttl, now: now}
    }
    
    func (c *TTLCache[K, V]) Set(key K, value V) {
    	c.mu.Lock()
    	defer c.mu.Unlock()
    	c.items[key] = ttlItem[V]{value, c.now().Add(c.ttl)}
    }
    
    func (c *TTLCache[K, V]) Get(key K) (V, bool) {
    	c.mu.Lock()
    	defer c.mu.Unlock()
    	it, ok := c.items[key]
    	if !ok || !c.now().Before(it.expires) {
    		var zero V
    		return zero, false
    	}
    	return it.value, true
    }
    
    func (c *TTLCache[K, V]) Cleanup() int {
    	c.mu.Lock()
    	defer c.mu.Unlock()
    	removed := 0
    	for k, it := range c.items {
    		if !c.now().Before(it.expires) {
    			delete(c.items, k)
    			removed++
    		}
    	}
    	return removed
    }
    
    func main() {
    	current := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
    	cache := NewTTLCache[string, int](10*time.Second, func() time.Time { return current })
    
    	cache.Set("session", 42)
    	current = current.Add(5 * time.Second)
    	fmt.Println(cache.Get("session")) // 42 true
    	current = current.Add(6 * time.Second)
    	fmt.Println(cache.Get("session")) // 0 false
    	fmt.Println(cache.Cleanup())      // 1
    }
  10. Q90.How would you aggregate a large log file line by line in Go?

    Read the file with a bufio.Scanner, which returns one line at a time, parse each line strictly, update per-key statistics in a map and print the results sorted by key. Memory stays proportional to the number of distinct keys, not the file size.

    Steps:

    1. Open the file, and defer file.Close().
    2. Create a scanner; raise its buffer limit with scanner.Buffer if lines can exceed the default of 64 KB.
    3. Split each line into fields and convert numbers with strconv, counting lines that fail instead of crashing.
    4. Accumulate counts, sums and errors per key.
    5. Check scanner.Err() after the loop; a read error otherwise looks like a short file.
    6. Sort keys for stable output.

    Scaling further: for compressed files, wrap the reader with a decompressor; for many files, process them in parallel with a worker pool and merge the maps.

    Example

    Four valid lines are aggregated per route; one malformed line is counted and skipped.

    package main
    
    import (
    	"bufio"
    	"fmt"
    	"slices"
    	"strconv"
    	"strings"
    )
    
    type stats struct{ count, errors, totalMs int }
    
    func main() {
    	input := "GET /jobs 200 120\nGET /jobs 500 900\nPOST /apply 201 300\nbad line\nGET /jobs 200 80\n"
    	scanner := bufio.NewScanner(strings.NewReader(input))
    
    	byRoute := map[string]*stats{}
    	skipped := 0
    	for scanner.Scan() {
    		f := strings.Fields(scanner.Text())
    		if len(f) != 4 {
    			skipped++
    			continue
    		}
    		status, err1 := strconv.Atoi(f[2])
    		ms, err2 := strconv.Atoi(f[3])
    		if err1 != nil || err2 != nil {
    			skipped++
    			continue
    		}
    		key := f[0] + " " + f[1]
    		s := byRoute[key]
    		if s == nil {
    			s = &stats{}
    			byRoute[key] = s
    		}
    		s.count++
    		s.totalMs += ms
    		if status >= 500 {
    			s.errors++
    		}
    	}
    	if err := scanner.Err(); err != nil {
    		fmt.Println("read error:", err)
    		return
    	}
    
    	keys := make([]string, 0, len(byRoute))
    	for k := range byRoute {
    		keys = append(keys, k)
    	}
    	slices.Sort(keys)
    	for _, k := range keys {
    		s := byRoute[k]
    		fmt.Printf("%s count=%d errors=%d avg=%d\n", k, s.count, s.errors, (s.totalMs+s.count/2)/s.count)
    	}
    	fmt.Println("skipped:", skipped)
    }
    // Output:
    // GET /jobs count=3 errors=1 avg=367
    // POST /apply count=1 errors=0 avg=300
    // skipped: 1
  11. Q91.How would you write a bounded parallel map that preserves the order of results in Go?

    Write each result into a preallocated slice at its input's index, limit concurrency with a semaphore channel and cancel the remaining work on the first error. Writing by index preserves order without sorting, and each goroutine touches only its own slot, so no lock is needed for the results.

    Design points:

    1. Generic signature: ParallelMap[T, U any](ctx, in []T, limit int, f func(context.Context, T) (U, error)) ([]U, error).
    2. Semaphore: acquire a slot before starting each goroutine, release it when the goroutine ends.
    3. First error wins: record it once with sync.Once, cancel the context and stop starting new work.
    4. Wait for everything before returning, so no goroutine writes to the slice afterwards.
    5. Pass the context to f, so in-flight work can stop early after a failure.

    Use cases: calling an external service for each item in a list, resizing images or validating records, with a concurrency limit that protects the dependency.

    Example

    Results come back in input order despite different completion times; a failing item cancels the rest.

    package main
    
    import (
    	"context"
    	"fmt"
    	"sync"
    	"time"
    )
    
    func ParallelMap[T, U any](ctx context.Context, in []T, limit int, f func(context.Context, T) (U, error)) ([]U, error) {
    	ctx, cancel := context.WithCancel(ctx)
    	defer cancel()
    	out := make([]U, len(in))
    	sem := make(chan struct{}, limit)
    	var wg sync.WaitGroup
    	var once sync.Once
    	var firstErr error
    
    	for i, v := range in {
    		sem <- struct{}{}
    		if ctx.Err() != nil {
    			<-sem
    			break
    		}
    		wg.Add(1)
    		go func() {
    			defer wg.Done()
    			defer func() { <-sem }()
    			u, err := f(ctx, v)
    			if err != nil {
    				once.Do(func() {
    					firstErr = err
    					cancel()
    				})
    				return
    			}
    			out[i] = u
    		}()
    	}
    	wg.Wait()
    	if firstErr != nil {
    		return nil, firstErr
    	}
    	return out, nil
    }
    
    func main() {
    	square := func(ctx context.Context, n int) (int, error) {
    		time.Sleep(time.Duration(7-n) * time.Millisecond)
    		return n * n, nil
    	}
    	res, err := ParallelMap(context.Background(), []int{1, 2, 3, 4, 5, 6}, 3, square)
    	fmt.Println(res, err) // [1 4 9 16 25 36] <nil>
    
    	failing := func(ctx context.Context, n int) (int, error) {
    		if n == 4 {
    			return 0, fmt.Errorf("item %d: bad input", n)
    		}
    		return n, nil
    	}
    	_, err = ParallelMap(context.Background(), []int{1, 2, 3, 4, 5, 6}, 2, failing)
    	fmt.Println(err) // item 4: bad input
    }
  12. Q92.How do you evolve a Go library's API without breaking its users?

    Follow semantic versioning strictly: add features in minor versions, fix bugs in patch versions and change the import path for breaking changes. Go's module system enforces the last rule: a new major version must be imported as a different path, such as /v2.

    Adding without breaking:

    1. New functions and types are always safe to add.
    2. Option structs or functional options let you add configuration without changing function signatures.
    3. New methods on interfaces break implementers; add a new interface, and check for it with a type assertion, instead of extending an existing one.
    4. Struct fields can be added, but callers using unkeyed struct literals will break; document that keyed literals are expected.

    Deprecating:

    • Mark old identifiers with a Deprecated: comment explaining the replacement, which tools show to users.
    • Keep deprecated code working until the next major version.

    Releasing a major version:

    • Create the /v2 module path, update internal imports and publish migration notes.
    • Consider keeping v1 maintained for security fixes for a while.
    • Provide adapters where possible, so users can migrate gradually.

    Run tests against example user code in continuous integration to catch accidental breaking changes.

  13. Q93.How do you secure a Go web service?

    Combine secure defaults in the HTTP server, careful handling of input and secrets, strong authentication and a well-maintained dependency set. Go's standard library is a strong foundation, but secure configuration is still your job.

    HTTP layer:

    • Set server timeouts, especially ReadHeaderTimeout, and request body limits.
    • Serve TLS with modern settings, or terminate it at a trusted proxy.
    • Set security headers and strict CORS rules for browser clients.

    Input and output:

    • Validate every input, decode JSON strictly and use parameterized queries.
    • Use html/template for HTML, which escapes output automatically, never text/template.
    • Clean file paths from users, and serve files from an os.Root or fs.FS so requests cannot escape the intended directory.

    Secrets and cryptography:

    1. Use crypto/rand for tokens and keys, never math/rand.
    2. Hash passwords with a slow password hashing function from the extended crypto packages.
    3. Compare secrets with crypto/subtle.ConstantTimeCompare.
    4. Load secrets from the environment or a secret store, and never log them.

    Dependencies and builds: run the official vulnerability checker in continuous integration, keep Go and modules updated and build with -trimpath from a clean environment.

  14. Q94.What is cgo, and why do many teams avoid it?

    cgo lets Go code call C libraries. It is useful when a mature C library has no Go equivalent, but it gives up several of Go's main advantages.

    Pure GoUsing cgo
    Static binaries that run on minimal imagesUsually needs the C library and its dependencies at run time
    Simple cross-compilation with GOOS and GOARCHCross-compiling needs a C toolchain for each target
    Fast buildsSlower builds, with a C compiler involved
    Go's tools see all the code: race detector, profiler, memory safetyC code is invisible to many Go tools and can corrupt memory
    Function calls are cheapEach call between Go and C has significant overhead

    When cgo is reasonable:

    1. Required system libraries, such as hardware drivers or platform-specific features.
    2. Performance-critical C libraries without a good Go alternative, called in large chunks of work rather than many tiny calls.

    How to limit the cost: isolate cgo in one small package behind a Go interface, build with CGO_ENABLED=0 everywhere else and test the boundary carefully, especially memory ownership and pointer-passing rules.

  15. Q95.How do you build and release Go binaries for several platforms?

    Go cross-compiles natively: set GOOS and GOARCH and build, with no extra toolchain needed for pure Go code. A release pipeline builds each target, embeds version information, produces checksums and signs the artifacts.

    Build steps:

    1. Targets: one build per pair of GOOS, the operating system, and GOARCH, the processor architecture, such as amd64 and arm64; go tool dist list prints every supported pair.
    2. Static builds: CGO_ENABLED=0 for portable binaries.
    3. Reproducibility: -trimpath, a pinned Go version and go mod download from verified checksums, so the same commit produces the same binary.
    4. Version information: Go embeds module and version control details automatically, readable with go version -m or runtime/debug.ReadBuildInfo; -ldflags "-X main.version=..." can set custom values.

    Release artifacts:

    • Archives per platform, a checksum file and signatures.
    • Container images for server deployments, built for several architectures.
    • Release notes generated from commits or changelog entries.

    Testing: run tests on each target architecture where possible, since rare issues, such as alignment of 64-bit atomic values on 32-bit platforms, only appear there.

  16. Q96.How do you upgrade a service to a new Go version safely?

    Go has a strong compatibility promise, so upgrades are usually smooth, but behavior can still change in edge cases. Upgrade deliberately: read the release notes, update the toolchain in one place, run all tests and roll out gradually.

    1. Read the release notes, looking at changes to the standard library packages you use and new GODEBUG settings.
    2. Update the go and toolchain lines in go.mod; the go line also controls language semantics, such as per-iteration loop variables from Go 1.22.
    3. Run tests with -race, linters and the vulnerability checker with the new version.
    4. Benchmark hot paths, since compiler and runtime changes can shift performance in either direction.
    5. Roll out gradually: a canary, then the rest, watching latency, memory and garbage collection metrics.
    6. Use GODEBUG to restore old behavior temporarily if a change breaks something, while you fix the code.

    Keep current: each Go release is supported until two newer major releases exist, so upgrading every six months or so avoids large jumps and keeps security fixes flowing.

  17. Q97.What do you look for when reviewing Go code for correctness?

    Many Go bugs come from a small set of patterns that compile cleanly but fail at run time or under load. Reviewing for these specifically catches most of them before production.

    Checklist:

    1. Errors: ignored with _, logged and returned twice, or wrapped with %v where callers need %w.
    2. Goroutines: every goroutine has an exit path, owners wait for them and panics are recovered where needed.
    3. Context: passed as the first parameter, used in all blocking calls and never stored in structs.
    4. Concurrency: shared maps and slices protected, and locks never held during input and output.
    5. Resources: response bodies, rows and files closed, and defer not used inside long loops.
    6. Nil handling: writes to nil maps, typed nil errors returned through interfaces and nil pointer dereferences.
    7. Slices: append results assigned, and shared backing arrays understood.
    8. Timeouts: HTTP servers and clients configured, not the zero values.
    9. Tests: table-driven cases for edge conditions, and the race detector in continuous integration.

    Let tools do the routine work: gofmt, go vet and a standard linter configuration in continuous integration, so human review focuses on design and logic.

    Interview tip: naming two or three concrete bugs you caught in review, and the lint rule you added afterwards, is far more convincing than listing principles.

  18. Q98.How does Go compare with other common approaches for a new backend service?

    Choose by the workload, the team and the ecosystem the service depends on. Go is a strong default for network services, but it is not the best fit for every project.

    GoGarbage-collected virtual machine languagesInterpreted dynamic languages
    Compiles to a single static binary with fast startupRuns on a managed runtime that needs warming upRuns through an interpreter, usually with a large package ecosystem
    Lightweight goroutines for high concurrencyMature threading and very large enterprise ecosystemsConcurrency often limited, or based on asynchronous code
    Small language, quick to learn and readRich language features and frameworksVery fast to prototype and script
    Best for network services, infrastructure tools and APIsBest for large domain-heavy systems with existing librariesBest for data work, scripting and rapid product iteration

    Questions that decide it:

    1. Which libraries does the service need, and do good Go versions exist?
    2. What does the team already know and operate well?
    3. Are startup time, memory use and deployment simplicity important?
    4. Is the work mostly concurrent input and output, where Go excels, or heavy numeric computing, where specialized ecosystems lead?

    Consistency matters too: running many languages raises the cost of tooling, libraries and on-call knowledge.

  19. Q99.How should you explain a Go backend project you personally worked on?

    Tell one project as a story about a service and its trade-offs: what it did, what you built, the hardest problem and the measured outcome. Interviewers want evidence that you understand Go's runtime and concurrency, not a list of packages.

    1. Context: what the service did, its traffic and its users.
    2. Your role: what you designed or built personally.
    3. Design choices: how you structured packages, handled concurrency, managed database access and configured timeouts, and why.
    4. A hard problem: a goroutine leak, a latency issue, a data race or a scaling limit, and the profiles, traces or tests that revealed the cause.
    5. Quality: tests, the race detector, benchmarks and how deployments were made safe.
    6. Results: numbers such as "p99 latency fell from 400 ms to 90 ms" or "memory dropped by 60% after removing per-request allocations".
    7. Reflection: what you would change now.

    What the interviewer is listening for:

    • Comfort with goroutines, channels, contexts and their failure modes.
    • Data-driven debugging with profiles and metrics.
    • Simple, explicit design choices that fit Go's style.

    Interview tip: be ready to sketch the goroutine and channel structure of your service on a whiteboard, since many follow-up questions start there.

  20. Q100.How do you discuss a production incident you handled in a Go service?

    Describe the incident in order, from detection to prevention, and highlight the Go-specific evidence you used: goroutine dumps, heap and processor profiles, execution traces and runtime metrics.

    1. Detection: the alert or report, and how quickly it fired.
    2. Impact: what users experienced and for how long.
    3. Mitigation: rollback, restart, traffic shift or feature switch, and why you chose it.
    4. Diagnosis: for example, a goroutine profile showing thousands of goroutines blocked on a channel send, or a heap profile pointing to an unbounded map.
    5. Root cause and contributing factors: such as a missing timeout or a context that was not passed through.
    6. Fix: the code change, plus tests that reproduce the problem.
    7. Prevention: new alerts on goroutine count or heap growth, lint rules, review checklist items or load tests.

    What the interviewer is listening for:

    • Mitigating first and investigating with evidence, not guesses.
    • Understanding of how Go programs fail under load.
    • Blameless, system-focused language.
    • Lasting improvements, verified after the fix.