Go · Backend Interview Q&A

Go (Golang) Interview Questions: 50 Questions on Goroutines, Channels & More

Goroutines and the GMP scheduler, channel semantics you must know cold, select patterns, interfaces and the nil-interface trap, garbage collection, context, and the concurrency bugs that show up in code reviews and interviews.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 22 min read
Illustration of many lightweight runners multiplexed onto a few lanes with pipes carrying capsules between them, representing goroutines and channels
TL;DR
  • Go interviews are concurrency interviews. Expect goroutines and the GMP scheduler, channel semantics (nil, closed, buffered), select, sync and context, plus a live coding task like a worker pool.
  • Memorise the channel table: send on closed panics, receive on closed returns zero value, ok=false, nil channel blocks forever, double close panics.
  • The most common trick question: a typed nil pointer inside an interface is not nil.
  • The most common real bug: goroutine leaks from sends or receives that never complete. Always give goroutines a way to exit (context, close, buffered result channel).
  • Know the version changes: per-iteration loop variables (Go 1.22), generics (1.18), GOMEMLIMIT (1.19).

Go has become a mainstream backend language in India: infrastructure and platform teams, fintechs, and companies such as Zerodha, Razorpay and many SaaS startups use it for high-concurrency services. Its interviews have a distinctive shape. The syntax is small enough that interviewers spend little time on it and a lot of time on concurrency semantics, where small misunderstandings produce deadlocks, leaks and races. This guide covers 50 questions with code and the exact output you should expect.

If you are coming from Java, it helps to compare with Java multithreading interview questions: Go replaces most explicit locking with communication over channels, but mutexes are still everywhere in real code.

Where and How Go Gets Tested

Round type 1

Language and runtime Q&A

Slices, maps, interfaces, scheduler, GC. Often rapid-fire with "what does this print" snippets.

Round type 2

Concurrency coding

Worker pool, rate limiter, fan-in/fan-out pipeline, concurrent crawler with a depth limit. Graded on correctness under cancellation.

Round type 3

Service design

An HTTP service with timeouts, graceful shutdown and bounded concurrency. Go's standard library makes this a natural follow-up.

Go Language Basics: Slices, Maps, Defer, Errors (Q1-10)

Q1. Why was Go designed, and where does it fit?

Go was created at Google for large networked services: fast compilation, a static single binary, simple syntax, built-in concurrency, garbage collection and a strong standard library (HTTP, JSON, crypto). It trades expressiveness (no inheritance, limited generics until 1.18, explicit error handling) for readability. It fits network services, CLIs and infrastructure (Docker, Kubernetes and Terraform are written in Go); it fits less well where you need manual memory control or hard real-time latency.

Q2. Array vs slice?

An array has a fixed length that is part of its type ([4]int and [5]int are different types) and is copied by value. A slice is a small header of pointer, length and capacity referencing an underlying array. Passing a slice copies the header, not the data, so callee modifications to elements are visible to the caller.

Q3. What does this print? (slice aliasing)

Go
a := []int{1, 2, 3, 4}
b := a[:2]          // len 2, cap 4, shares a's array
b = append(b, 99)   // fits in capacity: writes into a[2]
fmt.Println(a, b)   // [1 2 99 4] [1 2 99]

append writes in place when capacity allows, silently overwriting a[2]. When capacity runs out, it allocates a new array and the slices stop sharing. Defensive fix: a full slice expression a[:2:2] caps capacity so the append must copy.

Q4. How does append grow a slice?

When capacity is exceeded, the runtime allocates a larger array and copies. Growth is roughly doubling for small slices and a gentler factor for large ones (the exact formula is a runtime detail that has changed across versions). Amortised append is O(1). If you know the size, preallocate: make([]T, 0, n).

Q5. Map facts interviewers test.

  • Iteration order is deliberately randomised; never depend on it.
  • Reading a missing key returns the zero value; use v, ok := m[k] to distinguish.
  • Writing to a nil map panics; reading from one is fine.
  • Maps are not safe for concurrent writes: the runtime detects it and crashes with fatal error: concurrent map writes, which cannot be recovered.
  • Map elements are not addressable: m[k].field = 1 does not compile for struct values. Store pointers or reassign the whole struct.

Q6. What does defer do, and in what order?

defer schedules a call to run when the surrounding function returns, in LIFO order. Arguments are evaluated when the defer statement executes, not when the call runs.

