Go Interview Questions

Commonly asked Go interview questions with clear, practical answers.

Common Go interview questions, from language fundamentals through concurrency — the areas interviewers probe most.

Language fundamentals

Q: Why does Go refuse to compile with an unused import or unused local variable? It's a deliberate language design choice to keep codebases clean and prevent dead code from silently accumulating — the Go authors consider "unused but harmless" a myth in large, long-lived codebases.

Q: What's the difference between an array and a slice? An array's length is fixed and part of its type ([3]int). A slice ([]int) is a resizable view over an underlying array (pointer, length, capacity), and is what idiomatic Go uses almost everywhere in practice.

Q: What does Go's zero-value guarantee mean in practice? Every variable, even one only declared with var x int, starts at a well-defined default (0, "", false, nil) rather than uninitialized memory — this removes an entire class of undefined-behaviour bugs common in C.

Structs and interfaces

Q: How is polymorphism achieved in Go without classes? Through interfaces, which are satisfied implicitly (structural typing) — any type whose method set matches an interface automatically satisfies it, no explicit "implements" declaration required — combined with struct embedding for code reuse (composition).

Q: When should a method receiver be a pointer instead of a value? When the method must mutate the receiver, or when copying the struct on every call would be expensive (large structs). Otherwise a value receiver is simpler and avoids aliasing surprises.

Q: What is struct embedding? Placing one struct type as an anonymous field inside another, which "promotes" the embedded struct's fields and methods to the outer struct — Go's primary mechanism for composition instead of class inheritance.

Concurrency

Q: What's the difference between a goroutine and an OS thread? Goroutines are scheduled by the Go runtime (not the OS), start with a tiny growable stack (~2KB), and are cheap enough to spawn by the thousands. OS threads are heavier, kernel-scheduled, and much more expensive to create.

Q: What does an unbuffered channel guarantee? A send on an unbuffered channel blocks until a receiver is ready to receive — it's a synchronization point, guaranteeing the sender and receiver "rendezvous" at that moment.

Q: How would you detect a data race in a Go program? Run tests or the program with the built-in race detector: go run -race main.go or go test -race ./.... It instruments memory accesses at runtime and reports concurrent unsynchronized access to the same variable.

Error handling

Q: Why does Go prefer returning error values over exceptions? It keeps failure handling explicit and local to each call site rather than allowing control flow to jump silently across many stack frames, trading some verbosity for predictability and easier-to-follow code.

Q: What is the purpose of errors.Is and errors.As? errors.Is checks whether an error, or any error it wraps via %w, matches a specific sentinel value. errors.As checks whether an error, or anything it wraps, can be unwrapped into a specific concrete error type so you can access its fields.

Practical / design

Q: How would you design a worker pool in Go? Spawn a fixed number of goroutines that all read tasks from a shared channel, process them, and (optionally) send results to another channel; use a sync.WaitGroup to know when all workers have finished, and close channels once no more values will be sent.

Q: Why is Go a popular choice for building microservices? Fast startup, low memory footprint, a single static binary with no runtime dependency (ideal for minimal containers), first-class concurrency for handling many simultaneous requests, and a straightforward standard library for HTTP and gRPC.

Advanced concurrency & testing

Q: What is a goroutine leak, and how does context.Context help prevent one? A goroutine leak happens when a goroutine blocks forever — waiting on a channel that will never receive a value, or on work that will never finish — so it never returns and the memory/stack it holds is never reclaimed. This is especially easy to cause when a goroutine is waiting on a result the caller has already stopped caring about (a client disconnected, a request timed out). Passing a context.Context into that goroutine and select-ing on ctx.Done() alongside the actual work gives it a way to notice "nobody's waiting for this anymore" and return early, instead of blocking indefinitely.

Q: What's the difference between concurrency and parallelism in Go, and how does GOMAXPROCS relate to it? Concurrency is structuring a program as independently-executing pieces (goroutines) that can make progress out of order — it's about program structure. Parallelism is actually running pieces of work at the same physical instant on multiple CPU cores — it's about execution. A Go program can be highly concurrent (thousands of goroutines) while running on a single core, interleaved rather than simultaneous. GOMAXPROCS sets the maximum number of OS threads the Go scheduler will use to actually run goroutines simultaneously — it caps the degree of real parallelism available, while the number of goroutines caps the degree of concurrency; the two are independent dials.

Q: If you need every error from a batch of concurrent operations, not just the first one, is errgroup.Group still the right tool? Not directly — errgroup.Group.Wait() is specifically designed to return only the first non-nil error, on the assumption that the caller wants to stop and report as soon as anything fails. If every individual failure needs to be reported (e.g., "these 3 of 10 uploads failed, here's why each one failed"), the right tool is a manually-collected, mutex-protected slice of errors (or a buffered error channel) alongside a sync.WaitGroup, since that's the part errgroup deliberately doesn't do.

Q: What happens if you call t.Parallel() inside a table-driven subtest's loop, and what classic bug can that reintroduce? t.Parallel() marks that subtest to run concurrently with other parallel subtests once every sequential subtest before it has started. Because the subtest body is a closure over the loop variable (tt in a typical for _, tt := range tests table), running those closures in parallel reintroduces exactly the loop-variable-capture bug described on the goroutines-channels page — every parallel subtest can end up seeing the same, final value of tt unless it's re-declared inside the loop body (tt := tt) before calling t.Run. Go 1.22+ fixed this automatically by giving each loop iteration its own variable, but it's a real trap in code targeting older Go versions.