This is a set of 26 Go interview questions for hiring engineers who build and operate backend services, CLIs and infrastructure tooling in Go. It moves from the language traps that catch people coming from other languages, through generics, iterators and context, to goroutine leaks, runtime tuning and the design decisions that keep a Go codebase simple as it grows. At Ryz, Go candidates are sourced by recruiters who look at what they actually built, then complete structured NTRVSTA AI interviews on material like this. Recruiters review each candidate before and after, AI scores are advisory, and people make every decision.
Go is small enough that most candidates can define its features. The difference shows up when you ask what happens at runtime: who closes this channel, what cancels this goroutine, where this allocation goes. Pick five to seven questions, and for every answer ask "how would you find that in production?"
For mid-level roles, focus on Fundamentals and Intermediate. For senior roles, spend most of the time in the concurrency and architecture sections, and include the code-review exercise below, because Go concurrency bugs are easier to spot in code than to describe.
a := make([]int, 3, 10)
b := append(a, 4)
c := append(a, 5)
fmt.Println(b[3], c[3])
It prints 5 5. a has spare capacity, so both appends write into the same backing array at index 3, and b and c share it. To get an independent slice, copy it, or use a full slice expression like a[:3:3] so the next append must allocate.
What a strong answer shows: A precise model of length, capacity and shared backing arrays.
err != nil is true for the caller. Why?func validate() error {
var e *ValidationError // nil pointer
return e // non-nil interface holding a nil pointer
}
An interface value is nil only when both its type and value are nil. Returning a typed nil pointer produces an interface with a type and a nil value, which is not equal to nil. Return a literal nil on success.
What a strong answer shows: They understand interface internals, a classic source of production bugs.
Wrap with fmt.Errorf("load user %d: %w", id, err) to add context while keeping the original. Check with errors.Is for sentinel values like sql.ErrNoRows and errors.As for typed errors. errors.Join combines several errors. Wrap at boundaries where context helps, and avoid exposing internal errors as part of a package's API by accident.
What a strong answer shows: They treat wrapping as API design, not decoration.
When it mutates the receiver, when the struct is large, or when the type contains something that must not be copied, such as a sync.Mutex. Keep receivers consistent across a type. Method sets matter: if Save has a pointer receiver, only *User satisfies an interface requiring Save, not User.
What a strong answer shows: They connect receivers to method sets and copy safety.
f.Close() inside a loop over 10,000 files?Deferred calls run when the function returns, not at the end of each iteration, so every file stays open until the loop ends and the process can hit its file descriptor limit. Move the body into a helper function, or close explicitly. Also remember that deferred call arguments are evaluated when the defer statement runs.
What a strong answer shows: They know defer is function-scoped and when its arguments are bound.
Usually in the consuming package, kept small, often one or two methods. Producers return concrete types, consumers declare the behavior they need. That keeps packages decoupled and makes fakes in tests trivial. Large interfaces defined next to the implementation, "just in case", are a sign of patterns imported from other languages.
What a strong answer shows: Idiomatic Go design: accept interfaces, return structs.
Built-in maps are not safe for concurrent use, and the runtime detects some concurrent writes and aborts the program. Guard the map with a sync.Mutex or sync.RWMutex. sync.Map helps mainly for keys written once and read many times, or for disjoint key sets per goroutine. Writing to a nil map also panics.
What a strong answer shows: They know the default choice is a mutex and when sync.Map actually fits.
They help for data structures and algorithms that are the same across types, such as collections, caches and helpers over slices and maps. They hurt when used to abstract business logic that has one implementation.
func MaxBy[T any, K cmp.Ordered](items []T, key func(T) K) (T, bool) {
var best T
if len(items) == 0 {
return best, false
}
best = items[0]
for _, it := range items[1:] {
if key(it) > key(best) {
best = it
}
}
return best, true
}
What a strong answer shows: They know the standard slices, maps and cmp packages and use generics only where duplication is real.
Since Go 1.23, for range accepts functions of the form func(yield func(V) bool), typed as iter.Seq[V]. They suit sequences you do not want to materialize, like paginated API results or database rows.
func Pages(ctx context.Context, c *Client) iter.Seq2[Item, error] {
return func(yield func(Item, error) bool) {
for token := ""; ; {
page, err := c.List(ctx, token)
if err != nil {
yield(Item{}, err)
return
}
for _, it := range page.Items {
if !yield(it, nil) {
return
}
}
if page.Next == "" {
return
}
token = page.Next
}
}
}
What a strong answer shows: They respect the yield return value so callers can break, and handle errors inside the sequence.
context.Context be used in a service?Pass it as the first parameter through every call that does I/O or might block, derive timeouts with context.WithTimeout, and always call the returned cancel function. Do not store contexts in structs. Use values only for request-scoped data such as trace IDs, with unexported key types, never for optional parameters.
What a strong answer shows: Disciplined propagation and cleanup, not just "it carries a deadline".
http.ListenAndServe(":8080", mux) in production?The default server has no read, write or idle timeouts, so slow clients can hold connections indefinitely. Construct an http.Server with ReadHeaderTimeout, ReadTimeout, WriteTimeout and IdleTimeout. Outbound, the default http.Client also has no timeout. Since Go 1.22 the standard ServeMux supports method and path wildcards like GET /users/{id}, which removes the need for a router in many services.
What a strong answer shows: Production defaults, and current knowledge of the standard library.
Each iteration of a for loop now gets its own variable when the module's go directive is 1.22 or later. Closures and goroutines that capture the loop variable no longer all see the last value. Code in modules declaring older versions keeps the old semantics.
What a strong answer shows: They know the fix is tied to the go.mod version, which matters in mixed codebases.
Table-driven tests with subtests, httptest.NewRecorder for handlers and httptest.NewServer for clients. Run the suite with -race in CI. Use Testcontainers or a real Postgres for repository code rather than mocking SQL. Fuzz parsers with go test -fuzz. In Go 1.25, testing/synctest makes code with timers and goroutines testable without real sleeps.
What a strong answer shows: The race detector as a default, and tests that touch real dependencies.
log or a third-party logger to log/slog?slog, in the standard library since Go 1.21, gives structured key-value logging with levels, JSON and text handlers, and a Handler interface other libraries can target. Use slog.With to attach request fields and LogAttrs on hot paths to cut allocations.
What a strong answer shows: Structured logs as data and awareness of logging cost.
Grab a goroutine profile from /debug/pprof/goroutine?debug=2 and group stacks by where they are blocked. Typical causes: sends on a channel nobody reads after a timeout, a missing cancel(), a time.Ticker never stopped, or an HTTP response body never closed. The fix is usually a buffered channel of size one for single results, or a select on ctx.Done(). Tools like goleak catch leaks in tests.
What a strong answer shows: They read goroutine dumps and know the common leak shapes.
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(20)
results := make([]Result, len(urls))
for i, u := range urls {
g.Go(func() error {
r, err := fetch(ctx, u)
if err != nil {
return fmt.Errorf("fetch %s: %w", u, err)
}
results[i] = r
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
Each goroutine writes a distinct index, so no lock is needed. The derived context cancels the remaining fetches after the first error.
What a strong answer shows: Bounded concurrency, cancellation and a race-free result pattern, using golang.org/x/sync/errgroup rather than hand-rolled WaitGroups.
Use channels to transfer ownership of data or coordinate stages in a pipeline. Use a mutex to protect shared state, like a cache or a counter. A channel used as a lock, or a mutex guarding a queue that should be a channel, both make code harder to read. For simple counters, atomic.Int64 is cleaner than either.
What a strong answer shows: They pick primitives by problem shape, not ideology.
Before Go 1.25, GOMAXPROCS defaulted to the node's CPU count, so a pod on a 64-core node ran 64 Ps against a 2-CPU quota and got throttled. Teams used go.uber.org/automaxprocs to fix it. Since Go 1.25 the runtime considers the cgroup CPU limit when setting the default. Check the Go version and the effective GOMAXPROCS before tuning anything else.
What a strong answer shows: Version-aware runtime knowledge tied to a real container symptom.
Measure with heap profiles and GODEBUG=gctrace=1. Find escapes with go build -gcflags=-m and avoid needless pointers and interface conversions on hot paths. Preallocate slices, reuse buffers with sync.Pool carefully, and set GOMEMLIMIT a bit below the container limit so the GC works harder before the kernel kills the pod. GOGC trades memory for CPU.
What a strong answer shows: They understand GOGC versus GOMEMLIMIT and profile before optimizing.
Capture a CPU profile from net/http/pprof under realistic load and read it with go tool pprof, looking at flame graphs and cumulative time. Write benchmarks with b.Loop() (Go 1.24) and compare with benchstat. Profile-guided optimization lets you commit a representative profile as default.pgo in the main package for a few percent gain at no code cost.
What a strong answer shows: Profiling and statistically sound benchmarking before micro-optimizing.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
go worker.Run(ctx)
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
worker.Wait()
Keep the timeout below Kubernetes' termination grace period, and fail readiness first so the load balancer stops sending traffic.
What a strong answer shows: Coordination between the runtime, the orchestrator and in-flight work.
Package by domain, not by layer, and avoid util or common packages that everything imports. Use internal/ to stop accidental imports. Keep main thin and wire dependencies explicitly. Import cycles are a design signal: usually a missing small interface or a package doing two jobs.
What a strong answer shows: Package boundaries as the main architectural tool in Go.
Set SetMaxOpenConns, SetMaxIdleConns and SetConnMaxLifetime based on the database's limits and the number of replicas, since defaults allow unlimited open connections. Pass contexts so queries cancel with requests, and always close rows. Many teams use pgx directly for Postgres and sqlc to generate type-safe code from SQL instead of an ORM.
What a strong answer shows: Pool math across replicas and a clear position on ORMs versus generated code.
Go uses semantic import versioning: a v2 needs a new module path ending in /v2, so v1 and v2 can coexist during migration. Prefer additive changes first: new functions, options structs, deprecation comments that linters surface. Use go work workspaces to test dependent modules locally before release.
What a strong answer shows: They make migrations gradual for consumers.
Run a fixed pool of goroutines reading from the queue, use golang.org/x/time/rate to cap requests per second, and acknowledge messages only after success. Make jobs idempotent, retry with backoff and jitter, and send poison messages to a dead-letter queue. Export queue depth and processing latency so you can scale on real signals.
What a strong answer shows: Backpressure, idempotency and operability in one design.
gRPC gives typed contracts, streaming and efficient encoding, and deadlines propagate across calls. It costs browser support and easy debugging with curl. Whichever you choose, set deadlines on every call and evolve protobufs safely: never reuse field numbers, and reserve removed ones.
What a strong answer shows: Contract evolution and deadlines, not just performance claims.
_ or uses panic for ordinary failures.http.Client and http.Server with no timeouts.Give the candidate a 300-line Go service that consumes events from a channel standing in for a queue, enriches each one with a slow HTTP call and writes results to Postgres. Hide four bugs: a goroutine leak on timeout, a data race on a shared map, a missing rows.Close(), and a defer inside a loop. In 60 to 90 minutes of pairing, ask them to review it, fix what matters and add tests.
go test -race and go vet early?Ryz introduces senior Go developers who have already been assessed on concurrency, runtime behavior and service design. They are the top 1% of candidates we interview, they work on your team and repos, and they keep hours within ±1h of US time zones. Read about our vetting process, or adapt our Go developer job description for your role.
Five to seven, with follow-ups. Go answers are short, so the value comes from pushing on runtime behavior and failure modes, not from covering more topics.
For infrastructure and platform roles, yes, because much of that ecosystem is written in Go. For product backend roles, knowing how a Go process behaves in a container, including memory limits, CPU quotas and shutdown signals, matters more than writing operators.
Concurrency ownership. Mid-level engineers can start goroutines and use channels. Senior engineers design how work starts, stops, fails and is observed, and they can prove there are no leaks or races.
Questions we didn't answer? Email info@ryzlabs.com.