for i := 0; i < 3; i++ {
    defer fmt.Println(i)
}
// prints 2, 1, 0

Q7. Can defer change a function's return value?

func f() (result int) {
    defer func() { result++ }()
    return 0
}
// f() returns 1

With a named result, return 0 assigns result = 0, then deferred functions run and can modify it. This is how deferred code annotates errors: defer func() { if err != nil { err = fmt.Errorf("saveOrder: %w", err) } }().

Q8. How does error handling work, and what are wrapping, errors.Is and errors.As?

var ErrNotFound = errors.New("not found")

func getUser(id int) (*User, error) {
    u, err := db.find(id)
    if err != nil {
        return nil, fmt.Errorf("getUser %d: %w", id, err)   // wrap with context
    }
    return u, nil
}

if errors.Is(err, ErrNotFound) { /* 404 */ }        // matches anywhere in the chain

var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" { /* unique violation */ }

Errors are values. %w wraps while keeping the original inspectable; errors.Is compares against sentinel values through the chain; errors.As extracts a specific error type.

Q9. panic vs error, and how does recover work?

Return errors for expected failures. panic is for programmer bugs or truly unrecoverable states. recover() only works inside a deferred function, and only catches panics in the same goroutine. HTTP servers recover per request so one bad handler does not crash the process, but a panic in a goroutine you spawned from that handler will still crash everything unless that goroutine has its own recover.

Q10. Value vs pointer receivers?

Use a pointer receiver when the method mutates the receiver, when the struct is large, or when it contains a sync.Mutex (which must not be copied). Use value receivers for small immutable types. Be consistent per type: mixing them causes surprising interface satisfaction rules (Q13).

Go Interfaces Interview Questions (Q11-17)

Q11. How are interfaces implemented in Go?

Implicitly. A type satisfies an interface by having its methods; there is no implements keyword. That lets consumers define small interfaces for exactly what they need ("accept interfaces, return structs"), and lets you mock a third-party client by declaring an interface with the two methods you use.

Q12. What is an interface value internally?

A pair: the dynamic type and a pointer to the dynamic value (for non-empty interfaces, an itab holding the type and method table). An interface is nil only when both are nil.

Q13. The nil interface trap: what does this print?

Go
type MyErr struct{}

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

func validate(ok bool) error {
    var e *MyErr          // typed nil pointer
    if !ok {
        e = &MyErr{}
    }
    return e              // always a non-nil interface!
}

func main() {
    err := validate(true)
    fmt.Println(err == nil)   // false
}

The returned interface holds type *MyErr with a nil value, so it is not equal to nil and callers treat a success as a failure. Fix: return nil explicitly on the success path. This is probably the single most-asked Go trick question.

Q14. Why doesn't a value type satisfy my interface?

type Shape interface{ Area() float64 }
type Square struct{ s float64 }
func (q *Square) Area() float64 { return q.s * q.s }

var _ Shape = &Square{2}   // OK
var _ Shape = Square{2}    // compile error: Area has pointer receiver

The method set of T includes only value-receiver methods; *T includes both. A value stored in an interface is not addressable, so Go cannot call a pointer method on it.

Q15. Type assertion vs type switch?

s, ok := v.(string)          // comma-ok form never panics
switch x := v.(type) {
case int:    fmt.Println("int", x+1)
case string: fmt.Println("string", len(x))
case nil:    fmt.Println("nil")
default:     fmt.Printf("other %T\n", x)
}

The single-value form v.(string) panics if the type is wrong.

Q16. What is the empty interface, and when should you use generics instead?

interface{} (alias any) holds any value, at the cost of type safety and boxing. Since Go 1.18, generics express "works for any T" with compile-time checking:

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

type Number interface{ ~int | ~int64 | ~float64 }

func Sum[T Number](xs []T) T {
    var s T
    for _, x := range xs { s += x }
    return s
}

Use generics for containers and algorithms; keep interfaces for behaviour.

Q17. Struct embedding vs inheritance?

Embedding promotes the embedded type's fields and methods to the outer type, which looks like inheritance but is composition: there is no polymorphic override, and the embedded method's receiver is still the inner value. Embedding an interface in a struct is a common way to partially implement a large interface in tests.

Goroutines and the Go Scheduler (Q18-23)

Q18. Goroutine vs OS thread?

