100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Go

Common Go Interview Questions

The Go interview questions you'll actually be asked, from goroutines and channels to nil interfaces and defer semantics.

Interview PrepIntermediate16 min readJul 8, 2026
Analogies

Overview

Go interviews rarely ask you to recite syntax. Instead they probe whether you understand *why* Go is built the way it is: why goroutines are cheap, why the language has no exceptions, why nil can be tricky, and why composition beats inheritance. This guide walks through the questions that come up again and again, with answers pitched at the level an interviewer actually wants to hear.

🏏

Cricket analogy: A commentator asking a young pacer why they take a short run-up, rather than just show me your run-up, mirrors interviews probing why goroutines are cheap and why Go favors composition, not just syntax recall.

Frequently Asked Questions

What is the difference between a goroutine and an OS thread?

A goroutine is a lightweight, user-space unit of concurrency managed by the Go runtime scheduler, not by the operating system. It starts with a small (a few KB) growable stack, versus the fixed multi-MB stack of an OS thread, so you can spawn hundreds of thousands of goroutines cheaply. The Go runtime multiplexes many goroutines onto a much smaller number of OS threads (the M:N scheduler, using goroutines, OS threads, and logical processors/P's), performing context switches in user space, which is far cheaper than a kernel-level thread switch.

🏏

Cricket analogy: Domestic net bowlers can be called up in minutes without central contracts, unlike centrally contracted internationals with heavy overhead; goroutines start with a tiny few-KB stack versus an OS thread's multi-MB stack, so the runtime multiplexes many onto few OS threads like a franchise fielding many net bowlers through one ground.

When would you use a channel versus a sync.Mutex?

Use a channel when you're coordinating or communicating between goroutines — handing off data, signaling completion, or implementing pipelines and worker pools; the idiom is 'share memory by communicating.' Use a sync.Mutex when multiple goroutines need to protect access to a single shared piece of state in place, such as a counter or a map, and communicating the value over a channel would be needless overhead. Neither is strictly superior; channels model flow of data and ownership transfer, mutexes model protected shared state.

🏏

Cricket analogy: Passing the ball hand-to-hand in a relay throw from boundary to keeper is like a channel handing off data between goroutines, while a single scorer's book that only one official may write in at a time is like a sync.Mutex protecting shared state.

What's the difference between an array and a slice?

An array has a fixed length that is part of its type ([5]int and [10]int are different types), and arrays are value types — assigning or passing one copies all its elements. A slice is a small struct (pointer, length, capacity) that describes a view into an underlying array; it's a reference type in the sense that copying a slice copies the header, not the backing data, so mutations through one slice can be visible through another that shares the same backing array. Slices can grow via append, arrays cannot.

🏏

Cricket analogy: A fixed 11-player squad sheet is locked to that size like a Go array, while a rotating XII-man bench, a slice of pointer, length, capacity, is a flexible view that can grow as reserves are added via append.

How does Go implement interfaces, and what does 'implicit satisfaction' mean?

A Go interface is satisfied implicitly: a type does not declare 'implements InterfaceX' anywhere. If a concrete type has methods matching every method in an interface's method set, it automatically satisfies that interface. This decouples interface definition from implementation — you can define a small interface in your own package that a type from a completely unrelated package already satisfies, without either package knowing about the other. Internally, an interface value is a two-word pair: a pointer to type information and a pointer to the underlying data.

🏏

Cricket analogy: A domestic all-rounder doesn't need to formally declare I am a Test player; selectors watch if their batting and bowling skills already match the required profile, just as a Go type implicitly satisfies an interface by matching its method set, no implements declaration needed.

Explain defer, panic, and recover.

defer schedules a function call to run right before the enclosing function returns, regardless of how it returns (normal return or panic); deferred calls run in LIFO order and are commonly used for cleanup like closing files or unlocking mutexes. panic stops normal execution of the current goroutine, runs any deferred calls, and propagates up the call stack until the program crashes or a deferred function calls recover. recover, only useful inside a deferred function, stops the panic and returns the value passed to panic, letting the program regain control — this is Go's controlled, opt-in analogue to catching an exception.

🏏

Cricket analogy: A team's close-the-innings ritual, covering the pitch and packing gear, happens no matter how the innings ends, like defer always running before a function returns; a batting collapse is like panic unwinding, and a calm review request overturning the umpire's call is like recover stopping the panic.

What is the 'nil interface' gotcha?

An interface value is nil only when *both* its type and value pointers are nil. If you assign a nil pointer of a concrete type to an interface variable, the interface itself is non-nil, because it now holds type information (the concrete type), even though the underlying value is nil. This trips people up when a function returns an error interface that was assigned a typed nil pointer — the caller's if err != nil check passes even though 'logically' there was no error.

🏏

Cricket analogy: A scorecard that lists a player's name but shows did not bat still counts as an entry on the sheet, similar to how an interface holding a nil pointer of a known type is non-nil, tripping up a simple was there an entry check.

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

func doWork() error {
    var e *MyErr = nil
    return e // interface value is NOT nil: it holds type *MyErr
}

func main() {
    err := doWork()
    fmt.Println(err == nil) // false, surprisingly
}

When should a method use a pointer receiver versus a value receiver?

Use a pointer receiver when the method needs to mutate the receiver, when the struct is large enough that copying it is wasteful, or when the type contains fields that shouldn't be copied (like a sync.Mutex). Use a value receiver for small, immutable-in-spirit types where copying is cheap and you want value semantics. A common rule of thumb: if any method on the type needs a pointer receiver, make all methods on that type use pointer receivers for consistency, since a type's method set otherwise becomes inconsistent between its value and pointer forms.

🏏

Cricket analogy: A team physio adjusts the actual player's injury, mutating in place, rather than a photocopy of their medical file, like a pointer receiver mutating the real struct instead of a value receiver's copy; once one method needs direct access, all methods should work the same way for consistency.

Why doesn't Go have exceptions, and how is error handling done instead?

Go treats errors as ordinary values, not a separate control-flow channel. Functions that can fail return an error as their last return value, and callers are expected to check it explicitly with if err != nil. This makes failure paths visible in the code rather than hidden in try/catch blocks that can be forgotten, and it keeps error handling local and composable via wrapping (fmt.Errorf with %w) and errors.Is / errors.As. panic/recover exists but is reserved for truly exceptional, programmer-error situations (like an out-of-bounds index), not for expected failure conditions like a missing file.

🏏

Cricket analogy: A scorer who must write down every dismissal explicitly on the sheet, rather than relying on a hidden signal only the umpire notices, mirrors Go's if err != nil check making failure visible in the code instead of hidden in a try/catch.

How does Go support code reuse without classical inheritance?

Go has no class hierarchy or 'extends' keyword. Instead, it supports struct embedding: you place one struct (or interface) as an anonymous field inside another, and the outer type automatically promotes the embedded type's fields and methods, so they can be accessed as if they belonged to the outer type. This is composition, not inheritance — there's no polymorphic substitutability implied, and the outer type can selectively override a promoted method simply by defining its own method of the same name. It favors 'has-a' composition over 'is-a' hierarchies.

🏏

Cricket analogy: A wicketkeeper-batsman doesn't inherit from a generic Batsman class; instead the keeping role is embedded alongside batting skills, and the player can override how they approach an innings, mirroring Go's struct embedding as has-a composition, not is-a inheritance.

How does Go's garbage collector work, at a high level?

Go uses a concurrent, tri-color mark-and-sweep garbage collector that runs alongside your program's goroutines rather than stopping the whole world for long pauses. It marks reachable objects starting from roots (globals, stacks), sweeps unreachable memory, and uses write barriers to stay correct while the program keeps mutating pointers during the mark phase. The design goal is very low pause times (sub-millisecond in typical workloads) at the cost of some CPU overhead and non-deterministic collection timing, which is why Go programs generally don't need manual memory management.

🏏

Cricket analogy: Ground staff mow and re-mark the pitch boundaries while a match is still being played in the outfield, rather than halting play entirely, similar to Go's GC marking and sweeping concurrently with running goroutines instead of stopping the world.

What happens if you forget to close a channel — is it required?

Closing a channel is not required for garbage collection — the garbage collector can reclaim a channel with no goroutines referencing it whether or not it was closed. You close a channel specifically to signal to receivers that no more values are coming, which is what allows a for range over a channel or a receive in a select to know when to stop. Sending on a closed channel panics, and receiving from a closed, drained channel returns the zero value immediately with the second 'ok' return value set to false, so close is a communication signal, not a cleanup mechanism.

🏏

Cricket analogy: Declaring an innings closed doesn't discard the scorebook, it just signals no more runs are coming, like closing a channel signals no more values while the runtime can still garbage-collect it either way; batting after declaration is like sending on a closed channel, which panics.

Quick Reference

  • Goroutines start with ~2KB stacks that grow dynamically; OS threads start much larger and fixed.
  • 'Share memory by communicating' is the Go concurrency proverb — prefer channels over shared state when coordinating goroutines.
  • Slices carry (pointer, length, capacity); appending beyond capacity allocates a new backing array.
  • Interfaces are satisfied implicitly — no 'implements' keyword exists in Go.
  • A typed nil value assigned to an interface produces a non-nil interface value — always compare concrete pointers, not interfaces, when checking for a typed nil.
  • defer runs LIFO, after the return value is set but before the function actually returns to its caller.
  • recover only has an effect when called directly inside a deferred function.
  • Pointer receivers are needed for mutation; keep receiver type consistent across a type's whole method set.
  • Go has no generics-free excuse anymore — generics landed in Go 1.18 (2022) via type parameters.
  • Struct embedding promotes fields/methods but does not give you virtual-dispatch polymorphism like inheritance.
  • The GC is concurrent mark-and-sweep, tuned for low pause times over raw throughput.
  • A nil map can be read from safely (returns zero value) but writing to it panics; a nil slice can be appended to safely.

Key Takeaways

  • Interviewers care about the 'why' behind Go's design choices, not syntax trivia.
  • The nil-interface gotcha and pointer-vs-value receivers are the two most common trick questions.
  • Explain error handling as explicit values, not a lesser version of exceptions.
  • Be ready to contrast channels (coordination) with mutexes (protecting shared state) with a concrete example.
  • Know that embedding is composition, not inheritance, and say so explicitly.

Practice what you learned

Was this page helpful?

Topics covered

#Go#GoProgrammingStudyNotes#Programming#CommonGoInterviewQuestions#Common#Interview#Questions#Frequently#StudyNotes#SkillVeris