The questions that come up most often, each with a sample answer you can adapt into your own words. Read them out loud until the explanation feels natural.
What is a goroutine and how does it differ from an OS thread?
IntermediateA goroutine is a lightweight, runtime-managed concurrent function started with the go keyword. It begins with a tiny stack (a few KB) that grows as needed, so you can run hundreds of thousands of them, whereas OS threads cost megabytes each. The Go scheduler multiplexes many goroutines onto a small pool of OS threads, so goroutines are far cheaper to create and switch.
What is the difference between a buffered and an unbuffered channel?
IntermediateAn unbuffered channel is synchronous: a send blocks until a receiver is ready, so it acts as a rendezvous point that synchronises two goroutines. A buffered channel holds up to its capacity without a receiver; sends block only when the buffer is full and receives block only when it is empty. Use unbuffered for handoff/synchronisation and buffered to smooth bursty producers.
How does Go handle errors, and why no exceptions?
FundamentalsFunctions return errors as ordinary values, conventionally the last return value, and callers check with if err != nil. This makes the error path explicit and local rather than hidden in a stack of throws. Errors can be wrapped with fmt.Errorf and %w to add context while preserving the chain, then inspected with errors.Is and errors.As. panic/recover exists but is reserved for truly unrecoverable situations.
How do interfaces work in Go?
IntermediateInterfaces are satisfied implicitly: any type that has the required method set implements the interface without declaring so. This decouples packages, since a consumer can define the small interface it needs and any provider satisfies it. The empty interface (interface{} or any) accepts any value, used when you genuinely need to hold an unknown type, typically narrowed with a type assertion or switch.
What is a data race and how do you detect one?
AdvancedA data race occurs when two goroutines access the same variable concurrently and at least one writes, without synchronisation, producing undefined behaviour. You prevent it by communicating over channels or guarding the variable with a sync.Mutex. Go ships a race detector: running tests or the program with the -race flag instruments memory access and reports races at runtime, which is the standard way to find them.
What does the select statement do?
Advancedselect waits on multiple channel operations and proceeds with whichever is ready, choosing randomly if several are. It is how you coordinate several channels, implement timeouts (with time.After), and add cancellation (with a context's Done channel). A default case makes the select non-blocking. It is the channel equivalent of a switch.
What is the difference between an array and a slice?
IntermediateAn array has a fixed length that is part of its type ([3]int and [4]int are different types) and is copied by value. A slice is a lightweight view (pointer, length, capacity) over a backing array and is what you use in practice. The gotcha is that slices sharing a backing array can alias each other, and append may or may not allocate a new array depending on capacity.
How do you avoid a goroutine leak?
AdvancedA goroutine leaks when it blocks forever on a channel that never receives or sends, so it is never garbage collected. You avoid it by ensuring every goroutine has a guaranteed exit path: pass a context.Context and select on ctx.Done(), close channels when done, and use sync.WaitGroup to know when workers finish. The rule is that whoever starts a goroutine is responsible for its termination.
What does defer do and when do deferred calls run?
Intermediatedefer schedules a function call to run when the surrounding function returns, whether normally or via a panic. Deferred calls run last-in, first-out, and their arguments are evaluated at the defer statement, not at execution time, which is a classic quiz point. The idiomatic use is pairing acquisition with cleanup on adjacent lines: open a file then defer its Close, lock a mutex then defer Unlock. That keeps the cleanup visible next to the resource and correct on every return path.
func readConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
When do you use a pointer receiver versus a value receiver?
IntermediateA value receiver gets a copy, so mutations do not stick; a pointer receiver operates on the original and can modify it. Use a pointer receiver when the method mutates state or the struct is large enough that copying is wasteful, and a value receiver for small, immutable types. Consistency matters: if any method on a type needs a pointer receiver, give them all pointer receivers, because the method set of a value excludes pointer-receiver methods, which affects interface satisfaction in ways that confuse newcomers.
type Counter struct {
n int
}
func (c *Counter) Inc() {
c.n++
}
func (c Counter) Value() int {
return c.n
}
What is context.Context for?
Advancedcontext.Context carries deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines. A server derives a context per request; when the client disconnects or a timeout fires, ctx.Done() closes and every function down the call chain that respects it stops work promptly. The conventions are strict: context goes first in the parameter list, is never stored in a struct, and the value store is for request-scoped metadata like trace ids, not for passing ordinary parameters. It is the backbone of well-behaved Go services.
How do you check whether a key exists in a map?
FundamentalsUse the comma-ok idiom: value, ok := m[key]. Indexing a map with a missing key does not panic; it returns the zero value of the value type, so you cannot distinguish absent from present-but-zero without the second return. That distinction matters whenever zero is meaningful, like a count of 0 or an empty string. The related trap is that reading from a nil map is fine but writing to one panics, so maps must be initialised with make or a literal before use.