GoroutineOS thread
Initial stackA few KB, grows and shrinks dynamicallyTypically around 1-8 MB reserved, fixed
Scheduled byGo runtime, in user spaceOS kernel
Switch costCheap: save a few registersKernel context switch
Practical countHundreds of thousands are feasibleThousands before memory and scheduling hurt
IdentityNo exposed ID (deliberately)TID

Q19. Explain the GMP model.

G = goroutine M = OS thread (machine) P = processor (scheduling context) global run queue: [G][G] │ ┌────┴─────┐ ┌──────────┐ │ P0 │ │ P1 │ GOMAXPROCS = number of Ps │ local: │ │ local: │ │ [G][G][G]│ steal │ [ ] │ ◄── idle P steals half of P0's queue └────┬─────┘ ◄─────── └────┬─────┘ │ │ M0 (running G) M1 (running G) │ syscall blocks? M0 detaches from P0; P0 handed to another M

Each P has a local run queue of goroutines; an M must hold a P to run Go code. Idle Ps steal work from others. When a goroutine blocks in a system call, its M is detached and the P moves to another thread so other goroutines keep running. Network I/O does not block threads at all: the netpoller (epoll/kqueue/IOCP) parks the goroutine and wakes it when the socket is ready.

Q20. What is GOMAXPROCS?

The number of Ps, so the maximum number of goroutines executing Go code simultaneously. It defaults to the number of available CPUs. In containers, older Go versions read the host's core count and ignored the cgroup CPU limit, causing heavy throttling; libraries like automaxprocs fixed that, and Go 1.25 made the default container-aware. Check which version your service uses.

Q21. Is goroutine scheduling preemptive?

Yes. Originally preemption happened only at function calls, so a tight loop without calls could hog a P. Since Go 1.14 the runtime uses asynchronous preemption via signals, so long-running loops can be interrupted.

Q22. What does this print?

func main() {
    go fmt.Println("hello from goroutine")
}

Usually nothing. When main returns, the program exits without waiting for other goroutines. Use a sync.WaitGroup or a channel to wait.

Q23. Is concurrency the same as parallelism?

No. Concurrency is structuring a program as independently executing tasks; parallelism is executing them simultaneously on multiple cores. A Go program with GOMAXPROCS=1 is still concurrent (it interleaves goroutines) but not parallel. Rob Pike's talk "Concurrency is not Parallelism" is the canonical reference.

Go Channels and Select Interview Questions (Q24-33)

Q24. Unbuffered vs buffered channels?

An unbuffered channel is a synchronisation point: a send blocks until a receiver takes the value, and vice versa. A buffered channel of capacity n lets sends proceed until n values are queued; receives block only when it is empty. Buffers absorb bursts; they do not fix a consumer that is permanently slower than the producer.

Q25. The channel behaviour table.

Operationnil channelOpen channelClosed channel
SendBlocks foreverBlocks until received / buffer spacePanic
ReceiveBlocks foreverBlocks until a value is availableRemaining buffered values, then zero value with ok == false, never blocks
ClosePanicSucceedsPanic

Rule of thumb: only the sender closes, and only when no more sends will happen. Closing is a broadcast to receivers ("no more values"), not a resource cleanup; channels you never close are garbage collected normally.

Q26. Why does this deadlock?

func main() {
    ch := make(chan int)
    ch <- 1               // blocks: nobody is receiving
    fmt.Println(<-ch)
}
// fatal error: all goroutines are asleep - deadlock!

The send on an unbuffered channel waits for a receiver that can never arrive, since the only goroutine is the one blocked. A buffer of 1, or sending from a separate goroutine, fixes it. The runtime detects only global deadlock; if any other goroutine (for example an HTTP server) is alive, a stuck goroutine just leaks silently.

Q27. How does range over a channel end?

for v := range ch receives until the channel is closed and drained. If the producer never closes it, the loop blocks forever, a common leak.

Q28. How does select choose a case?

It blocks until at least one case can proceed. If several are ready, it chooses uniformly at random, so no case starves. With a default case, select never blocks.

select {
case msg := <-orders:
    handle(msg)
case <-ctx.Done():
    return ctx.Err()
case <-time.After(2 * time.Second):
    return errors.New("timed out waiting for order")
}

A follow-up: in a long-running loop, prefer one time.NewTimer or time.Ticker you reset or stop rather than calling time.After every iteration. Before Go 1.23, unstopped timers from time.After were not collected until they fired, which could pile up memory in hot loops.

Q29. How do you do a non-blocking send?

