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 61-80 of 100

  1. Q61.How do you prevent SQL injection in Go?

    Always pass values as query parameters, never build SQL text by joining strings with user input. With placeholders, the driver sends values separately from the query, so input can never change the structure of the statement.

    Safe patterns:

    • db.QueryContext(ctx, "SELECT name FROM users WHERE email = $1", email), with the placeholder style your driver uses.
    • Dynamic filters: build the query from fixed fragments chosen in code, and add values to the argument list.
    • Identifiers, such as sort columns or table names, cannot be parameterized; map allowed user choices to fixed column names through a lookup table.
    • IN lists: generate one placeholder per value, rather than joining values into the text.

    Defense in depth:

    1. Run the service with a database user that has only the permissions it needs.
    2. Validate input types and ranges at the API boundary.
    3. Review any use of fmt.Sprintf near SQL in code review, and use static analysis that flags it.
    4. Log queries without parameter values that contain personal data.

    Query builders and generated code from SQL files also prevent injection, provided they use parameters internally.

  2. Q62.How do you handle NULL database values in Go?

    Go's basic types cannot be null, so scanning a NULL into a string or int fails. Choose a representation that makes the absence of a value explicit.

    sql.Null typesPointersCoalesce in SQL
    sql.NullString, sql.NullInt64 or the generic sql.Null[T], with a Valid flagScan into *string or *int64, which is nil for NULLReplace NULL in the query, such as COALESCE(phone, '')
    Explicit and clear in data access codeWorks naturally with JSON, where nil becomes nullKeeps Go types simple
    Need converting before use or encodingNeed nil checks before useLoses the difference between NULL and the replacement value

    Choosing one:

    1. Use pointers or sql.Null[T] when the difference between "unknown" and "empty" matters, such as an optional middle name.
    2. Use COALESCE when a default is genuinely correct for the business.
    3. Prefer NOT NULL columns with defaults in the schema when absence has no meaning, which removes the problem at the source.

    Keep the conversion in the data access layer, so the rest of the code uses clear domain types.

  3. Q63.How do you structure a Go backend project as it grows?

    Start simple and add structure only when the code needs it. A common layout keeps main small, puts business logic in packages organized by domain and isolates infrastructure, such as databases and HTTP, behind small interfaces defined where they are used.

    flowchart TD
      A["cmd/api: main wires everything together"] --> B["internal/orders: domain logic"]
      A --> C["internal/payments: domain logic"]
      A --> D["internal/http: handlers and middleware"]
      D --> B
      D --> C
      B --> E["internal/storage: database access"]
      C --> F["internal/clients: external services"]

    Principles:

    1. main wires dependencies: reads configuration, creates the database pool and clients, constructs services and starts the server.
    2. Domain packages hold business rules and do not import HTTP or database packages directly.
    3. Interfaces defined by consumers: the orders package declares the small storage interface it needs.
    4. internal prevents other modules from depending on your implementation.
    5. Tests next to code, in the same package for unit tests.

    Avoid: copying deep layouts from other languages, with many layers and packages named models, services and utils; they create indirection without benefit in Go.

  4. Q64.How do you use dependency injection in Go without a framework?

    Pass dependencies explicitly through constructors. A service receives the interfaces and values it needs, such as a store, a clock or a logger, and main creates the real implementations and passes them in. Tests pass fakes instead.

    Why plain constructors work well in Go:

    • Dependencies are visible in the function signature.
    • The compiler checks that everything is provided.
    • No reflection or code generation is needed for most services.

    Guidelines:

    1. Small interfaces declared in the consuming package, with only the methods it calls.
    2. Inject time and randomness, such as a now func() time.Time, so behavior is deterministic in tests.
    3. Validate in constructors, returning an error for missing or invalid dependencies.
    4. Keep wiring in one place, usually main or a small setup function.
    5. Avoid global variables for dependencies, which hide coupling and make parallel tests unsafe.

    Code generation tools for wiring exist, but most teams only need them when the dependency graph becomes very large.

    Example

    The service depends on a small store interface and an injected clock; a fake store and fixed time make the output predictable.

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    type Order struct {
    	ID       string
    	PlacedAt time.Time
    }
    
    type OrderStore interface {
    	Save(ctx context.Context, o Order) error
    }
    
    type Service struct {
    	store OrderStore
    	now   func() time.Time
    }
    
    func NewService(store OrderStore, now func() time.Time) *Service {
    	return &Service{store: store, now: now}
    }
    
    func (s *Service) Place(ctx context.Context, id string) (Order, error) {
    	o := Order{ID: id, PlacedAt: s.now()}
    	if err := s.store.Save(ctx, o); err != nil {
    		return Order{}, fmt.Errorf("place order %s: %w", id, err)
    	}
    	return o, nil
    }
    
    type memoryStore struct{ saved []Order }
    
    func (m *memoryStore) Save(_ context.Context, o Order) error {
    	m.saved = append(m.saved, o)
    	return nil
    }
    
    func main() {
    	store := &memoryStore{}
    	fixed := func() time.Time { return time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC) }
    	svc := NewService(store, fixed)
    
    	o, err := svc.Place(context.Background(), "ord-1")
    	fmt.Println(o.ID, o.PlacedAt.Format(time.RFC3339), err) // ord-1 2026-03-01T10:00:00Z <nil>
    	fmt.Println(len(store.saved))                          // 1
    }
  5. Q65.How do you write table-driven tests in Go?

    Put the test cases in a slice of structs, each with a name, the input and the expected output, then loop over them and run each one as a subtest with t.Run. Adding a case is one line, and failures report exactly which case broke.

    Structure:

    1. A tests slice with fields such as name, in and want.
    2. A loop calling t.Run(tt.name, func(t *testing.T) { ... }).
    3. A comparison that reports the input, the result and the expectation in the failure message.

    Good practices:

    • Name cases by behavior, such as "extra spaces", so failures are easy to understand.
    • Include edge cases: empty input, boundaries and invalid values.
    • Use t.Errorf to keep going after a failure, and t.Fatalf only when the test cannot continue.
    • Run subtests in parallel with t.Parallel() when cases are independent.
    • Run one case with go test -run 'TestSlug/punctuation'.
    • Compare structs with reflect.DeepEqual or the comparison helpers in slices and maps.

    The standard testing package covers most needs without assertion libraries.

    Example

    Four cases run as named subtests; spaces in names become underscores in the output.

    package slug
    
    import (
    	"strings"
    	"testing"
    	"unicode"
    )
    
    func Slug(s string) string {
    	var b strings.Builder
    	dash := false
    	for _, r := range strings.ToLower(strings.TrimSpace(s)) {
    		if unicode.IsLetter(r) || unicode.IsDigit(r) {
    			b.WriteRune(r)
    			dash = false
    		} else if !dash && b.Len() > 0 {
    			b.WriteByte('-')
    			dash = true
    		}
    	}
    	return strings.TrimSuffix(b.String(), "-")
    }
    
    func TestSlug(t *testing.T) {
    	tests := []struct{ name, in, want string }{
    		{"simple", "Senior Backend Developer", "senior-backend-developer"},
    		{"punctuation", "Engineer (Remote)!", "engineer-remote"},
    		{"extra spaces", "  Data   Engineer  ", "data-engineer"},
    		{"empty", "", ""},
    	}
    	for _, tt := range tests {
    		t.Run(tt.name, func(t *testing.T) {
    			if got := Slug(tt.in); got != tt.want {
    				t.Errorf("Slug(%q) = %q, want %q", tt.in, got, tt.want)
    			}
    		})
    	}
    }
    // Output:
    // === RUN   TestSlug
    // === RUN   TestSlug/simple
    // === RUN   TestSlug/punctuation
    // === RUN   TestSlug/extra_spaces
    // === RUN   TestSlug/empty
    // --- PASS: TestSlug
    //     --- PASS: TestSlug/simple
    //     --- PASS: TestSlug/punctuation
    //     --- PASS: TestSlug/extra_spaces
    //     --- PASS: TestSlug/empty
    // PASS

    The output is from go test -v, with timings removed.

  6. Q66.How do you test HTTP code with `httptest`?

    The net/http/httptest package provides two tools: a recorder that captures a handler's response in memory, and a real test server on a local port. Together they cover both server-side handlers and client code without external dependencies.

    httptest.NewRecorder:

    • Call the handler directly with httptest.NewRequest and a recorder.
    • Check rec.Code, headers and the body.
    • Fast and ideal for unit tests of handlers and middleware.

    httptest.NewServer:

    • Starts a real HTTP server on a random local port with your handler.
    • Use it to test client code: point the client at server.URL and control the responses, including errors, slow replies and malformed bodies.
    • Always defer server.Close().

    What to test:

    1. Status codes and response bodies for success and failure paths.
    2. Headers, such as content type and cache control.
    3. Timeouts, by making the test server sleep longer than the client's timeout.
    4. Retries, by failing the first request and succeeding on the second.

    Example

    A test server returns JSON, and a client with a timeout reads it over a real local connection.

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"net/http"
    	"net/http/httptest"
    	"time"
    )
    
    func main() {
    	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		w.Header().Set("Content-Type", "application/json")
    		json.NewEncoder(w).Encode(map[string]string{"status": "ok", "path": r.URL.Path})
    	}))
    	defer server.Close()
    
    	client := &http.Client{Timeout: 2 * time.Second}
    	resp, err := client.Get(server.URL + "/health")
    	if err != nil {
    		fmt.Println("error:", err)
    		return
    	}
    	defer resp.Body.Close()
    
    	var body map[string]string
    	json.NewDecoder(resp.Body).Decode(&body)
    	fmt.Println(resp.StatusCode, resp.Header.Get("Content-Type"), body["status"], body["path"])
    }
    // Output:
    // 200 application/json ok /health
  7. Q67.How do you replace dependencies with fakes in Go tests?

    Define small interfaces for the dependencies your code calls, then write simple fake implementations in the test that record calls and return chosen results. Hand-written fakes are usually clearer than generated mocks for small interfaces.

    Kinds of test doubles:

    • Fakes: working lightweight implementations, such as an in-memory store.
    • Stubs: return fixed results or errors to drive a code path.
    • Spies: record what was called and with which arguments, for later checks.

    Guidelines:

    1. Keep interfaces small, so fakes stay small too.
    2. Test behavior, not calls: prefer checking outcomes, such as the saved order, over asserting exact call sequences, which makes tests brittle.
    3. Make failure paths easy: a fake with an err field lets one test check how the code handles a database error.
    4. Use real implementations when cheap: an httptest.Server or a temporary directory is often better than a fake.
    5. Integration tests still matter: run some tests against a real database to catch query mistakes that fakes cannot.

    Function fields, such as type fakeClock struct{ now func() time.Time }, are a lightweight way to stub single behaviors.

  8. Q68.How do benchmarks and fuzz tests work in Go?

    Both are built into the standard testing package. Benchmarks measure how fast code runs and how much it allocates; fuzz tests generate many random inputs to find crashes and broken invariants that hand-written cases miss.

    BenchmarksFuzz tests
    Functions named BenchmarkXxx(b *testing.B)Functions named FuzzXxx(f *testing.F)
    Run with go test -bench . -benchmemRun with go test -fuzz FuzzXxx
    Report time per operation and allocationsReport inputs that cause failures, and save them as regression cases
    Use b.Loop() to run the measured code repeatedlyUse f.Add to seed inputs, then check properties of the results

    Writing useful benchmarks:

    1. Benchmark realistic inputs, such as typical payload sizes.
    2. Compare versions with a statistics tool over several runs, since single runs are noisy.
    3. Keep results from being optimized away by using them.

    Writing useful fuzz tests:

    1. Target parsers, decoders and anything handling untrusted input.
    2. Check properties, such as "encoding then decoding returns the original" or "the function never panics".
    3. Commit the failing inputs the fuzzer finds; they run as normal tests afterwards.
  9. Q69.How do you log in Go with `log/slog`?

    log/slog is the standard library's structured logger. It writes records with a level, a message and key-value attributes, through handlers that output text or JSON. Structured logs can be filtered and searched by field, which makes incidents far easier to investigate.

    Key features:

    • Levels: Debug, Info, Warn and Error, with a minimum level set on the handler.
    • Attributes: logger.Info("order loaded", "order_id", 42) adds typed fields.
    • Child loggers: logger.With("request_id", id) attaches fields to every later record.
    • Groups: nest related attributes under one key.
    • Context: InfoContext(ctx, ...) lets handlers pull values, such as trace IDs, from the context.

    Good practices:

    1. Create one JSON logger at startup for production, and pass it, or a child, to components.
    2. Add a request-scoped logger in middleware with request ID and route.
    3. Never log secrets or sensitive personal data; use ReplaceAttr to redact known keys.
    4. Log errors once, where they are handled, not at every layer.

    Example

    A request-scoped logger adds fields to every record; the time field is removed here only to keep the output stable.

    package main
    
    import (
    	"errors"
    	"log/slog"
    	"os"
    )
    
    func main() {
    	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    		Level: slog.LevelInfo,
    		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
    			if a.Key == slog.TimeKey {
    				return slog.Attr{}
    			}
    			return a
    		},
    	}))
    
    	reqLogger := logger.With("request_id", "r-123", "route", "/orders/{id}")
    	reqLogger.Info("order loaded", "order_id", 42, "duration_ms", 12)
    	reqLogger.Debug("not shown below the Info level")
    	reqLogger.Error("payment failed", "err", errors.New("card declined"))
    }
    // Output:
    // {"level":"INFO","msg":"order loaded","request_id":"r-123","route":"/orders/{id}","order_id":42,"duration_ms":12}
    // {"level":"ERROR","msg":"payment failed","request_id":"r-123","route":"/orders/{id}","err":"card declined"}
  10. Q70.How do you expose health checks and runtime metrics from a Go service?

    Expose separate endpoints for liveness and readiness, and publish metrics about requests, dependencies and the Go runtime itself. The standard library provides the building blocks; metrics systems usually add a client library on top.

    Health endpoints:

    • Liveness: returns 200 if the process is running and not stuck; keep it trivial.
    • Readiness: returns 200 only when the service can serve traffic, such as after startup completes and before shutdown begins, and when critical local resources are available.

    Metrics to publish:

    1. Requests: rate, errors and latency histograms per route template.
    2. Dependencies: latency and errors of database and outgoing calls, plus connection pool statistics from db.Stats().
    3. Runtime: goroutine count, heap size, garbage collection pauses and scheduling latency from runtime/metrics.

    Standard library options: expvar publishes variables as JSON on /debug/vars, and runtime/metrics gives a stable list of runtime measurements to export.

    Serve debug and metrics endpoints on an internal port, separate from the public API.

  11. Q71.How do you configure a Go service from environment variables and flags?

    Load configuration once at startup into a typed struct, combining defaults, environment variables and command-line flags in a clear order of precedence, then validate it and fail fast with a helpful message if anything is wrong.

    A common precedence order: defaults, then environment variables, then flags, so operators can override anything for a single run.

    Practices:

    1. Typed fields: parse durations, numbers and booleans at startup, so bad values fail immediately rather than at the first request.
    2. One place: a load function returns the config struct; the rest of the code receives it explicitly.
    3. Testable: pass the arguments and an environment lookup function into load, instead of reading globals, so tests can supply values.
    4. Secrets: read them from files or a secret store provided by the platform, and never log them.
    5. Document every setting with a flag description and a default.

    The standard flag package handles flags, including durations, and os.LookupEnv distinguishes unset variables from empty ones.

    Example

    Environment values override defaults, a flag overrides the environment and an invalid duration fails with a clear error.

    package main
    
    import (
    	"flag"
    	"fmt"
    	"time"
    )
    
    type Config struct {
    	Addr    string
    	Timeout time.Duration
    	Debug   bool
    }
    
    func load(args []string, getenv func(string) string) (Config, error) {
    	cfg := Config{Addr: ":8080", Timeout: 5 * time.Second}
    	if v := getenv("APP_ADDR"); v != "" {
    		cfg.Addr = v
    	}
    	if v := getenv("APP_TIMEOUT"); v != "" {
    		d, err := time.ParseDuration(v)
    		if err != nil {
    			return cfg, fmt.Errorf("APP_TIMEOUT: %w", err)
    		}
    		cfg.Timeout = d
    	}
    	fs := flag.NewFlagSet("server", flag.ContinueOnError)
    	fs.StringVar(&cfg.Addr, "addr", cfg.Addr, "listen address")
    	fs.DurationVar(&cfg.Timeout, "timeout", cfg.Timeout, "request timeout")
    	fs.BoolVar(&cfg.Debug, "debug", false, "debug logging")
    	return cfg, fs.Parse(args)
    }
    
    func main() {
    	env := map[string]string{"APP_ADDR": ":9000", "APP_TIMEOUT": "3s"}
    	cfg, err := load([]string{"-timeout", "10s"}, func(k string) string { return env[k] })
    	fmt.Printf("%+v %v\n", cfg, err)
    
    	_, err = load(nil, func(k string) string {
    		if k == "APP_TIMEOUT" {
    			return "ten"
    		}
    		return ""
    	})
    	fmt.Println(err)
    }
    // Output:
    // {Addr::9000 Timeout:10s Debug:false} <nil>
    // APP_TIMEOUT: time: invalid duration "ten"
  12. Q72.How do you build small, secure container images for Go services?

    Compile a static binary in a build stage, then copy only that binary into a minimal final image that contains little besides certificates and time zone data. Go makes this easy because a pure Go program needs no runtime or shared libraries.

    1. Static build: set CGO_ENABLED=0 so the binary does not depend on the system C library.
    2. Reproducible flags: -trimpath removes local paths, and -ldflags can embed the version and commit.
    3. Multi-stage build: compile with the full Go toolchain image, then copy the binary into a minimal base image, or an empty one.
    4. Include what the binary needs: trusted certificate authorities for outgoing TLS, and time zone data, which Go can also embed with the time/tzdata package.
    5. Run as a non-root user, with a read-only file system where possible.
    6. Pin base images by digest and rebuild regularly for security patches.

    Benefits: images of a few megabytes to tens of megabytes, fast pulls and fewer packages to patch. The trade-off is that minimal images have no shell for debugging, so rely on logs, profiles and temporary debug containers instead.

  13. Q73.How do you embed static files into a Go binary?

    The embed package lets you include files and directories in the compiled binary with a //go:embed directive above a variable. The files travel with the executable, so deployments need no separate asset directory.

    Ways to embed:

    • A single file into a string or []byte, such as a default configuration or a SQL schema.
    • Many files into an embed.FS, a read-only file system that works with http.FileServerFS, template.ParseFS and other standard functions.

    Rules:

    1. Paths are relative to the source file's directory and cannot use ...
    2. Hidden files and those starting with _ are excluded unless the pattern uses the all: prefix.
    3. The directive must sit directly above a package-level variable declaration.

    Common uses:

    • HTML templates and static web assets for small admin interfaces.
    • Database migration files run at startup.
    • Default configuration and test fixtures.

    Trade-offs: changing an embedded file requires rebuilding the binary, and large assets make the binary bigger. For frequently changing or very large content, serve files from storage instead.

  14. Q74.How do you handle time and time zones correctly in Go?

    Store and exchange times in UTC with a standard format, convert to a user's time zone only for display and use durations and the monotonic clock for measuring elapsed time.

    Key facts about Go's time package:

    • Layouts use a reference time: formats are written as the date 2006-01-02 15:04:05, not with letter codes.
    • time.Time includes a location; t.In(loc) changes how it is displayed, not the moment it represents.
    • Monotonic clock: time.Now() includes a monotonic reading, so time.Since(start) is correct even if the wall clock jumps.
    • Compare with methods: use Equal, Before and After, not ==, which also compares locations and monotonic data.
    • Durations are typed, such as 5 * time.Second, which avoids unit mistakes.

    Practical rules:

    1. Load time zones by name with time.LoadLocation, and embed the zone database with time/tzdata for minimal images.
    2. Use RFC3339 in APIs and logs.
    3. Be careful with calendar arithmetic: adding 24 hours is not always "the next day" across daylight saving changes; use AddDate for calendar days.
    4. Inject the clock into code that depends on the current time, so tests are deterministic.

    Example

    A UTC time is displayed in another zone, durations print readably and parsing without a zone gives UTC.

    package main
    
    import (
    	"fmt"
    	"time"
    	_ "time/tzdata"
    )
    
    func main() {
    	loc, err := time.LoadLocation("Asia/Kolkata")
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	t := time.Date(2026, 3, 1, 9, 30, 0, 0, time.UTC)
    	fmt.Println(t.In(loc).Format(time.RFC3339)) // 2026-03-01T15:00:00+05:30
    	fmt.Println(t.Equal(t.In(loc)))             // true
    
    	d := 90 * time.Minute
    	fmt.Println(d, d.Hours()) // 1h30m0s 1.5
    
    	parsed, _ := time.Parse("2006-01-02 15:04", "2026-03-01 18:45")
    	fmt.Println(parsed.Weekday(), parsed.AddDate(0, 0, 1).Format("Jan 2 15:04 MST")) // Sunday Mar 2 18:45 UTC
    }
  15. Q75.How does reflection work in Go, and why should you avoid it in hot paths?

    The reflect package inspects and manipulates values whose types are known only at run time: their kind, fields, struct tags and methods. Standard library packages such as encoding/json use it to work with any struct.

    Core concepts:

    • reflect.TypeOf(x) describes the type, including struct fields and their tags.
    • reflect.ValueOf(x) holds the value; to modify it, you need a pointer and Elem(), so the value is settable.
    • Kinds classify types into categories such as struct, slice, map and pointer.

    Why to avoid it in performance-critical code:

    1. Slower: reflective access is much slower than direct field access, and it often allocates.
    2. No compile-time checks: type mistakes become run-time panics.
    3. Harder to read and to refactor safely.

    Alternatives:

    • Generics for type-safe code that works with many types.
    • Interfaces with methods for behavior that varies by type.
    • Code generation for repetitive mapping code, such as converting between structs.

    When reflection is reasonable: general-purpose libraries, such as encoders, validators and test helpers, where flexibility matters more than speed, and where results can be cached per type.

  16. Q76.A Go service's memory keeps growing for days. How do you investigate?

    Separate real growth in live data from normal runtime behavior, then use heap and goroutine profiles to find what is holding memory. In Go, most "leaks" are references kept by long-lived structures, such as maps, caches or blocked goroutines, rather than memory that is never freed.

    1. Check the trend: live heap after garbage collection, not just process memory, since the runtime returns memory to the operating system gradually.
    2. Check the goroutine count: a rising count means leaked goroutines holding their stacks and everything they reference.
    3. Compare heap profiles taken hours apart, looking at in-use memory by allocation site; the growing site points to the data structure.
    4. Look for unbounded structures: caches without eviction, maps keyed by request or session IDs that are never deleted, and slices that keep growing.
    5. Check sub-slices: small slices of large buffers keep the whole buffer alive.
    6. Check timers and tickers that are never stopped in long-lived loops, and response bodies that are never closed.

    Fix and verify: bound the structure, add eviction or cleanup, then watch the live heap level off under the same traffic. Add an alert on live heap growth and goroutine count.

    Example

    Live heap grows by about 200 MB per day while traffic is flat.

    1. Two heap profiles, six hours apart, show in-use memory growing at a sync.Map that stores rate-limit state per client IP address.
    2. Entries are added for every new address and never removed, so the map holds millions of idle clients.
    3. Replacing it with a bounded structure that evicts entries idle for ten minutes keeps the map near 50,000 entries.
    4. The live heap stabilizes at about 300 MB and stays flat over the following week.
  17. Q77.Latency spikes every few minutes in a Go service. What could be causing it?

    Regular spikes point to something periodic: garbage collection under memory pressure, scheduled work inside the process, or something external, such as a dependency's background job. Correlate the spikes with runtime metrics and traces to see which.

    Causes inside the process:

    • Garbage collection pressure: when the live heap is close to GOMEMLIMIT, the collector runs very often and makes request goroutines help with marking.
    • Large allocations in bursts: periodic tasks, such as rebuilding a cache, allocate heavily and trigger collections.
    • Lock contention: a background job holding a lock that request handlers need.
    • Processor throttling in containers when GOMAXPROCS exceeds the quota on older Go versions.

    Causes outside the process:

    • Database maintenance, backups or replication catch-up.
    • Connection churn, such as all pooled connections expiring at the same time.

    How to confirm: compare spike times with garbage collection cycles and pause metrics, collect an execution trace during a spike and check mutex and block profiles.

    Typical fixes: reduce allocations, give the heap headroom, spread out background jobs, add jitter to connection lifetimes and shorten critical sections.

    Example

    A service has a live heap of about 1.6 GB and runs with GOMEMLIMIT set to 1.8 GB.

    1. The heap may grow only about 200 MB before the limit forces a collection, so collections run every few seconds under load.
    2. Each collection makes busy request goroutines spend time on assist work, and latency at the 99th percentile jumps during each cycle.
    3. Reducing a cache from 600 MB to 200 MB lowers the live heap to about 1.2 GB.
    4. With 600 MB of headroom, collections become far less frequent, and the latency spikes disappear.
  18. Q78.Outgoing HTTP calls from a Go service start failing with connection errors under load. What is a likely cause?

    A common cause is response bodies that are not fully read and closed. The transport can reuse a connection only after its body is closed and drained; otherwise each request opens a new connection, which exhausts file descriptors, local ports or the server's connection limits.

    Symptoms:

    • Growing numbers of open connections and file descriptors.
    • Errors such as too many open files, or connection resets and timeouts at the destination.
    • Latency rising as every request pays for a new connection and TLS handshake.

    Other causes to check:

    1. A new client per request, each with its own transport and pool, so nothing is reused.
    2. MaxIdleConnsPerHost too small: the default of 2 forces most connections to close after each request at high concurrency.
    3. No timeouts: stuck calls keep connections and goroutines busy.

    Fixes:

    • defer resp.Body.Close() immediately after checking the error, and read the body to the end, for example with io.Copy(io.Discard, resp.Body), before closing.
    • Create one client per destination at startup and reuse it.
    • Tune the transport's idle connection limits for your concurrency.
    • Monitor connection counts per destination.
  19. Q79.A Go service stops responding under load, but its processor use is low. How do you debug it?

    Low processor use with no progress means goroutines are waiting: on locks, channels, connection pools or slow dependencies. A goroutine dump shows exactly what they are waiting for.

    1. Collect a goroutine profile from the profiling endpoint, or send a quit signal in a test environment to print all stacks.
    2. Group by stack: look for many goroutines blocked at the same line.
    3. Identify the resource:
      • sync.Mutex.Lock points to lock contention or a deadlock.
      • database/sql connection waits point to an exhausted pool.
      • Channel send or receive points to a missing consumer or producer.
      • Network reads point to a slow dependency without a timeout.
    4. Check pool statistics: db.Stats() shows wait count and wait duration.
    5. Check recent changes: a new lock around slow work, or a call added inside a transaction.

    Common fixes:

    • Add timeouts on every external call and on waiting for resources.
    • Shorten critical sections and never hold a lock during input and output.
    • Size pools to match concurrency, and limit incoming concurrency to what the pools can serve.

    Interview tip: "low processor plus high latency means waiting, so I take a goroutine dump" is a crisp diagnostic rule that interviewers value.

  20. Q80.Requests in a Go service wait a long time for database connections. How do you fix it?

    The pool has fewer connections than requests need at the current traffic and hold times. Either requests hold connections too long, the pool is too small for the traffic, or the database cannot accept more connections.

    How to confirm: db.Stats() shows WaitCount and WaitDuration rising, with InUse equal to MaxOpenConnections during peaks.

    Fixes, in order of preference:

    1. Shorten hold times: keep transactions short, never call other services while holding a connection and always close rows.
    2. Speed up slow queries, with indexes or simpler queries.
    3. Raise SetMaxOpenConns if the database has spare capacity, keeping the total across all instances below its limit.
    4. Limit incoming concurrency to what the pool can serve, so requests fail fast instead of queueing for seconds.
    5. Use read replicas for read-heavy endpoints.

    Prevention: pass request contexts to queries so waits end when clients give up, and alert on pool wait duration.

    Example

    Each instance allows 10 connections, and each request holds a connection for 40 ms on average.

    1. Maximum throughput per instance: 10 / 0.040 = 250 requests per second.
    2. Traffic is 400 requests per second per instance, so requests queue for connections.
    3. A profile shows 25 ms of each hold time is spent calling a pricing service inside the transaction.
    4. Moving that call before the transaction cuts the hold time to 15 ms, raising capacity to 10 / 0.015 ≈ 667 requests per second, without adding connections.