10 Go Interview Questions with Runnable Examples and Failure Cases

Quick Overview
Practice Go concurrency, interfaces, slices, cancellation, and method sets with seven runnable examples, expected outputs, and failure cases.
Go interview questions are easier to answer when you can predict a small program's behavior and explain what would make it fail. Start with the contract: who owns the data, what synchronizes access, and how each goroutine finishes. Then name the language feature or library that provides that behavior.
These ten questions cover Go fundamentals with independent, runnable teaching examples. They use synthetic data and do not reproduce solutions to the linked interview prompts. Save one complete Go block at a time as main.go and run go run main.go. Predict the output before running it. The examples use only the standard library and were checked with Go 1.27.3. All seven stated outputs matched; the shared-map and cancellation examples also passed race-detector runs. Those runs check the exercised paths, not every possible program behavior.
1. How is a goroutine different from an OS thread?
A goroutine is an independently executing function call managed by the Go runtime. The operating system schedules threads; the runtime schedules goroutines onto threads. There is no one-thread-per-goroutine promise, and launching more goroutines does not guarantee more parallel execution.
Concurrency describes how work is organized; parallelism describes simultaneous execution. Neither removes the need to synchronize shared state. A goroutine blocked forever on a send or receive can retain memory and other resources even when its original caller no longer needs the result.
Before writing go work(), explain three things: how work starts, what makes it stop, and how the caller observes completion or failure. A buffer can delay a blocked send without providing a shutdown policy. The Go team's pipeline and cancellation guide develops that lifecycle reasoning.
Practice the public prompt for Go concurrency and database choices. Keep scheduler behavior separate from guarantees your own program must establish.
2. What changes when a channel is buffered, closed, or nil?
An unbuffered send needs a corresponding receiver. A buffered send can complete while space remains; that does not mean the consumer has finished processing the value.
| Channel state | Send behavior | Receive behavior |
|---|---|---|
| Open, unbuffered | Waits for a receiver | Waits for a sender |
| Open, buffered | Waits when full | Waits when empty |
| Closed | Panics | Drains queued values, then returns the zero value with ok == false |
| Nil | Blocks indefinitely | Blocks indefinitely |
Inside a select, a nil channel disables its communication case. A closed channel's receive can remain immediately ready after its buffer drains, so an unhandled closed-channel case can cause a busy loop.
package main
import "fmt"
func main() {
values := make(chan int, 1)
values <- 7
close(values)
value, ok := <-values
fmt.Println(value, ok)
value, ok = <-values
fmt.Println(value, ok)
var disabled chan int
select {
case disabled <- 1:
fmt.Println("sent")
default:
fmt.Println("nil send disabled")
}
}
7 true
0 false
nil send disabled
The default demonstrates readiness once; it is not a recommended polling loop. Coordinate closure with the side that knows all sends are finished. Sending on a closed channel or closing it twice panics. Use the channel specification for channel behavior and the memory model for synchronization rules. Communication does not protect unrelated concurrent accesses automatically.
3. Why can an error containing a nil pointer be non-nil?
An interface is nil when it has neither a dynamic type nor a dynamic value. Assigning a typed nil pointer to an interface supplies a dynamic type, so the interface is not nil.
package main
import "fmt"
type problem struct{}
func (*problem) Error() string { return "problem" }
func typedNil() error {
var err *problem
return err
}
func success() error { return nil }
func main() {
fmt.Println(typedNil() == nil)
fmt.Println(success() == nil)
}
false
true
Return the literal nil for the successful result of an error-returning function. A nil receiver does not universally make method calls safe: the method implementation decides whether it dereferences the receiver. This example's Error method does not dereference one.
The Go FAQ on nil errors explains the distinction. Continue with the internal guide to typed nils, method sets, and error values. Explain what an interface contains before predicting an equality test.
4. When does append change another slice's data?
An array's length is part of its type. A slice describes a segment of a backing array, with a length and capacity. Copying a slice value does not copy its elements.
Appending can reuse the backing array when capacity permits. Restricting a slice's capacity can force an append to allocate new storage:
package main
import "fmt"
func main() {
base := []int{1, 2, 3}
view := base[:2]
grown := append(view, 9)
fmt.Println(base, grown)
base = []int{1, 2, 3}
limited := base[:2:2]
isolated := append(limited, 9)
fmt.Println(base, isolated)
}
[1 2 9] [1 2 9]
[1 2 3] [1 2 9]
The three-index slice limits capacity to two. It does not copy the existing elements or make the slice immutable: modifying an existing element through limited would still affect base before the append. Retain the returned slice when you want to use the appended result; a function's local slice header does not automatically update its caller's header.
Read the slice expressions and append contract. Avoid relying on an exact capacity-growth factor. Practice predicting a sub-slice append's output, then explore capacity, aliasing, and append surprises.
5. Is a map safe when several goroutines access it?
A fully initialized map can be shared for reads after safe publication if no goroutine changes it. Concurrent access involving writes needs an appropriate synchronization policy. Define the entire protected operation: a read followed by an increment and write must be one critical section.
package main
import (
"fmt"
"sync"
)
func main() {
counts := make(map[string]int)
var mu sync.Mutex
var wg sync.WaitGroup
for worker := 0; worker < 4; worker++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := 0; n < 100; n++ {
mu.Lock()
counts["jobs"]++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(counts["jobs"])
}
400
The final read happens after all workers finish; there are no concurrent writes at that point. Every worker uses the same mutex. Protecting writes while leaving concurrent reads unprotected would not establish the required policy.
sync.Mutex provides synchronization. sync.Map serves particular concurrent access patterns and does not make arbitrary multi-operation workflows atomic. Use the map FAQ and practice identifying and handling race conditions. A runtime map failure is a symptom, not a synchronization strategy.
6. What runs during panic unwinding, and where does recover work?
Deferred calls run in reverse registration order when the surrounding function returns, including during panic unwinding. Their function values and arguments are evaluated when the defer statement executes; a closure can instead read captured variables when it runs.
package main
import "fmt"
func run() (message string) {
defer func() {
if value := recover(); value != nil {
message = fmt.Sprint("recovered:", value)
}
}()
defer fmt.Println("second")
defer fmt.Println("first")
panic("boom")
}
func main() {
fmt.Println(run())
}
first
second
recovered:boom
Recovery must happen through a direct recover call in the appropriate deferred function in the panicking goroutine. A recover in the parent goroutine does not contain a child's panic. Successful recovery does not resume execution at the panic statement; here, the deferred function sets the named result before run returns.
Effective Go's recovery section explains the control flow. Use returned errors for expected failures. At a deliberate recovery boundary, define how failure is reported and whether state remains usable rather than silently treating recovered work as success.
7. Does canceling a context terminate a goroutine?
No. Cancellation is a cooperative signal. The operation must observe it and return. Passing a context through an API is useful only when downstream work also honors its lifetime.
This worker has no sender on its jobs channel. Cancellation gives it an exit path, and the caller waits for that exit without a sleep:
package main
import (
"context"
"fmt"
)
func receive(ctx context.Context, jobs <-chan int) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-jobs:
return nil
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
jobs := make(chan int)
finished := make(chan error, 1)
go func() { finished <- receive(ctx, jobs) }()
cancel()
fmt.Println(<-finished)
}
context canceled
If both a job and cancellation are ready, select does not give cancellation priority. The sample's jobs channel has no sender, so only cancellation becomes ready. A real worker must define how much already-started work it finishes and how blocking sends or I/O respond to shutdown.
Follow the context package contract: pass contexts explicitly through calls, call returned cancellation functions, and use context values for request-scoped metadata rather than optional function arguments. Continue with deadlines, cancellation, and goroutine leaks. In service code, connect that lifecycle to the REST API interview guide.
8. How do you investigate heap allocation and GC cost?
Start with evidence from the actual workload. Escape analysis influences where values are allocated; returning a pointer is not automatically a performance bug. Keeping a small slice of a large backing array reachable can retain more memory than its length suggests.
In a project with benchmarks, measure allocation counts and inspect compiler diagnostics:
go test -bench . -benchmem
go build -gcflags='-m'
Record the toolchain, workload, input sizes, and measurement method. Profiles distinguish allocation volume, retained memory, CPU work, and lock contention. A change that reduces one measure can worsen another.
The official garbage collector guide explains the time, memory, and allocation trade-offs. Consult it for the runtime version you use instead of promising a fixed pause time or memorizing an implementation detail as a language guarantee. A sync.Pool can discard stored items and is not durable application storage.
Use the internal pprof exercises on hotspots, retained memory, and contention to practice selecting evidence before proposing an optimization.
9. Why does a pointer satisfy an interface when the value does not?
For a defined non-interface type T, its method set includes methods declared with receiver T. The method set of *T also includes methods declared with receiver *T. Addressable-value method-call shorthand does not change interface satisfaction.
package main
import "fmt"
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
type Incrementer interface{ Inc() }
func main() {
var counter Counter
counter.Inc()
var incrementer Incrementer = &counter
incrementer.Inc()
fmt.Println(counter.n)
}
2
The direct call can take the address of counter. Assigning a Counter value itself to Incrementer would fail because the required method is in the pointer's method set. The receiver choice here allows mutation; it does not make concurrent increments safe.
A value receiver copies the receiver value, but referenced data inside it can remain shared. Do not copy a Mutex or WaitGroup after use. Follow the method-set rules and choose receivers according to mutation, copying, and API semantics.
10. How do select, WaitGroup, and Mutex fit together?
They answer different coordination questions:
| Requirement | Tool | Remaining responsibility |
|---|---|---|
| Wait for a channel operation | select | Define closure, cancellation, and readiness behavior |
| Wait for a collection of tasks | sync.WaitGroup | Register tasks correctly and propagate failures separately |
| Protect shared mutable state | sync.Mutex | Make every relevant access follow the same locking policy |
| Signal cancellation or a deadline | context.Context | Ensure work observes it and actually finishes |
In the counter example, Add happens before launching each worker, each worker defers Done, and Wait precedes reading the final map. The WaitGroup does not protect the increments; the mutex does. Neither automatically returns a worker's error to the caller.
For a pipeline, sketch who sends, who receives, who closes each channel, and who waits for senders before closing an output. A consumer that stops early must not strand producers on blocked sends. Use the sync documentation to check coordination contracts rather than treating all concurrency primitives as substitutes.
For a service-level exercise, review the public prompt for a Go token-bucket rate limiter used as HTTP middleware. Identify shared state and lifecycle boundaries before changing its design.
Build a practice loop around observable behavior
Explain each answer, predict an output or failure, then change one assumption. Close the jobs channel, remove a cancellation path, introduce a shared write, or change a receiver. State the expected result before running the variation.
Run concurrent examples with go run -race main.go, or use go test -race ./... in a module containing tests. The race detector guide describes a dynamic check over executed paths. A clean run does not prove all schedules race-free, and race freedom does not prove freedom from deadlock or goroutine leaks.
The Go release history and Go 1 compatibility policy help you identify version-specific behavior; the compatibility policy includes exceptions. For algorithm practice, use the coding interview patterns guide. Compare Go's scheduling and blocking model with the Node.js interview guide when preparing cross-language explanations. Eztrackr's broader technical interview preparation guide offers an additional preparation checklist; use the official Go references above for language and runtime contracts.
Finish by explaining a limitation of your example. Small, repeatable programs establish particular behavior. They do not replace realistic load tests, production observability, or a clear shutdown design.
Comments (0)