Skip to content

Top 35 Golang Interview Questions and Answers [2026]

October 5, 2026 13 min read
Golang interview guide by Second Talent
TL;DR: Go interviews in 2026 test concurrency (goroutines, channels, context cancellation), interface and slice semantics, error wrapping and profiling. Candidates should know that Go 1.27 added generic methods and a standard encoding/json/v2, and that the Green Tea garbage collector has been the default since Go 1.26.

Go 1.27, released on August 19, 2026, lets a method declare its own type parameters, something the generics design left out when generics arrived in Go 1.18.

The same release rebuilt encoding/json on a new v2 engine and made the goroutine leak profile generally available.

The fundamentals below still decide most interviews: how the scheduler runs goroutines, when a channel beats a mutex, and why a nil pointer inside an interface is not nil.

Key takeaways
  1. 1The current release is Go 1.27.1 (September 1, 2026). Go supports the two newest major releases, so 1.26 and 1.27 get security fixes and 1.25 no longer does.
  2. 2Since Go 1.25, GOMAXPROCS follows the container's CPU limit on Linux, which removed the main reason teams imported automaxprocs.
  3. 3Go 1.26 added errors.AsType, a generic, type-safe version of errors.As, and let new take an expression such as new(int64(42)).
  4. 4Since Go 1.22, each loop iteration gets fresh loop variables, but only in modules whose go.mod says go 1.22 or later.

Goroutines and Channels

1. How do goroutines differ from OS threads?

A goroutine is a function scheduled by the Go runtime, not by the operating system. It starts with a stack of a few kilobytes that grows and shrinks as needed, so a service can run hundreds of thousands of them on a handful of OS threads.

The difference interviewers probe is what happens when one blocks. A goroutine waiting on a channel, a mutex or a network socket is parked by the runtime and costs no thread at all; the network poller wakes it when the socket is ready.

Only a blocking system call, such as a file read on some platforms or a cgo call, ties up an OS thread, and the scheduler compensates for that (next question).

2. Explain the scheduler's G, M and P model.

G is a goroutine, M is an OS thread, and P is a processor: the right to run Go code, with its own queue of runnable goroutines. There are GOMAXPROCS Ps, so at most that many goroutines execute Go code at once.

Swimlane of the Go scheduler when a goroutine makes a blocking system call: the goroutine enters the call, its thread keeps it but hands the P to another thread, the other goroutines keep running, and when the call returns the goroutine looks for a free P or goes back on the run queue.

An M must hold a P to run a G. When a G enters a blocking system call, its M keeps the G but hands the P to another M, so the other goroutines on that queue keep running.

An idle P steals half of another P's queue, which keeps all cores busy.

Since Go 1.14 the runtime can also preempt a goroutine asynchronously, so a tight loop with no function calls no longer starves the rest of the program.

3. What is the difference between buffered and unbuffered channels?

An unbuffered channel is a synchronization point: a send blocks until a receiver takes the value, so both goroutines meet.

A buffered channel accepts sends until its buffer is full and only then blocks, so the sender and receiver are decoupled by up to cap(ch) items.

Unbuffered: make(chan T)
  • Send blocks until a receiver takes the value
  • Use it to hand off work and know it was received
Buffered: make(chan T, n)
  • Send blocks only when n items are waiting
  • Use it to absorb bursts or to let senders exit without a reader

Three rules that come up in follow-ups:

  • Sending on a closed channel panics. Receiving from one returns the zero value at once, with ok set to false.
  • Only the sender should close a channel, and only when receivers need to know no more values are coming.
  • A nil channel blocks forever on both send and receive, which is useful in select (next question).

4. How does select behave when several cases are ready?

It picks one of the ready cases at random, with uniform probability, so no case can starve the others. If no case is ready, select blocks, unless it has a default case, which makes it non-blocking.

A strong candidate knows the nil-channel trick: setting a channel variable to nil disables its case, which is how a loop stops reading from a source that has closed while it keeps reading from the others.

The example below returns the first result and cancels the rest; the buffered channel means the slower goroutines can still send and exit instead of leaking.