select {
case events <- e:
default:
    droppedEvents.Inc()   // buffer full: drop instead of blocking the request path
}

Q30. How can a nil channel be useful?

Because a nil channel is never ready, setting a channel variable to nil disables its case in a select. Merging two channels until both are closed:

func merge(a, b <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for a != nil || b != nil {
            select {
            case v, ok := <-a:
                if !ok { a = nil; continue }   // a closed: stop selecting it
                out <- v
            case v, ok := <-b:
                if !ok { b = nil; continue }
                out <- v
            }
        }
    }()
    return out
}

Q31. Write a worker pool with cancellation.

Go
package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

type Job struct{ ID int }
type Result struct {
    JobID int
    Out   string
}

func worker(ctx context.Context, id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            return
        case j, ok := <-jobs:
            if !ok {
                return // no more jobs
            }
            r := Result{JobID: j.ID, Out: fmt.Sprintf("worker %d did job %d", id, j.ID)}
            select {
            case results <- r:
            case <-ctx.Done():
                return // never block forever on a send
            }
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    jobs := make(chan Job)
    results := make(chan Result)
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(ctx, i, jobs, results, &wg)
    }

    go func() { // producer: the only sender, so it closes jobs
        defer close(jobs)
        for i := 1; i <= 10; i++ {
            select {
            case jobs <- Job{ID: i}:
            case <-ctx.Done():
                return
            }
        }
    }()

    go func() { // close results only after every worker has exited
        wg.Wait()
        close(results)
    }()

    for r := range results {
        fmt.Println(r.Out)
    }
}

What the interviewer checks: the producer closes jobs; a separate goroutine closes results after wg.Wait() (closing it from a worker would panic other workers); every blocking operation also listens on ctx.Done(); wg.Add happens before go, not inside the goroutine.

Q32. Build a pipeline (generator, square, sink).

