FREE STUDY GUIDE · GO BACKEND INTERVIEWS

Go (Golang) interview questions, with the why behind each answer.

A practical starting point for mid-level and senior backend interviews. Read 15 complete answers here for free, then use the follow-ups to test whether you can explain the runtime and concurrency trade-offs in your own words.

These answers distinguish language guarantees from runtime implementation details. Check version-sensitive behavior against the Go release you use.

01 / TOPIC

Goroutines and the runtime

Explain how Go runs work before reaching for scheduler trivia.

How is a goroutine different from an OS thread?

A goroutine is a unit of work managed by the Go runtime. The runtime schedules many goroutines across a smaller or changing set of operating-system threads; a goroutine is not permanently tied to one thread. In an interview, explain how this makes concurrency convenient, then distinguish concurrency from simultaneous execution.

Follow-up: What happens when a goroutine blocks in a system call?

What do G, M, and P mean in Go’s scheduler?

G represents a goroutine, M an operating-system thread, and P the runtime resource needed to execute Go code. An M normally needs a P to run a G. Treat G/M/P as a useful model of the current runtime, not as a promise that every scheduling detail is fixed by the language specification.

Follow-up: How is the number of Ps related to GOMAXPROCS?

Does GOMAXPROCS limit how many goroutines you can create?

No. It limits how many threads can execute Go code at the same time, not the number of goroutines that may exist. Goroutines can be runnable, waiting, or blocked. A good answer discusses CPU parallelism separately from the number of concurrent tasks.

Follow-up: Why can a program have thousands of goroutines but only a few running at once?

COMPLETE ANSWER

How does sync.Mutex behave under contention?

The uncontended path attempts to acquire the lock atomically. When several goroutines compete, a waiter may briefly spin and then park so it does not waste a CPU indefinitely. Unlocking makes a waiter eligible to run; runtime fairness behavior helps prevent a waiter from being bypassed forever. The exact spin counts, state bits, and starvation thresholds are implementation details that can change between Go releases. In production, keep critical sections short, avoid holding a lock across slow I/O, and measure contention before replacing a simple Mutex with a more complex design. A Mutex must not be copied after first use.

Follow-up: When might an RWMutex be slower than a Mutex?

For the underlying behavior: Go runtime scheduler source ↗

02 / TOPIC

Channels and coordination

Focus on blocking behavior, ownership, and safe shutdown.

When would you use a buffered rather than an unbuffered channel?

An unbuffered send waits for a receiver. A buffered channel lets sends proceed while there is capacity, so producers and consumers can be decoupled up to that limit. A buffer changes backpressure; it does not make an unbounded queue or remove the need to plan cancellation and shutdown.

Follow-up: What happens when the buffer becomes full?

Who should close a channel, and what does a receiver see?

The goroutine that owns sending usually closes the channel to signal that no more values will arrive. A receiver can drain buffered values after close; a receive after the buffer is empty returns the zero value with ok == false. Sending to a closed channel panics, so multiple senders need an explicit shutdown plan.

Follow-up: Why is closing a channel from a receiver usually unsafe?

What does select do when several cases are ready?

Go chooses one ready communication case. Do not rely on a particular case winning or on fairness for correctness. A default case runs only when no communication case can proceed; used carelessly in a loop, it can cause busy spinning.

Follow-up: What effect does a nil channel have in a select case?

How do you stop a worker when its request is canceled?

Pass a context from the caller and make blocking work observe ctx.Done(). Cancellation is a signal, not a forced kill: the worker must return, and callers should still wait for any owned goroutines to finish. Keep the job-channel close operation with its sender.

Follow-up: How would you avoid leaking a sender blocked on a channel?

How do you detect and fix a data race?

A data race occurs when concurrent operations access the same memory without the required synchronization and at least one writes. Run tests with go test -race ./... to find many real executions of this pattern. Fix ownership or add synchronization such as a mutex or channel; passing the race detector is not proof that every possible path is race-free.

Follow-up: How is a data race different from a higher-level ordering bug?

COMPLETE ANSWER