func first(ctx context.Context, urls []string, fetch func(context.Context, string) string) string {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // tells the slower fetches to stop
    results := make(chan string, len(urls)) // buffered: losers never block
    for _, u := range urls {
        go func() { results <- fetch(ctx, u) }()
    }
    select {
    case r := <-results:
        return r
    case <-ctx.Done():
        return ""
    }
}

5. When would you use a mutex instead of a channel?

Use a mutex to protect shared state, and a channel to pass ownership of data or to signal between goroutines.

A counter, a cache or a map of connections is state: a sync.Mutex or sync.RWMutex around it is shorter and faster than a goroutine that owns it and serves requests over channels.

Decision flow for Go synchronization: passing ownership of data or signaling means a channel, waiting for a group means WaitGroup or errgroup, a single counter or pointer means sync/atomic, and other shared state means a sync.Mutex.

For a single integer or pointer, sync/atomic types such as atomic.Int64 avoid the lock entirely. The proverb "share memory by communicating" is advice about design, not a ban on mutexes; the standard library uses both.

6. How do you find a data race?

Run the tests, or a staging build, with -race. The race detector instruments memory accesses and reports two goroutines touching the same variable without a happens-before relationship, with both stack traces.

It finds only races that execute, so it needs tests that exercise concurrent paths. The race detector documentation puts the cost at 5 to 10 times the memory and 2 to 20 times the run time, which is why it is not left on in production.

The rules it enforces are in the Go memory model: without a channel operation, a lock or an atomic between them, a write in one goroutine is not guaranteed to be visible to a read in another.

A map written from two goroutines is worse than a race: the runtime detects it and stops the program with "concurrent map writes", which cannot be recovered.

7. How do you wait for a group of goroutines and collect the first error?

For plain waiting, sync.WaitGroup. Go 1.25 added WaitGroup.Go, which does the Add(1) and Done() for you and removes the classic bug of calling Add inside the new goroutine:

var wg sync.WaitGroup
for _, job := range jobs {
    wg.Go(func() { process(job) })
}
wg.Wait()

When the goroutines can fail, golang.org/x/sync/errgroup is the usual answer. errgroup.WithContext returns the first error from Wait and cancels the shared context so the other goroutines can stop early, and SetLimit caps how many run at once.

Context and HTTP

8. What is context.Context for, and what are the rules for using it?

A context carries a cancellation signal, a deadline and request-scoped values across API boundaries. When a client disconnects or a timeout fires, every function holding that context can see ctx.Done() close and stop work.

The context package documentation sets the rules: pass it as the first parameter, named ctx; do not store it in a struct; never pass a nil context (use context.TODO()); and always call the cancel function returned by WithCancel, WithTimeout or WithDeadline, usually with defer.

Skipping the cancel leaks the child context until its parent ends, and go vet flags it.

9. How should middleware pass request data through context?

Store only request-scoped data, such as a request ID or the authenticated user, and use an unexported key type so no other package can collide with or overwrite the value. Expose typed helper functions instead of the key itself:

type ctxKey struct{}

func WithUserID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, ctxKey{}, id)
}

func UserID(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(ctxKey{}).(string)
    return id, ok
}

func auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := WithUserID(r.Context(), userFromToken(r))
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Dependencies such as a database handle or a logger configuration do not belong in context; pass them as struct fields or arguments. A string key is the common mistake: two packages that both use "user" silently read each other's values.

For a hands-on middleware exercise, see our Go coding challenges.

10. What do context.WithoutCancel and context.AfterFunc solve?

Both arrived in Go 1.21. WithoutCancel returns a context that keeps the parent's values but is not canceled with it.

Use it for work that must finish after the request ends, such as writing an audit record, while keeping the request ID for logs.

AfterFunc(ctx, f) runs f in its own goroutine once ctx is canceled, and returns a stop function. It replaces the hand-written goroutine that waits on ctx.Done() only to close a connection or wake a condition variable.

Go 1.20 had already added WithCancelCause, so the reason for a cancellation can be read back with context.Cause.

11. What changed in net/http routing in Go 1.22?

