# Go (Golang) Interview Questions 2025 — Goroutines, Channels, Interfaces, Error Handling, Context, Generics

Source: https://www.techinterview.org/post/3233474456/go-golang-interview-questions-2025-goroutines-channels-interfaces-error-handling-context-generics-concurrency-patterns/
Updated: 2026-04-21 · techinterview.org

Go is the language of cloud infrastructure -- powering Kubernetes, Docker, Terraform, and most modern backend services at companies like Google, Uber, Cloudflare, and Stripe. Go interview questions focus on concurrency (goroutines/channels), interfaces, error handling philosophy, and the design decisions that make Go uniquely suited for distributed systems. This guide covers what you need for senior Go engineering interviews.

## Goroutines and Concurrency

Goroutines are lightweight threads managed by the Go runtime. Creating one: go func() { doWork() }(). Cost: ~2 KB initial stack (grows dynamically). You can run millions of goroutines. The Go runtime multiplexes goroutines onto OS threads (M:N scheduling). Key concurrency patterns: (1) Fan-out/fan-in -- spawn N goroutines (fan-out) to process work in parallel, collect results into one channel (fan-in). (2) Worker pool -- a fixed number of goroutines consume from a shared work channel. Limits concurrency while processing a large number of tasks. (3) Pipeline -- chain goroutines via channels: stage1 -> channel -> stage2 -> channel -> stage3. Each stage processes and forwards data. (4) Select -- multiplex on multiple channels: select { case msg := <-ch1: handle(msg); case <-ctx.Done(): return; case <-time.After(5*time.Second): timeout(); }. Select blocks until one case is ready. Race conditions: goroutines sharing mutable state without synchronization cause data races. Detection: go run -race or go test -race. Fix with: channels (share by communicating, not by sharing memory), sync.Mutex (lock shared state), or sync/atomic (atomic operations on primitives). Go proverb: "Do not communicate by sharing memory; share memory by communicating." Channels are the idiomatic synchronization mechanism. Mutexes are for protecting shared state when channels are awkward.

## Channels

Channels are typed conduits for passing data between goroutines. ch := make(chan int) (unbuffered) or make(chan int, 10) (buffered with capacity 10). Unbuffered: the sender blocks until a receiver is ready (synchronization point). Buffered: the sender blocks only when the buffer is full; the receiver blocks when empty. Direction: chan<- int (send-only), <-chan int (receive-only). Use in function signatures to enforce communication direction. Closing: close(ch) signals no more values will be sent. Receivers: for msg := range ch reads until the channel is closed. Reading from a closed channel returns the zero value immediately (use the two-value form: val, ok := <-ch; if !ok { /* closed */ }). Common patterns: (1) Done channel -- signal goroutines to stop: done := make(chan struct{}). The goroutine selects on done and exits when it is closed. close(done) broadcasts to all listeners. (2) Semaphore -- buffered channel as a counting semaphore: sem := make(chan struct{}, 10). Acquire: sem <- struct{}{}. Release: <-sem. Limits concurrency to 10. (3) Timeout -- select with time.After: select { case res := <-ch: use(res); case <-time.After(3*time.Second): return ErrTimeout; }. Interview question: "What happens if you send on a closed channel?" Answer: panic. Always: only the sender should close a channel. Never close from the receiver side. "What happens if you send on a nil channel?" Answer: blocks forever.

## Interfaces and Composition

Go interfaces are implicit -- a type implements an interface by implementing its methods (no "implements" keyword). type Writer interface { Write([]byte) (int, error) }. Any type with a Write method satisfies io.Writer. This enables: (1) Polymorphism without inheritance -- pass any Writer (file, buffer, HTTP response, network connection) to functions accepting io.Writer. (2) Small, focused interfaces -- io.Reader (one method), io.Writer (one method), io.ReadWriter (both). Compose small interfaces into larger ones. Go proverb: "The bigger the interface, the weaker the abstraction." (3) Mock-friendly testing -- define an interface for your dependency. In tests, pass a mock implementation. No mocking framework needed. Type assertions: var w Writer = getWriter(); if f, ok := w.(*os.File); ok { /* f is a *os.File */ }. Safe type assertion with the two-value form (panics without ok). Type switch: switch v := w.(type) { case *os.File: ...; case *bytes.Buffer: ...; }. Empty interface: interface{} (or any in Go 1.18+) accepts any type. Use sparingly -- you lose type safety. Prefer generics (Go 1.18+) for type-safe parameterized code. Embedding: type ReadWriter struct { Reader; Writer } -- ReadWriter has all methods of Reader and Writer. This is composition, not inheritance. Go does not have classes or inheritance -- composition via embedding and interface satisfaction is the design philosophy.

## Error Handling

Go uses explicit error values instead of exceptions. Functions return (result, error). The caller checks: if err != nil { return fmt.Errorf("failed to open file: %w", err) }. The %w verb wraps errors -- callers can unwrap with errors.Is() and errors.As(). Why no exceptions: explicit errors are visible in the function signature. You cannot accidentally ignore an error (the compiler warns about unused variables). Error handling is part of the normal control flow, not a separate exception path. Error wrapping: add context as errors propagate up the call stack. Each layer wraps with additional information: "failed to process order: failed to charge card: connection timeout." The final error message tells you the full chain. errors.Is(err, ErrNotFound) checks if any error in the chain is ErrNotFound. errors.As(err, &target) extracts a specific error type. Sentinel errors: var ErrNotFound = errors.New("not found"). Package-level error values that callers check with errors.Is. Custom error types: type ValidationError struct { Field string; Message string }. Implement the Error() string method. Callers extract with errors.As. panic/recover: for truly unrecoverable situations (programming errors, not runtime errors). Panic unwinds the stack. Recover catches the panic (in a deferred function). Do NOT use panic for normal error handling -- this is a strong Go convention. Libraries should almost never panic; they should return errors.

## Context, Generics, and Performance

context.Context: carries deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines. ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second); defer cancel(). Pass ctx to all downstream calls. Functions check ctx.Done() to detect cancellation. HTTP handlers receive context from the request: r.Context(). Database queries accept context: db.QueryContext(ctx, query). Rule: context is always the first parameter: func DoWork(ctx context.Context, args ...) error. Generics (Go 1.18+): func Map[T, U any](slice []T, f func(T) U) []U. Type parameters enable type-safe reusable code without interface{}/reflection. Constraints: [T comparable] (supports ==), [T constraints.Ordered] (supports ). Use for: generic data structures (linked list, stack, queue), utility functions (map, filter, reduce), and type-safe containers. Do not overuse -- Go philosophy favors simplicity over abstraction. Performance: Go compiles to native code (no JVM, no interpreter). Fast startup (no warm-up), low memory (no runtime overhead), and predictable latency (simple GC with sub-millisecond pauses). Profiling: go tool pprof for CPU and memory profiling. go test -bench for benchmarking. go tool trace for execution tracing. Common optimizations: avoid allocations in hot paths (use sync.Pool for object reuse), prefer value types over pointers for small structs (reduces GC pressure), and use buffered I/O (bufio.Reader/Writer).