What does a channel maintain, and what happens during send and receive?

A channel tracks its element type and size, buffered-element count and capacity, buffer positions, and queues of blocked senders and receivers. On a send, the runtime can hand the value directly to a waiting receiver; otherwise it uses free buffer space, or parks the sender until communication becomes possible. A receive can take a waiting sender’s value, read a buffered value, or park if the channel is empty and still open. Closing does not discard buffered values: receivers drain them first, then receive the zero value with ok == false. Sending to a closed channel panics. These runtime fields explain the behavior, but the language-level blocking and close rules are the contract to rely on.

Follow-up: Why can a send on a full buffered channel still block?

COMPLETE ANSWER

Are built-in Go maps safe for concurrent use?

Concurrent reads of a built-in map are fine when no goroutine writes to it. Once writes may overlap with reads or other writes, coordinate access: guard the map with a Mutex or RWMutex, give one goroutine ownership and communicate through channels, or choose a specialized concurrent structure. Do not treat an apparently successful test run as proof of safety; unsynchronized concurrent access is a data race. For an interview, state the rule first, then explain how you would preserve invariants across operations such as “check, then insert.” A concurrent container alone does not make a multi-step operation atomic.

Follow-up: When would a map plus Mutex be clearer than sync.Map?

COMPLETE ANSWER

When is sync.Map appropriate?

sync.Map is safe for concurrent use and is optimized for two common patterns: entries written once and read many times, and goroutines operating mostly on disjoint key sets. It can reduce lock contention in those workloads. Most other code is easier to reason about with a typed map protected by a Mutex or RWMutex, especially when several fields must satisfy one invariant. The implementation has used read-optimized and slower update paths, but those internals are not a stable API; choose it from workload and measurements, not a memorized field layout. It must not be copied after first use.

Follow-up: How would you benchmark sync.Map against a locked map for your workload?

For the underlying behavior: Go memory model ↗

03 / TOPIC

Garbage collection and memory

Separate the language-level answer from compiler and collector behavior.

How does Go garbage collection work?

Go’s collector traces reachable heap objects and makes unreachable memory available for reuse with a mark-sweep approach. Much of the work runs concurrently with application code, although brief pauses still occur. For performance questions, talk about allocation rate, live heap, and latency rather than claiming that GC means “no pauses.”

Follow-up: Why can reducing allocations change GC CPU cost?

What is escape analysis, and does taking an address always allocate on the heap?

Escape analysis is a compiler decision about whether a value can safely live in a goroutine stack frame or needs longer-lived storage. Taking an address alone does not prove a heap allocation. Inspect your own code with go build -gcflags=-m=3 ./... and verify important performance claims with a benchmark or profile.

Follow-up: Why might returning a pointer be safe in Go?

COMPLETE ANSWER

How does Go allocate small and large objects?

The Go runtime groups many small heap allocations by size class. A P-local mcache serves common small-object allocations without taking a global lock; when it needs more space, it obtains spans from central structures. An mspan manages pages containing objects of a size class, while mheap manages page-level heap allocation. Tiny, small, and large objects can take different paths, and sufficiently large allocations bypass the small-object fast path. Exact thresholds and data structures depend on the Go release. First ask whether an object escapes at all: values kept on a goroutine stack do not follow the heap-allocation path. Use compiler diagnostics and allocation profiles to verify a performance claim about real code.

Follow-up: Why can an allocation-heavy hot path increase both CPU and GC work?

For the underlying behavior: Official Go GC guide ↗

REASON THROUGH THE CODE

A channel-close question in 10 lines.

The sender closes the channel after its final send. A range loop receives both buffered values and then stops. Ask yourself how the behavior changes if the receiver closes the channel instead.

package main

import "fmt"

func main() {
    jobs := make(chan int, 2)
    jobs <- 1
    jobs <- 2
    close(jobs) // sender owns close
    for job := range jobs {
        fmt.Println(job)
    }
}

KEEP STUDYING

Take the short answers with you.

The free PDF collects 14 quick answers. The complete playbook goes deeper into implementation details, code examples, and additional interview questions.