http.ServeMux patterns now accept an HTTP method and path wildcards, which covers most of what teams used third-party routers for:

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    fmt.Fprint(w, id)
})

A POST to that path gets a 405 Method Not Allowed automatically. When two patterns match, the more specific one wins, and patterns that conflict with no clear winner panic at registration instead of failing at request time.

The Go blog post on routing covers the precedence rules.

12. Which timeouts must a production HTTP server and client set?

Both default to no timeouts at all. On http.Server, set at least ReadHeaderTimeout, which stops a client from holding a connection open by sending headers slowly, plus ReadTimeout, WriteTimeout and IdleTimeout to suit the workload.

On the client, http.DefaultClient has a zero Timeout, which means a hung upstream can block a goroutine forever; create a client with a Timeout or pass a context with a deadline to each request.

The common production bug. A service calls a partner API with http.Get, which uses http.DefaultClient. The partner hangs, goroutines pile up behind it, and memory climbs until the pod is killed. A client with Timeout: 10 * time.Second turns that into an error per request.

Clients should also be reused. A new http.Client or Transport per request throws away the connection pool, and forgetting to close resp.Body leaks connections.

Go 1.27 now drains a small amount of unread body on Close so the connection can be reused, but the Close call itself is still required.

Types and Interfaces

13. Why can an error be non-nil when it holds a nil pointer?

An interface value is a pair: a dynamic type and a value. It is nil only when both are nil. Returning a typed nil pointer through an error produces an interface whose type is set, so err != nil is true.

type MyErr struct{}

func (*MyErr) Error() string { return "boom" }

func find() error {
    var p *MyErr // nil pointer
    return p     // non-nil error: type is *MyErr, value is nil
}

fmt.Println(find() == nil) // false

The fix is to return a literal nil on the success path. This question separates candidates who have debugged real Go code from those who have only read about interfaces.

14. When do you use a pointer receiver instead of a value receiver?

Use a pointer receiver when the method modifies the receiver, when the struct is large enough that copying it matters, or when the type contains a sync.Mutex or another value that must not be copied.

Otherwise a value receiver is fine, and it is common for small immutable types such as time.Time.

The follow-up is method sets. The method set of *T includes methods with both receiver kinds; the method set of T includes only value receivers.

So if Save has a pointer receiver, a T value does not satisfy an interface that requires Save, and the compiler says so. Mixing receiver kinds on one type is legal but the Go code review guidance recommends picking one.

15. What does append do to the slice you passed in?

A slice is a pointer to an array plus a length and a capacity. If the capacity has room, append writes into the same array and the new slice shares memory with the old one.

If not, it allocates a new, larger array and copies, and the two slices become independent.

a := make([]int, 3, 4)
b := append(a, 4) // fits in capacity: shares a's array
b[0] = 99
fmt.Println(a[0]) // 99

c := append(b, 5) // over capacity: new array
c[0] = 7
fmt.Println(b[0]) // still 99

This is why functions that append to a caller's slice return the result, and why taking a sub-slice of a large buffer keeps the whole buffer in memory. slices.Clone or a full slice expression s[lo:hi:hi] breaks the sharing.

16. What should a candidate know about maps in Go?

Maps are not safe for concurrent writes, and iteration order is unspecified and deliberately randomized between runs, so code must not depend on it.

Reading a missing key returns the zero value; the two-value form v, ok := m[k] tells absence from a stored zero.

Since Go 1.24 the built-in map uses a Swiss table implementation, part of a runtime change the release notes put at 2 to 3% lower CPU overhead on average.

For concurrent access, a mutex around a plain map is the default. sync.Map is tuned for two cases named in its documentation: keys written once and read many times, and goroutines working on disjoint sets of keys.

17. How do you wrap and inspect errors?

Wrap with fmt.Errorf("load config: %w", err) to add context while keeping the original error in the chain. Then check the chain with errors.Is for a sentinel value such as fs.ErrNotExist, or extract a typed error.

Go 1.20 added errors.Join and multiple %w verbs in one Errorf; Go 1.26 added the generic errors.AsType:

_, err := os.Open("/does/not/exist")
err = fmt.Errorf("load config: %w", err)

if errors.Is(err, fs.ErrNotExist) { /* true */ }

if pe, ok := errors.AsType[*fs.PathError](err); ok {
    fmt.Println(pe.Op) // "open"
}

AsType returns the typed value directly, avoids the pointer-to-a-variable pattern of errors.As, and is faster. A senior answer also covers when not to wrap: exposing a dependency's error type through %w makes it part of your package's API.

18. When is panic and recover the right tool?

Rarely.

Errors are returned as values; panic is for programmer errors and states that should be impossible, such as an index out of range or a failed invariant during initialization. recover only works inside a deferred function in the same goroutine that panicked.

That last point matters in servers. net/http recovers a panic in a handler and logs it, so one bad request does not crash the process, but a panic in a goroutine the handler started is not covered and takes the whole program down.

Code that starts goroutines from library code should recover and convert panics to errors at that boundary.

19. When should you use generics, and when not?

Use type parameters when the same logic applies to many element types and the alternative is any plus type assertions: containers, and functions over slices and maps like those in the slices and maps packages.

Constraints are interfaces; ~int matches any type whose underlying type is int, and comparable covers map keys.

func Map[T, U any](s []T, f func(T) U) []U {
    out := make([]U, 0, len(s))
    for _, v := range s {
        out = append(out, f(v))
    }
    return out
}

Do not reach for generics when an ordinary interface describes behavior: a function that calls Read should take an io.Reader, not a type parameter constrained by it.

20. Is struct embedding inheritance?

No. Embedding a type promotes its fields and methods to the outer struct, which gives reuse, but there is no virtual dispatch: a promoted method's receiver is still the embedded value, and it cannot call an overriding method on the outer type.

The outer type can define a method with the same name, which shadows the promoted one.

Embedding an interface in a struct is a common testing trick: the struct satisfies the interface and you implement only the methods the test needs.

Calling any other method panics with a nil dereference, so it suits fakes, not production code.

Performance and Memory

21. What is escape analysis, and how do you see its decisions?

The compiler decides whether each value can live on the goroutine's stack or must be allocated on the heap.

A value escapes when it outlives the function: its address is returned, stored in a heap object, captured by a closure that outlives the call, or passed to an interface whose size is not known at compile time.

Build with go build -gcflags=-m to print the decisions. Stack allocation is nearly free and creates no work for the garbage collector, so in hot paths reducing escapes is often the cheapest optimization available.

22. How do GOGC and GOMEMLIMIT control the garbage collector?

GOGC (default 100) sets how much the heap may grow over the live heap before the next collection: at 100, a 200 MB live heap triggers a cycle at about 400 MB. Raising it trades memory for less CPU spent collecting.

GOMEMLIMIT, added in Go 1.19, is a soft limit on total runtime memory. As usage nears it, the collector runs more often.

In a container, setting it 5 to 10% below the memory limit avoids OOM kills during spikes; setting it too close to the live heap makes the program spend most of its time collecting. The GC guide shows both settings with interactive graphs.

23. How would you profile a slow Go service?

Import net/http/pprof on an internal port and pull profiles from the running service with go tool pprof: CPU to find hot functions, heap (alloc_space for allocation churn, inuse_space for what is held), goroutine for stuck goroutines, and mutex and block profiles for contention.

The execution tracer (go tool trace) shows scheduling latency and GC pauses that profiles average away.

Profile-guided optimization, generally available since Go 1.21, closes the loop: commit a production CPU profile as default.pgo in the main package, and the compiler inlines and devirtualizes the hot paths.

The PGO documentation reports gains of around 2 to 14% on representative programs.

24. When does sync.Pool help, and when does it hurt?

sync.Pool reuses short-lived objects, such as byte buffers in an encoder, to cut allocations in a hot path. It helps only when profiling shows allocation of that object is a real cost.

It hurts when used as a cache: the pool may drop items at any garbage collection, so nothing stored there is guaranteed to come back.

Objects must be reset before reuse, and large buffers returned to the pool keep memory pinned; the standard library's own fmt package refuses to pool buffers above a size cap for that reason.

25. How do you write a benchmark that measures the right thing?