func gen(ctx context.Context, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

func square(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

// for v := range square(ctx, gen(ctx, 1, 2, 3)) { fmt.Println(v) }  // 1 4 9

Each stage owns and closes its output channel. Fan-out means running several square stages reading the same input; fan-in means merging their outputs (Q30's pattern generalised with a WaitGroup).

Q33. Implement a simple rate limiter.

limiter := time.NewTicker(100 * time.Millisecond)   // 10 requests/second
defer limiter.Stop()
for _, req := range requests {
    <-limiter.C          // wait for the next tick
    go call(req)
}

For bursts, use a buffered channel as a token bucket, or the production-grade golang.org/x/time/rate limiter (rate.NewLimiter(10, 20), then limiter.Wait(ctx)).

The sync Package and context (Q34-40)

Q34. Channels or mutexes?

The Go proverb: "Don't communicate by sharing memory; share memory by communicating." In practice: use channels to transfer ownership of data or coordinate stages; use a mutex to protect shared state such as a cache or counter. A mutex around a map is simpler and faster than a goroutine-owned map with request channels for most cases.

Q35. Mutex vs RWMutex?

sync.Mutex allows one holder. sync.RWMutex allows many concurrent readers or one writer; it helps when reads dominate and critical sections are non-trivial. For tiny critical sections, a plain Mutex is often faster because RWMutex has more bookkeeping. Never copy a struct containing a mutex after first use; go vet catches this.

Go: a concurrency-safe cache
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(k string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[k]
    return v, ok
}

func (c *Cache) Set(k, v string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[k] = v
}

Q36. What are WaitGroup, Once and errgroup for?

WaitGroup waits for a set of goroutines (call Add before starting them; Go 1.25 adds a wg.Go(func) helper that does Add and Done for you). sync.Once runs initialisation exactly once, safely, across goroutines. errgroup.Group (from golang.org/x/sync) runs goroutines, returns the first error, cancels a shared context when one fails, and with SetLimit bounds concurrency: the idiomatic way to call five downstream services in parallel.

Q37. When is sync.Map appropriate?

The docs describe two cases: keys written once and read many times (caches that only grow), or goroutines working on disjoint key sets. For general read-write maps, a regular map with a Mutex is usually simpler and as fast or faster, with type safety.

Q38. What does sync/atomic offer?

Lock-free operations on single values: atomic.Int64 with Add, Load, Store, CompareAndSwap. Ideal for counters and flags. Anything involving more than one variable that must change together needs a mutex.

Q39. How does context propagate cancellation?

func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
    defer cancel()   // release resources even on success

    order, err := h.repo.Find(ctx, r.PathValue("id"))   // DB driver honours ctx
    if errors.Is(err, context.DeadlineExceeded) {
        http.Error(w, "timeout", http.StatusGatewayTimeout)
        return
    }
    // ...
}

Contexts form a tree: cancelling a parent cancels all children. r.Context() is cancelled when the client disconnects, so a well-plumbed service stops wasting work on abandoned requests. Always defer cancel(); go vet warns if you drop it.

Q40. Context rules and anti-patterns?

  • Pass ctx as the first parameter; do not store it in structs.
  • Never pass a nil context; use context.TODO() if unsure.
  • Use WithValue only for request-scoped metadata (trace IDs, auth claims), with unexported key types to avoid collisions, never for optional function parameters.
  • Check ctx.Err() in long loops so CPU-bound work also stops.

Common Go Concurrency Bugs Interviewers Ask About (Q41-46)

Q41. What is a goroutine leak? Show one and fix it.

// LEAKY: on timeout, the goroutine blocks forever on the unbuffered send
func fetchWithTimeout(url string) (string, error) {
    ch := make(chan string)
    go func() { ch <- slowFetch(url) }()
    select {
    case body := <-ch:
        return body, nil
    case <-time.After(time.Second):
        return "", errors.New("timeout")   // nobody will ever receive from ch
    }
}

// FIXED: buffer of 1 lets the goroutine finish and be collected
ch := make(chan string, 1)

Better still, pass a context into slowFetch so the work itself stops. Detect leaks by watching runtime.NumGoroutine() or the /debug/pprof/goroutine profile grow over time, and in tests with go.uber.org/goleak.

Q42. What is a data race and how do you find it?

Two goroutines access the same memory concurrently, at least one writes, and there is no synchronisation. Results are undefined, not just "slightly wrong". Find races with go test -race ./...; it reports both stack traces. Run it in CI.

var count int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() { defer wg.Done(); count++ }()   // RACE: read-modify-write
}
wg.Wait()
fmt.Println(count)   // often less than 1000; -race flags it
// fix: atomic.Int64, or a mutex around count++

Q43. The loop variable capture bug: did Go 1.22 fix it?

for _, v := range []string{"a", "b", "c"} {
    go func() { fmt.Println(v) }()
}

Before Go 1.22, all goroutines shared one v variable and often printed "c c c". Since Go 1.22 (for modules declaring go 1.22 or later in go.mod), each iteration has its own variable, so it prints a, b and c in some order. In an interview, mention both behaviours and the old fix (v := v or passing v as an argument).

Q44. What's wrong with calling wg.Add inside the goroutine?

wg.Wait() may run before any goroutine has called Add, see a zero counter and return immediately. Call Add in the parent before the go statement.

Q45. Spawning a goroutine per request without limits: what breaks?

Goroutines are cheap, not free. A burst of 100,000 requests each launching goroutines that call a database exhausts the connection pool, memory, or the downstream service. Bound concurrency with a worker pool, a semaphore (a buffered channel of size N, or golang.org/x/sync/semaphore), or errgroup.SetLimit.

Q46. A mutex deadlock example?

Two goroutines lock A then B and B then A respectively: each holds one and waits forever for the other. Prevent with a consistent lock order, shorter critical sections, or not calling unknown code (callbacks) while holding a lock. Another classic: Go mutexes are not reentrant, so a method that holds the lock and calls another method that locks the same mutex deadlocks itself.

Go Garbage Collector, Memory and Performance (Q47-50)

Q47. How does Go's garbage collector work?

A concurrent, tri-colour mark-and-sweep collector. It is non-generational and non-compacting. Marking runs concurrently with your goroutines using a write barrier, with brief stop-the-world phases typically well under a millisecond. Goroutines that allocate heavily during a cycle are drafted into "mark assist", which is how GC cost shows up as latency in allocation-heavy code.

Q48. What do GOGC and GOMEMLIMIT do?

GOGC (default 100) sets the next GC target relative to the live heap: with 100 MB live, the next cycle starts around 200 MB. Higher values mean fewer GCs and more memory. GOMEMLIMIT (Go 1.19+) is a soft total memory limit: the GC works harder as the process approaches it, which is ideal in containers. A common setup is GOMEMLIMIT at roughly 80-90% of the container limit so the GC acts before the OOM killer does.

Q49. What is escape analysis?

The compiler decides whether a value can live on the goroutine's stack (cheap, freed automatically) or must "escape" to the heap (GC-managed). Returning a pointer to a local, storing it in an interface, or capturing it in a long-lived closure usually causes escape. Inspect decisions with go build -gcflags=-m. Fewer heap allocations means less GC work.

Q50. How do you profile a slow Go service?

import _ "net/http/pprof"   // registers /debug/pprof/ handlers on DefaultServeMux

// then:
// go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30   (CPU)
// go tool pprof http://localhost:6060/debug/pprof/heap                 (memory)
// curl http://localhost:6060/debug/pprof/goroutine?debug=2             (all stacks)

Write benchmarks with testing.B and run go test -bench . -benchmem to see allocations per operation. Typical wins: preallocating slices and maps, reusing buffers with sync.Pool, avoiding needless []byte/string conversions, and using strings.Builder. Expose the pprof endpoints only on an internal port.

How to Prepare for a Go Interview

  1. Write the concurrency patterns from memory. Worker pool, pipeline, fan-in merge, bounded parallel fetch with errgroup, rate limiter. Each should handle cancellation and close channels correctly.
  2. Run everything with -race. Deliberately write the racy counter, the leaky timeout and the wg.Add-inside-goroutine bug, and watch the tooling catch them.
  3. Build one real service. An HTTP API with net/http (Go 1.22's router supports methods and path parameters), context timeouts on DB calls, structured logging with log/slog, graceful shutdown with server.Shutdown(ctx) on SIGTERM, and pprof.
  4. Read the source of truth. The Go Memory Model and Effective Go are short and answer most "why" questions.
⚠️
Red flags interviewers noticeClosing a channel from the receiver side; using time.Sleep to "wait for goroutines"; ignoring errors with _; launching goroutines with no exit path; claiming maps are thread-safe for reads and writes; and treating channels as a replacement for every mutex.

Go roles almost always include a service-design discussion, so pair this with REST API design questions and microservices interview questions. Many Go services ship as containers on Kubernetes, which is why Docker and Kubernetes questions often follow in the same loop.

Frequently Asked Questions

What is the difference between a goroutine and an OS thread?▾
A goroutine is a function scheduled by the Go runtime rather than the operating system. It starts with a small stack of a few kilobytes that grows and shrinks as needed, and the runtime multiplexes many goroutines onto a small number of OS threads. That makes goroutines cheap enough to create hundreds of thousands, whereas OS threads have large fixed stacks and expensive kernel context switches.
What happens when you send on a closed channel in Go?▾
Sending on a closed channel panics. Receiving from a closed channel never blocks: it returns any remaining buffered values and then the zero value with ok set to false. Closing an already closed channel or a nil channel also panics, which is why the convention is that only the sender closes a channel, and only once.
Why is my Go error not nil even though I returned a nil pointer?▾
An interface value is nil only when both its dynamic type and its value are nil. Returning a nil *MyError as an error produces an interface whose type is *MyError and whose value is nil, so err != nil is true. Return a literal nil for the success case instead of a typed nil pointer.
How does select work in Go?▾
select waits on several channel operations and runs the case of whichever one is ready first. If several are ready at the same time, it picks one at random to avoid starvation. A default case makes select non-blocking, and a case on a nil channel is never selected, which lets you disable cases dynamically.
What is context.Context used for in Go?▾
context carries cancellation signals, deadlines and request-scoped values across API boundaries and goroutines. A handler creates a context with a timeout, passes it to database and HTTP calls, and when the client disconnects or the deadline passes, every downstream operation listening on ctx.Done() stops. Pass it as the first parameter and do not store it in structs.
How does garbage collection work in Go?▾
Go uses a concurrent, tri-color mark-and-sweep collector that is non-generational and non-compacting and runs mostly alongside your program with short stop-the-world pauses. GOGC (default 100) controls how much the heap may grow before the next cycle, and GOMEMLIMIT sets a soft memory ceiling. Reducing allocations through escape analysis and reuse is the main way to reduce GC cost.
How do you detect data races in Go?▾
Run tests or the binary with the race detector using go test -race or go run -race. It instruments memory accesses and reports conflicting unsynchronised reads and writes with stack traces for both goroutines. It only finds races on code paths that actually execute, so run it in CI with tests that exercise concurrency.

Prepare for Go Backend Interviews

Prepflix's interview program pairs language-level depth like Go concurrency with DSA, system design and mock interviews for Indian product companies.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.