Interview questions

Go interview questions for senior developers (2026)

Go questions for engineers who run services in production: slice aliasing, typed nils, range-over-func iterators, goroutine leaks, GC tuning and graceful shutdown.

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.

How to use these questions

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.

Fundamentals

What does this print, and why?

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.

A function returns an error that is nil inside the function, but 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.

How do you wrap and inspect errors in modern Go?

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 should a method have a pointer receiver?

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.

What is wrong with deferring 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.

Where should interfaces be defined in a Go codebase?

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.

Why do some programs crash with "concurrent map writes"?

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.

Intermediate

When do generics make Go code better, and when do they make it worse?

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.

How do range-over-func iterators work, and when would you write one?

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.

How should 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".

What is risky about 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.

What changed about loop variables in Go 1.22?

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.

How do you test a Go HTTP service well?

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.

Why move from 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.

Concurrency and the runtime

The goroutine count on a service grows by a few thousand per hour. How do you find the leak?

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.

Fetch 500 URLs with at most 20 in flight and stop on the first error.

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.

Channels or a mutex: how do you decide?

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.

A Go service in Kubernetes with a 2-CPU limit shows heavy CPU throttling. What is going on?

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.

How do you reduce GC pressure and memory use in a Go service?

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.

How would you speed up a CPU-bound handler?

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.

Senior and architecture

Write graceful shutdown for an HTTP server with background workers.

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.

How do you structure a large Go repository so it stays easy to change?

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.

How do you configure database access for a high-traffic Go service?

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.

You must make a breaking change to a widely used internal Go module. How?

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.

Design a worker that consumes jobs from a queue at a steady rate without overloading a downstream API.

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.

When would you choose gRPC over JSON over HTTP for internal services?

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.

Red flags to watch for

A practical exercise

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.

Hire senior Go developers vetted with these questions

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.

FAQ

How many Go questions fit in a one-hour interview?

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.

Should Go candidates know Kubernetes?

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.

What is the biggest gap between mid-level and senior Go engineers?

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.

Explore Ryz Labs

Staff augmentationDedicated development teamsAI pod teamsForward deployed engineersNearshore software developmentAI engineering teamsHire engineers by roleRyz Labs vs competitorsAlternatives guidesBuyer guidesCase studiesHow we vet engineers
Ryz Labs

Senior engineers in your time zone. AI pod teams that ship.

Tell us who you need. You'll get a scoped plan, a price and the names of the people who would do the work.

Start a conversation →