Since Go 1.24, write the loop with b.Loop().

Setup before the loop runs once per benchmark, and the compiler keeps the loop body's arguments and results alive, so it cannot optimize the measured code away, a common flaw in the older for i := 0; i < b.N; i++ form.

func BenchmarkParse(b *testing.B) {
    input := loadFixture() // runs once, not timed per iteration
    for b.Loop() {
        Parse(input)
    }
}

Run with -benchmem to see allocations per operation, repeat with -count=10, and compare runs with benchstat rather than eyeballing single numbers.

Modules and Testing

26. How does Go choose dependency versions?

With minimal version selection: for each module, Go uses the highest of the minimum versions required anywhere in the build, not the newest version published.

Builds are reproducible without a lock file, because the answer depends only on go.mod files. go.sum records checksums, and downloads are verified against the public checksum database.

Follow-ups worth knowing: replace directives apply only in the main module, so a library's replacements do not reach its users; GOPRIVATE keeps private module paths away from the public proxy and checksum database; and since Go 1.24, tool directives in go.mod track build tools such as code generators, replacing the old tools.go blank-import workaround.

27. What does the go line in go.mod control?

More than a minimum version. Since Go 1.21 it is a hard requirement, and the toolchain line lets the go command download a newer toolchain when a module needs one.

It also sets language semantics: the per-iteration loop variables of Go 1.22 apply only to modules declaring go 1.22 or later.

Go 1.26 changed the default for new modules: go mod init run with a 1.N toolchain now writes go 1.(N-1).0, so new modules stay compatible with the older supported release.

A candidate who knows this can explain why raising the go line is a deliberate change, not a formality.

28. How do you test time-dependent concurrent code without sleeps?

With testing/synctest, generally available since Go 1.25. synctest.Test runs a function in an isolated bubble with a fake clock that jumps forward whenever every goroutine in the bubble is blocked, so a test of a 30-second timeout finishes in microseconds and is deterministic. synctest.Wait blocks until all goroutines in the bubble are idle.

Go 1.27 added synctest.Sleep and httptest.NewTestServer, which runs an HTTP server over an in-memory network that works inside a bubble. Together they replace most time.Sleep calls that made concurrent tests slow and flaky.

29. What is fuzzing in Go, and when is it worth adding?

Native fuzzing, added in Go 1.18, runs a test function with generated inputs, mutating a seed corpus to reach new code paths, and saves any input that fails to testdata/fuzz so it becomes a permanent regression test.

func FuzzParse(f *testing.F) {
    f.Add("2026-09-28")
    f.Fuzz(func(t *testing.T, s string) {
        if _, err := Parse(s); err != nil {
            return // invalid input is fine; panics are not
        }
    })
}

It pays off on parsers, decoders and anything that handles untrusted input. Run it with go test -fuzz=FuzzParse; plain go test runs only the seed corpus, so it stays fast in CI.

What Changed Recently

Feb 11, 2025
Go 1.24: Swiss table maps, b.Loop, tool directives
Aug 12, 2025
Go 1.25: container-aware GOMAXPROCS, WaitGroup.Go, synctest
Feb 10, 2026
Go 1.26: Green Tea GC by default, new(expr), rebuilt go fix
Aug 19, 2026
Go 1.27: generic methods, encoding/json/v2, goroutine leak profile

30. What are generic methods in Go 1.27, and what can they still not do?

According to the Go 1.27 release notes, a method can now declare its own type parameters. Before 1.27, a type-parameterized operation on a type had to be a package-level function, or the whole type had to carry the parameter.

type Cache struct{ m map[string]any }

func (c *Cache) Get[T any](key string) (T, bool) {
    v, ok := c.m[key].(T)
    return v, ok
}

n, ok := cache.Get[int]("retries")

The limits: interface methods cannot declare type parameters, and a generic method cannot implement an interface method. The standard library's first use is (*rand.Rand).N in math/rand/v2, the method form of the generic rand.N function.

31. What is encoding/json/v2, and do teams have to migrate?

