Top 20 Go Interview Questions (With Answers & Code)
The most commonly asked Go interview questions, with detailed answers and runnable code examples. Covers goroutines, channels, interfaces, error handling, and more.
GoInterview PrepBackend Engineering
Go has become one of the most in-demand backend languages at companies like Google, Uber, Cloudflare, and Twitch. If you have a Go interview coming up, this guide covers the 20 questions you're most likely to face — with real code, not just definitions.
A goroutine is a lightweight thread managed by the Go runtime, not the OS. You start one with the go keyword. Goroutines are cheap — you can easily run hundreds of thousands concurrently.
time.Sleep(100 * time.Millisecond) // wait for goroutines
}
Key points:
Goroutines start with ~8KB stack (grows dynamically)
They are multiplexed onto OS threads by the Go scheduler (M:N model)
go is non-blocking — the calling goroutine continues immediately
2. What are channels?
Channels are typed conduits that let goroutines communicate safely. They enforce a "communicate by sharing memory" philosophy rather than "share memory to communicate."
package mainimport "fmt"func sum(nums []int, result chan int) { total := 0 for _, n := range nums { total += n } result <- total // send}func main() { nums := []int{1, 2, 3, 4, 5, 6} ch := make(chan int) go sum(nums[:3], ch) go sum(nums[3:], ch) a, b := <-ch, <-ch // receive both fmt.Println("Sum:", a+b) // 21}
Buffered vs unbuffered:
make(chan int) — unbuffered: send blocks until receiver is ready
make(chan int, 5) — buffered: send only blocks when buffer is full
3. What is a goroutine leak?
A goroutine leak happens when a goroutine is started but never terminates. This is one of the most common bugs in Go production code.
// BUG: This goroutine leaks if nothing reads from chfunc leak() { ch := make(chan int) go func() { val := <-ch // blocks forever — nobody sends fmt.Println(val) }()}
How to prevent leaks:
Always pair goroutines with a done signal (context cancellation, close(ch), or a dedicated quit channel)
Use goleak in tests to detect leaked goroutines
// Fixed: use context for cancellationfunc noLeak(ctx context.Context) { ch := make(chan int) go func() { select { case val := <-ch: fmt.Println(val) case <-ctx.Done(): return // clean exit } }()}
4. How does defer work?
defer schedules a function call to run when the surrounding function returns — even if it returns via panic. Multiple defers execute in LIFO (last in, first out) order.
Gotcha: Deferred functions capture variables by reference, not value.
func gotcha() { x := 1 defer fmt.Println(x) // prints 1, not 2 x = 2}// BUT:func gotcha2() { x := 1 defer func() { fmt.Println(x) }() // prints 2 (closure captures reference) x = 2}
5. What's the difference between new and make?
| | new(T) | make(T, ...) |
|---|---|---|
| Returns | *T (pointer to zero value) | T (initialized value) |
| Works on | Any type | Only slice, map, chan |
| Use case | Allocate a pointer to a struct | Initialize built-in reference types |
// new: allocates and returns a pointerp := new(int) // *int, *p == 0s := new([]int) // *[]int, but the slice is nil// make: initializes and returns the value directlysl := make([]int, 5) // []int with len=5, cap=5m := make(map[string]int) // empty, ready-to-use mapch := make(chan int, 10) // buffered channel
In practice, new is rarely needed. Struct literals (&MyStruct{}) are more idiomatic.
6. How does error handling work in Go?
Go returns errors as values — no exceptions. Functions that can fail return (result, error).
import ( "errors" "fmt")// Custom error typetype ValidationError struct { Field string Message string}func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message)}func validateAge(age int) error { if age < 0 { return &ValidationError{Field: "age", Message: "must be non-negative"} } if age > 150 { return errors.New("age is unrealistically high") } return nil}func main() { if err := validateAge(-1); err != nil { var ve *ValidationError if errors.As(err, &ve) { fmt.Println("Field:", ve.Field) // "age" } }}
Wrapping errors (Go 1.13+):
// Wrap with contextreturn fmt.Errorf("load config: %w", err)// Unwraperrors.Is(err, os.ErrNotExist) // checks wrapped chainerrors.As(err, &target) // extracts type from chain
7. What are interfaces?
An interface in Go is a set of method signatures. A type implicitly satisfies an interface by implementing all its methods — no implements keyword.
// BUG: all goroutines print the same valuefor i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // captures i by reference}// FIX: pass as argumentfor i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i)}
10. How do slices differ from arrays?
Arrays have fixed length and are value types. Slices are dynamic, reference a backing array, and are far more common.
Maps are hash tables. They must be initialized with make or a literal before use.
// Declare and initializefreq := make(map[string]int)words := []string{"go", "is", "great", "go", "is", "fast"}for _, w := range words { freq[w]++}// Check existence — never use the zero value to detect absenceif count, ok := freq["go"]; ok { fmt.Printf("'go' appears %d times\n", count)}// Deletedelete(freq, "is")// Iterate (order is random)for word, count := range freq { fmt.Printf("%s: %d\n", word, count)}
Important: Maps are not safe for concurrent use. Use sync.RWMutex or sync.Map for concurrent access.
12. What is the select statement?
select is like a switch for channels — it blocks until one of several channel operations can proceed.
If multiple cases are ready simultaneously, select picks one at random — this is by design to prevent starvation.
13. How does Go handle race conditions?
Go has a built-in race detector (go run -race, go test -race). Use sync.Mutex or channels to protect shared state.
type SafeCounter struct { mu sync.Mutex count int}func (c *SafeCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.count++}func (c *SafeCounter) Value() int { c.mu.RLock() // use RWMutex for read-heavy workloads defer c.mu.RUnlock() return c.count}
Always run go test -race in CI. It catches races that would be extremely hard to find in production.
14. What is sync.WaitGroup?
WaitGroup waits for a collection of goroutines to finish.
func processItems(items []string) { var wg sync.WaitGroup for _, item := range items { wg.Add(1) go func(s string) { defer wg.Done() fmt.Println("Processing:", s) }(item) } wg.Wait() // blocks until all goroutines call Done() fmt.Println("All done")}
Rule: Call wg.Add(1)before starting the goroutine, not inside it.
15. What is context used for?
The context package propagates cancellation, deadlines, and request-scoped values across API boundaries and goroutines.
Best practice: Always pass ctx as the first argument to functions that do I/O. Never store a context in a struct.
16. What are Go generics?
Go 1.18 added generics with type parameters. They let you write type-safe code that works across types without interface{}.
// Generic Map functionfunc Map[T, U any](slice []T, fn func(T) U) []U { result := make([]U, len(slice)) for i, v := range slice { result[i] = fn(v) } return result}func main() { nums := []int{1, 2, 3, 4} doubled := Map(nums, func(n int) int { return n * 2 }) // [2 4 6 8] strs := Map(nums, func(n int) string { return fmt.Sprintf("#%d", n) }) // ["#1" "#2" "#3" "#4"]}// Type constrainttype Number interface { int | int64 | float64}func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total}
17. What is panic and recover?
panic stops normal execution and unwinds the stack, running deferred functions. recover can catch a panic inside a deferred function.
func safeDivide(a, b int) (result int, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered from panic: %v", r) } }() return a / b, nil // panics if b == 0}func main() { result, err := safeDivide(10, 0) fmt.Println(result, err) // 0, "recovered from panic: runtime error: integer divide by zero"}
When to use: Only recover from panics at the top of call stacks (e.g., HTTP handlers) to prevent one bad request from crashing the whole server. For normal error conditions, return errors.
18. How does Go's garbage collector work?
Go uses a concurrent, tri-color mark-and-sweep GC that runs mostly alongside your program — pauses are typically under 1ms.
Key points for interviews:
GC is triggered when heap doubles since last collection
You can hint with runtime.GC() but rarely need to
GOGC env var controls aggressiveness (default 100 = collect when heap doubles)
GOMEMLIMIT (Go 1.19+) sets a soft memory cap
// Profile allocations: run go test -memprofile mem.out// then: go tool pprof mem.out
19. What is a method set?
A method set defines which methods can be called on a type. It determines interface satisfaction.
type Animal struct{ Name string }func (a Animal) Speak() string { return a.Name + " speaks" }func (a *Animal) Rename(n string) { a.Name = n }// Animal (value) method set: {Speak}// *Animal (pointer) method set: {Speak, Rename}type Speaker interface{ Speak() string }type Renamer interface{ Speak() string; Rename(string) }var s Speaker = Animal{Name: "Cat"} // works: Animal satisfies Speakervar r Renamer = &Animal{Name: "Dog"} // works: *Animal satisfies Renamer// var r2 Renamer = Animal{...} // compile error: Animal doesn't have Rename
20. How do you write unit tests in Go?
Go has first-class testing built in with the testing package — no frameworks needed.
// math.gopackage mathfunc Add(a, b int) int { return a + b }func Divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil}