← All languages
Go interview questions
40 curated Go concept questions with model answers. Practice them out loud in the scored Fundamentals room.
Go interviews are about concurrency and simplicity of design. Interviewers ask about goroutines, channels, the implicit interface model, and error handling because those are the idioms that make Go code idiomatic and easy to operate.
- Goroutines, channels, and select: coordinate workers, avoid leaks, and know when a mutex is simpler than a channel.
- Interfaces are satisfied implicitly: design small interfaces, and understand how the empty interface and type assertions work.
- Slices vs arrays and defer/panic/recover: the slice header (pointer, length, capacity), append behaviour, and the intended use of recover.
What interviewers are really testing
Go interviewers look for idiomatic simplicity: goroutines coordinated through channels, small interfaces satisfied implicitly, and explicit error handling. They want to see that you can design concurrent code that does not leak goroutines or dead-lock, and that you work with the language's opinions instead of fighting them.
Basic
What's the difference between an array and a slice?
- An array has a fixed length that is part of its type ([3]int); it is copied by value on assignment.
- A slice is a descriptor (pointer, length, capacity) over a backing array, so it can grow and is cheap to pass.
- append may allocate a new backing array when capacity is exceeded, so the returned slice must be reassigned.
- Two slices can share a backing array, so a write in one is visible in the other.
- Use arrays for fixed-size data, slices almost everywhere else.
What are the key properties and gotchas of Go maps?
- Maps are unordered key→value hashes; reads of a missing key return the zero value, never an error.
- Use the two-value form `v, ok := m[k]` to distinguish zero from absent.
- Writing to a nil map panics; declare with make or a literal before writing.
- Map iteration order is randomized by design.
- Maps are reference types; passing one shares the same underlying data, and they are not safe for concurrent writes.
How do interfaces work in Go and how is that different from Java?
- Interfaces are satisfied implicitly: a type implements an interface by having the required methods, with no `implements` declaration.
- This decouples packages — the consumer can define the interface it needs.
- The empty interface `interface{}` (or `any`) is satisfied by every type.
- Small interfaces are idiomatic (io.Reader has one method).
- A type assertion or type switch recovers the concrete type at runtime.
How does embedding differ from inheritance?
- Embedding a type as an anonymous field promotes its methods and fields to the outer struct — composition, not inheritance.
- There is no vtable-style override; an outer method with the same name shadows the embedded one.
- Calling the embedded method explicitly is possible via the type name.
- Interfaces can embed other interfaces to compose method sets.
- Go deliberately favors composition over class hierarchies.
How does error handling in Go work?
- error is an interface with an Error() string; functions return it as the last value, and callers check `if err != nil`.
- Errors are values: wrap them with fmt.Errorf("%w", err) and inspect with errors.Is/errors.As.
- Sentinel errors (io.EOF) and custom error types are both idiomatic.
- panic/recover is for truly exceptional, unrecoverable bugs, not ordinary control flow.
- The verbosity is intentional: it makes every failure path explicit.
What does defer do and what are its rules?
- defer schedules a call to run when the surrounding function returns, LIFO order.
- It is the idiom for cleanup (close files, unlock mutexes) next to the acquisition.
- Arguments to a deferred call are evaluated when defer executes, not when it runs.
- A deferred closure can capture and modify named return values (used to alter the returned error).
- defer runs even on panic as the stack unwinds.
How do pointers work in Go?
- & takes an address; * dereferences. Pointers have zero value nil.
- Go has no pointer arithmetic, which makes it memory-safe compared with C.
- Passing a pointer lets a function mutate the caller's value and avoids copying large structs.
- A nil pointer dereference panics; check before use.
- new(T) allocates a zeroed T and returns a pointer.
What's the difference between `var` and `:=`?
- `var x T` declares with an explicit type and zero value, usable at package or function scope.
- `x := v` is short declaration, only inside functions, inferring the type from the right-hand side.
- `:=` requires at least one new variable on the left; the rest may be reassigned.
- Package-level declarations must use var/const/type/func, not `:=`.
- Idiomatic Go uses `:=` for locals and explicit types when clarity or a specific type is needed.
What is a goroutine?
- A goroutine is a lightweight thread of execution managed by the Go runtime, started with `go f()`.
- They start with a small stack that grows as needed, so tens of thousands are practical.
- The runtime multiplexes goroutines onto OS threads (M:N scheduling).
- Communication should use channels; `go` alone gives concurrency, not parallelism.
- The main function exiting terminates the program without waiting for goroutines.
When do you use a value receiver vs a pointer receiver?
- A value receiver gets a copy and cannot mutate the original; a pointer receiver can mutate the receiver.
- Use pointer receivers when the method modifies state, when the struct is large, or to keep the method set consistent.
- The method set rule: a value has the methods with value receivers; a pointer has both. This affects interface satisfaction.
- Be consistent within a type — mixing receivers makes the API confusing.
- A nil pointer receiver is valid if the method checks for nil.
What is a raw string literal?
- Backticks create a raw string: no escape sequences and newlines are literal.
- Useful for regex, JSON, SQL, and multi-line text.
- The only thing you can't include is a backtick itself.
- Regular double-quoted strings process escapes like \n and \t.
- Runes in double quotes are Unicode code points; raw strings are UTF-8 bytes.
What is iota used for?
- iota resets to 0 in each const block and increments per ConstSpec.
- It generates sequential constants without repeating values.
- Expressions can use it: 1 << iota for bit flags.
- Skipping with _ and restarting via a new const block are common idioms.
- Combined with a String() method it makes readable enums.
What are struct tags?
- Tags are string metadata on struct fields, conventionally key:"value".
- encoding/json, xml, and ORMs read them to control field names and behavior.
- They are read via reflection (reflect.StructTag.Get).
- Common keys: json, xml, yaml, db, validate.
- json:"-" omits a field; omitempty skips zero values.
How does Go's switch differ from C/Java?
- Cases don't fall through by default; use `fallthrough` explicitly (it does not test the next case).
- A switch without an expression switches on true, enabling clean if/else chains.
- Multiple values per case (case 1, 2, 3) and no braces are required.
- break exits the switch; continue is not valid inside a case.
- Type switches (switch v := x.(type)) dispatch on dynamic type.
What are the gotchas of append?
- append returns a new slice header; always assign the result.
- If capacity is enough it writes in place, so aliased slices can be surprised.
- When it grows, a new backing array is allocated and the old aliases are unaffected.
- Appending to a nil slice is valid and allocates.
- Preallocate with make([]T, 0, n) to avoid repeated copies.
What are init functions and when do they run?
- init() runs after package-level variables are initialized and before main().
- A package can have multiple init functions across files; order within a file is source order.
- Imported packages initialize before the importing package.
- Use init for registering drivers or setting defaults, not for heavy work or I/O.
- init cannot be called explicitly and takes no arguments.
What makes a program runnable?
- package main plus func main() is the entry point.
- A directory is a package; all files in it share the package name.
- Exported identifiers start with a capital letter; unexported start lowercase.
- go run builds and runs; go build produces a binary.
- There is no main for library packages.
What's the difference between type conversion and type assertion?
- Conversion T(x) works between compatible concrete types (int to float64, []byte to string).
- A type assertion x.(T) extracts a concrete type from an interface and panics if wrong.
- The two-value form v, ok := x.(T) reports success without panicking.
- A type switch handles several possible dynamic types.
- Interfaces convert to other interfaces implicitly when the method set is a superset.
What are typed and untyped constants?
- Untyped constants are arbitrary-precision and take a type from context.
- Typed constants (const x int = 1) are fixed to that type.
- Constants are compile-time and cannot be a function call result (except built-ins like len of a constant).
- Untyped constants enable large intermediate math without overflow, e.g. 1 << 40.
- iota produces untyped constants by default.
How does copy work?
- copy(dst, src) copies min(len(dst), len(src)) elements and returns the count.
- It copies values, so a [][]byte copies slice headers, not the inner data.
- Use it to detach a sub-slice from its backing array.
- append([]T(nil), s...) is a common full-copy idiom.
- copy on overlapping slices behaves as if the source were copied first.
Advanced
Explain buffered vs unbuffered channels.
- An unbuffered channel synchronizes sender and receiver — the send blocks until a receive is ready.
- A buffered channel holds up to its capacity without blocking the sender; sends block only when full, receives when empty.
- Closing a channel signals no more values; receiving from a closed channel returns the zero value and ok=false.
- Sending on a closed channel panics; only the sender should close.
- Directional channel types (<-chan / chan<-) express intent in signatures.
What is select used for?
- select waits on multiple channel operations and runs the first case that is ready.
- If several are ready it chooses pseudo-randomly, which prevents starvation.
- A `default` case makes it non-blocking — a common pattern for polling or timeouts.
- A `time.After` case implements a timeout; a done channel implements cancellation.
- select is the control structure that ties goroutines and channels together.
What is context.Context used for?
- It carries cancellation, deadlines, and request-scoped values across API boundaries and goroutines.
- context.WithCancel/WithTimeout/WithDeadline derive a cancellable context; Done() returns a channel closed on cancellation.
- Pass ctx as the first parameter and honor it in blocking operations; never store it in a struct.
- It is the standard way to stop work when a request is aborted or a deadline passes.
- Values should be request metadata (trace IDs), not optional function parameters.
How does the Go scheduler work (G, M, P)?
- G = goroutine, M = OS thread, P = logical processor holding a run queue; GOMAXPROCS sets the number of Ps.
- The scheduler multiplexes many Gs onto a limited set of Ms, giving cheap concurrency.
- Work stealing balances runnable goroutines across Ps; syscalls can hand off a P to another M.
- This is cooperative + preemptive since Go 1.14 (async preemption), so a tight loop no longer blocks scheduling forever.
- Blocking syscalls and cgo have different behavior — cgo calls block an OS thread.
When should you use panic and recover?
- panic aborts normal control flow and unwinds the stack running deferred functions; recover stops it inside a deferred call.
- Use panic for programmer errors and truly unrecoverable states (failed invariant, nil map write).
- Do not use panic for expected errors — return them instead.
- recover is useful at goroutine boundaries or in servers to convert a panic into a 500 instead of crashing.
- A recover in a defer only works within the same goroutine.
Why can an interface holding a nil pointer be non-nil?
- An interface value is a pair (type, value); it is nil only when both are nil.
- Returning a typed nil pointer as an error interface produces a non-nil interface whose value is nil.
- So `var p *MyError = nil; var err error = p; err != nil` is true.
- Fix: return the untyped nil explicitly, or check the concrete pointer before wrapping.
- This is a classic Go bug source in error-returning constructors.
Compare channels and sync primitives (Mutex, WaitGroup, Once).
- Channels model communication and ownership transfer between goroutines.
- sync.Mutex protects shared state with explicit lock/unlock; a common rule is to keep the critical section small.
- sync.RWMutex allows concurrent readers; sync.WaitGroup waits for a set of goroutines to finish.
- sync.Once guarantees one-time initialization; sync/atomic provides lock-free counters.
- Guideline: use channels to pass data, mutexes to guard state.
How does a slice grow and how do you avoid surprises?
- A slice is a {ptr, len, cap} header; append writes into spare capacity or allocates a bigger array and copies.
- Growth roughly doubles for small slices, then grows more slowly for large ones.
- Because growth allocates, slices sharing a backing array can diverge after one appends.
- Preallocate with make([]T, 0, n) when the final size is known to avoid repeated copies.
- Slicing does not copy; use the built-in copy or append([]T(nil), s...) to detach.
What is escape analysis?
- The compiler decides whether a value can live on the stack or must escape to the heap.
- Values whose addresses outlive the function, are stored in interfaces, or are too large escape to the heap.
- Heap allocation adds GC pressure, so avoiding unnecessary escapes is a common optimization.
- `go build -gcflags=-m` shows escape decisions.
- Returning a pointer to a local is safe — the compiler moves it to the heap rather than dangling.
How do generics work in Go?
- Type parameters let functions and types be generic: `func Map[T any, U any](s []T, f func(T) U) []U`.
- Constraints (interfaces, including unions like `~int | ~string`) restrict allowed type arguments.
- `any` is an alias for interface{}; `comparable` allows == and map keys.
- Generics create a single definition instantiated by the compiler (GC shape stenciling), so there is some runtime shape sharing.
- They reduce reflection/interface{} code but should not replace interfaces for behavior.
How do you build a worker pool?
- Start a fixed number of workers reading from a shared jobs channel and writing to a results channel.
- Close the results channel with a WaitGroup after workers finish, so the consumer's range terminates.
- Bound concurrency to protect downstream resources (DB, APIs).
- Always provide a cancellation path (context) so workers stop on shutdown.
- Buffered channels and semaphores are alternatives to a fixed pool.
What are fan-out and fan-in?
- Fan-out distributes work to multiple goroutines reading one channel.
- Fan-in merges multiple channels into one with a goroutine per input.
- They compose into pipelines where each stage is a set of goroutines.
- Closing semantics matter: close each producer, and the merger closes after all inputs close.
- Use a WaitGroup to know when all inputs are done.
How do you rate-limit with time.Ticker?
- A ticker channel emits at a fixed interval; receiving gates each operation.
- time.Ticker must be stopped to release resources.
- time.Tick does not expose Stop, so prefer NewTicker in long-lived code.
- For bursts use golang.org/x/time/rate (token bucket) instead of a plain ticker.
- Combine with a context to stop on cancellation.
How do you cancel a goroutine pipeline?
- Pass a context and check ctx.Done() with select in every stage.
- On cancellation, stop producing and drain or close channels so upstream doesn't block.
- A producer that respects ctx prevents goroutine leaks.
- Use errgroup.WithContext to cancel siblings when one fails.
- context is the standard cancellation signal across API boundaries.
How do you profile a Go program?
- net/http/pprof exposes /debug/pprof for live CPU, heap, goroutine, block, and mutex profiles.
- go tool pprof analyzes the profiles (top, list, web).
- runtime/pprof writes profiles from programs without an HTTP server.
- go test -bench -cpuprofile/-memprofile profiles benchmarks.
- Find allocations with -alloc_space and escape analysis with -gcflags=-m.
What does //go:embed do?
- It embeds files or directories into the binary at build time.
- Embed into string, []byte, or embed.FS.
- The directive must be above a package-level variable and can't reach outside the package directory.
- Useful for templates, static assets, and default configs in a single binary.
- The embedded files are read-only and not watched at runtime.
What can reflection do in Go and why avoid it?
- reflect.TypeOf/ValueOf inspect dynamic type and value; you can read fields, tags, and call methods.
- encoding/json, ORMs, and validators rely on it.
- It is slower, bypasses compile-time checks, and panics on misuse.
- Values from unexported fields are not settable, and Interface() on a nil can panic.
- Prefer interfaces, generics, or code generation when possible.
What is the unsafe package for?
- It bypasses Go's type safety for pointer arithmetic and reinterpretation (e.g. []byte <-> string without copying).
- unsafe.Pointer conversions have strict rules; uintptr arithmetic is not a real pointer and can be moved by GC.
- Required for some zero-copy interop and runtime internals.
- Code using unsafe is not guaranteed across Go versions.
- Prefer safe alternatives; use unsafe only in measured hot paths.
What is singleflight used for?
- golang.org/x/sync/singleflight deduplicates concurrent identical calls so only one executes.
- The others wait and share the result (cache stampede protection).
- It returns the result plus a `shared` flag indicating whether it was deduplicated.
- Errors are shared too, so a failed call fails all waiters.
- A common pattern in front of expensive DB/API reads.
How does error wrapping work?
- fmt.Errorf("%w", err) wraps an error so errors.Is/errors.As can unwrap it.
- errors.Is compares by identity through the chain; errors.As extracts a typed error.
- Multi-wrapping with %w (Go 1.20+) supports several wrapped errors.
- errors.Unwrap exposes the next error in the chain.
- Wrap with context (operation, ID) but keep the original for matching.
Common mistakes
- Launching goroutines without a way to stop them, leaking them for the life of the process.
- Sending on a channel nobody reads, or reading a nil channel, and dead-locking the program.
- Treating a slice as if append always mutates the original backing array.
- Ignoring errors, or shadowing the err variable inside an if or for scope.
How to rehearse these answers
- Write a worker pool with context cancellation and prove no goroutine is left running.
- Explain the slice header and show one case where append reallocates and one where it does not.
- Replace a sync.Mutex-protected counter with a channel-based design, then say when the mutex was simpler.
- Implement a custom error with errors.Is and errors.As and unwrap it through a call chain.
New to technical interviews? Read how to rehearse coding and system design.
Other languages
JavaScript interview questions · Python interview questions · Java interview questions · C++ interview questions · C# interview questions