Go 1.27 ships encoding/json/v2 and the lower-level encoding/json/jsontext as standard packages; they first appeared behind a GOEXPERIMENT in Go 1.25. v2 has stricter defaults: it rejects invalid UTF-8 and duplicate object keys, which v1 accepted.

Options are passed as variadic arguments instead of through custom marshalers.

Migration is optional. The v1 encoding/json package keeps its behavior but now runs on the v2 implementation, and the release notes describe unmarshaling as "significantly faster" with marshaling at parity.

Error message text may differ, which breaks tests that compare error strings. Building with GOEXPERIMENT=nojsonv2 restores the old implementation while that opt-out lasts.

32. What changed in the garbage collector in Go 1.26?

The Green Tea collector, an experiment in 1.25, became the default. It changes how small objects are marked and scanned to get better memory locality and CPU scalability.

The Go 1.26 release notes expect a 10 to 40% reduction in GC overhead for programs that lean on the collector, and roughly 10% more on Intel Ice Lake, AMD Zen 4 and newer CPUs, where it uses vector instructions.

It can still be turned off at build time with GOEXPERIMENT=nogreenteagc, which works on Go 1.27.1. A candidate describing GC tuning should mention that pre-2026 benchmarks of GC overhead no longer describe the default collector.

33. How does the goroutine leak profile work?

Generally available in Go 1.27 as the goroutineleak profile in runtime/pprof and at /debug/pprof/goroutineleak, it reports goroutines blocked on a channel, mutex or condition variable that no running goroutine can reach.

The garbage collector does the detection: if the primitive a goroutine waits on is unreachable, nothing can ever wake it.

The classic case is a goroutine sending on an unbuffered channel after its receiver returned early on a timeout.

The profile misses leaks where the channel is still reachable through a global variable, so it complements, not replaces, the habit of giving every goroutine a clear exit path.

34. How does Go 1.25 set GOMAXPROCS in a container?

On Linux, the runtime now reads the cgroup CPU bandwidth limit and sets GOMAXPROCS to it when it is lower than the number of CPUs. In Kubernetes, that is the container's CPU limit; CPU requests are not considered.

The runtime also updates the value if the limit changes while the process runs.

Before 1.25, a Go service with a 2-CPU limit on a 64-core node ran 64 Ps, burned its CPU quota in bursts and got throttled, which showed up as tail latency. The fix was the automaxprocs library; on 1.25 and later it is redundant.

Setting GOMAXPROCS manually disables the automatic behavior, per the Go 1.25 release notes.

35. What does the rebuilt go fix command do?

Go 1.26 replaced the old, obsolete fixers in go fix with modernizers: analyzers from the same framework as go vet that rewrite code to current idioms, such as replacing a hand-written loop with slices.Contains or wg.Add and Done pairs with WaitGroup.Go.

Library authors can mark a deprecated function with //go:fix inline so that go fix rewrites callers to its replacement.

Go 1.27 added four more modernizers. The practical interview angle: running go fix ./... after raising the go line is now a normal upgrade step, and a candidate who has done it can say which rewrites they reviewed by hand.

Signs of a Strong Answer

  • They say how each goroutine they start will exit, and who cancels it, before being asked.
  • They explain the typed-nil interface bug from question 13 and how they found it in real code.
  • They pick a mutex for shared state without apologizing for not using a channel.
  • They read a CPU or heap profile to justify an optimization instead of guessing, and know when -race cannot help.
  • They know which Go version their service targets and what the go line changes, such as loop variables and GOMAXPROCS defaults.
  • They can name something from Go 1.25 to 1.27 they have used, such as WaitGroup.Go, synctest or errors.AsType.

Hiring Go Developers

Go engineers who can reason about goroutine lifetimes and profiles are a small part of the backend pool.

Second Talent matches companies with pre-vetted Golang developers from Asia, screened with questions like these, and our Golang developer cost guide lists current rates by market.

Tell us the role and we send a shortlist within 24 hours. Start hiring, or pair this guide with our gRPC and Docker interview guides.

Ready to hire? Hire Golang Developers: vetted AI-native talent, with first profiles in 24 hours.

Get developers who ship with AI agents.

Starts at $23/Hour.

Start Hiring

$0 cost until you hire