Skip to main content
Go advanced Lesson 25 of 25

Go Interview Preparation

Top 35 Go interview questions and answers covering goroutines, channels, interfaces, memory, the scheduler, and common coding problems.

Fundamentals

Q1: What are the main differences between Go and other languages like Java or Python?

Go compiles to a native binary with no runtime dependency. It has goroutines (lightweight concurrency) built into the language, no class-based inheritance (composition via embedding and interfaces), errors as values instead of exceptions, and a garbage collector tuned for low latency. Unlike Python, Go is statically typed and compiled. Unlike Java, there is no JVM — binaries run directly on the OS.

Q2: What is the zero value in Go? Why does it matter?

Every type in Go has a zero value it takes when declared without initialization: 0 for integers, 0.0 for floats, false for bool, "" for string, nil for pointers, slices, maps, channels, and interfaces.

This eliminates uninitialized variable bugs. You can safely use a declared sync.Mutex or bytes.Buffer without calling any constructor — they work correctly from their zero value. The zero value design means Go types are always in a valid, usable state.

Q3: What is the difference between new and make?

new(T) allocates zeroed memory for type T and returns a *T. It works for any type.

make(T, args) only works for slices, maps, and channels. It allocates and initializes the internal data structure (the slice header, the hash table, the channel buffer). It returns the type itself, not a pointer. Without make, a map or channel would be nil and unusable.

p := new(int)       // *int pointing to 0
s := make([]int, 5) // []int with len=5, cap=5, initialized
m := make(map[string]int) // initialized hash map — nil map panics on write

Q4: What is the difference between a slice and an array?

An array has a fixed length that is part of its type: [5]int and [6]int are different, incompatible types. Arrays are value types — assignment copies all elements.

A slice is a three-word descriptor (pointer, len, cap) that refers to a section of an underlying array. Slices are reference types — assignment copies the descriptor, not the data. Slices are what you use in practice; arrays are rare outside of low-level code.

Q5: Explain the internal structure of a slice.

A slice header has three fields:

  • ptr: pointer to the first element in the underlying array
  • len: number of accessible elements
  • cap: number of elements from ptr to the end of the underlying array

append adds elements. When len == cap, append allocates a new larger array, copies data, and returns a new slice header pointing to the new array. This is why you must always use the return value of append — the original slice variable may point to old memory.


Interfaces and Types

Q6: How does Go implement interfaces?

Implicitly. A type satisfies an interface by implementing all the methods in the interface — no implements keyword is needed. An interface value is a two-word pair: (type pointer, data pointer). This enables duck typing with compile-time safety. If you add a method to an interface, every type that no longer satisfies it produces a compile error — not a runtime surprise.

Q7: What is the empty interface and when should you use it?

interface{} (or any since Go 1.18) has no methods, so every type satisfies it. Use it when you genuinely need to accept any type — for example, fmt.Println, JSON marshaling, or generic containers before Go 1.18. Avoid it when a concrete type or constrained generic would work — any loses type safety and typically requires a type assertion to get the value back.

Q8: What is a type assertion? What happens if it fails?

A type assertion extracts the concrete value from an interface. The single-value form panics if the type is wrong; the two-value form is safe and sets ok to false instead of panicking.

var i interface{} = "hello"
s := i.(string)     // panics if i is not a string
s, ok := i.(string) // safe — ok=false if wrong type, no panic

Always use the two-value form unless you are certain of the type.

Q9: What is the difference between a pointer receiver and a value receiver?

A value receiver gets a copy — it cannot modify the original and is safe to call on nil values if the method doesn’t dereference the receiver. A pointer receiver gets the address — it can modify the original and avoids copying large structs.

Rule: if any method uses a pointer receiver, make all methods use pointer receivers for consistency. The method set of *T includes both T and *T methods; the method set of T only includes T methods — this matters for interface satisfaction.

Q10: Explain the nil interface gotcha.

var p *MyError = nil
var err error = p
fmt.Println(err == nil) // false!

An interface value is nil only if both its type and value are nil. Here, err has type *MyError set — it’s not nil even though the underlying pointer is nil. This is a classic source of bugs. Always return error (not a concrete error type) from functions to avoid this.


Goroutines and Concurrency

Q11: What is a goroutine? How is it different from a thread?

A goroutine is a lightweight coroutine managed by the Go runtime scheduler. It starts with a 2–8 KB stack that grows dynamically as needed. You can run hundreds of thousands of goroutines where OS threads would exhaust memory — each OS thread takes 1–8 MB of stack. The Go scheduler multiplexes goroutines onto OS threads (M:N scheduling) and handles blocking I/O by moving the OS thread to another goroutine rather than blocking it.

Q12: What is the Go scheduler’s M:N model?

  • M = OS threads (managed by the OS kernel)
  • G = goroutines (managed by the Go runtime)
  • P = logical processors (GOMAXPROCS, defaults to CPU count)

Each P has a local run queue of goroutines. When a goroutine blocks on I/O or a system call, the P detaches from the blocked OS thread and attaches to another one, keeping all Ps busy. Work-stealing: idle Ps steal goroutines from the run queues of busy Ps.

Q13: What is a goroutine leak? How do you prevent it?

A goroutine that blocks forever without ever returning is a leak — it consumes memory and scheduler resources indefinitely. Common causes: sending to an unbuffered channel with no receiver, receiving from a channel that is never closed, and waiting on a lock that is never released.

Prevention: use context.Context for cancellation so goroutines have an exit path, always close channels from the sender side, use select with a ctx.Done() case, and run the race detector and goroutine profiler to detect leaks.

Q14: What is the difference between buffered and unbuffered channels?

An unbuffered channel (make(chan T)) synchronizes sender and receiver — both must be ready simultaneously, making the send and receive happen at the same instant. A buffered channel (make(chan T, N)) allows up to N sends without a receiver. The sender blocks only when the buffer is full. Buffered channels decouple producer and consumer speed; unbuffered channels guarantee synchronization.

Q15: What does select do in Go?

select waits on multiple channel operations and executes the first one that is ready. If multiple are ready simultaneously, one is chosen at random — this prevents starvation. A default case makes select non-blocking: it runs immediately if no channel is ready. select is the idiomatic way to implement timeouts, cancellation, and multiplexing.

Q16: How do you safely share data between goroutines?

  1. Communicate via channels — pass ownership of data rather than sharing it
  2. Protect with sync.Mutex or sync.RWMutex for shared state
  3. Use sync/atomic for single-variable counter operations
  4. Use sync.Map for concurrent map access with frequent reads

The Go proverb: “Do not communicate by sharing memory; share memory by communicating.”

Q17: When would you use sync.WaitGroup?

When you launch N goroutines and need to wait for all of them to finish before proceeding. Call Add(1) before launching each goroutine, Done() (via defer) when it finishes, and Wait() to block the calling goroutine until the count reaches zero. Never call Add inside the goroutine — there is a race between the Add and the Wait.

Q18: What is a race condition? How do you detect one?

A race condition occurs when two goroutines access the same variable concurrently and at least one access is a write, without synchronization. The result is undefined behaviour — corrupted data, wrong values, crashes. Run with go test -race or go run -race — Go’s built-in race detector instruments every memory access at runtime and reports conflicts with full stack traces.


Error Handling

Q19: Why does Go use errors as values instead of exceptions?

Errors are normal return values — they appear in function signatures and force callers to handle them explicitly. This makes error paths visible and local: you see exactly which calls can fail and handle each at the call site. Exceptions create invisible control flow jumps that can bypass cleanup code and make it hard to reason about what can fail. Go’s philosophy: errors that can happen are not exceptional — handle them explicitly.

Q20: What is the difference between errors.Is and errors.As?

errors.Is checks if any error in the wrapped chain matches a target value (sentinel error). errors.As checks if any error in the chain matches a target type and extracts it into a variable so you can access its fields.

errors.Is(err, ErrNotFound)     // true if ErrNotFound appears anywhere in the chain
errors.As(err, &myTypedErr)     // true if *MyError appears in the chain; fills myTypedErr

Q21: What does fmt.Errorf("%w", err) do?

It wraps err in a new error that includes the original as a cause. The %w verb (not %v) preserves the original error inside the chain so errors.Is and errors.As can traverse it. %v embeds the error string only — the original error is lost and cannot be matched. Use %w whenever callers might need to inspect the underlying error.


Memory and Performance

Q22: What is escape analysis?

The compiler determines whether a variable’s lifetime is bounded by the function (allocate on stack — fast, no GC) or whether it escapes to be referenced after the function returns (allocate on heap — GC managed). Returning a pointer, storing in an interface, or capturing in a goroutine closure causes escape. Stack allocations are orders of magnitude cheaper — understanding escape analysis lets you write high-performance code.

go build -gcflags='-m' ./...  # shows each escape decision with the reason

Q23: Describe Go’s garbage collector.

Go uses a concurrent, tri-color mark-and-sweep GC. It runs mostly concurrently with the application — only two very brief stop-the-world pauses per cycle, typically under 1ms. GOGC (default 100) controls when GC runs: GC triggers when the heap size doubles since the last collection. GOMEMLIMIT (Go 1.21+) sets a soft memory ceiling that causes the GC to run more aggressively before the limit is reached.

Q24: What is sync.Pool?

A pool of temporary objects that can be reused across goroutines to reduce GC pressure. The pool holds objects that are no longer needed; the next caller gets one instead of allocating fresh. Objects may be collected at any GC cycle — Pool is a performance hint, not a reliable cache. Used for byte buffers, encoder/decoder instances, and other frequently allocated short-lived objects.


Code Problems

Q25: Fix the race condition:

// Buggy — concurrent writes to count without synchronization
var count int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        count++ // RACE: read-modify-write is not atomic
    }()
}
wg.Wait()
// Fix 1: atomic operation — fastest, works for single-variable counters
var count int64
atomic.AddInt64(&count, 1)

// Fix 2: mutex — use when protecting a block of code or compound operation
var mu sync.Mutex
mu.Lock()
count++
mu.Unlock()

Q26: What does this code print?

for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }()
}
time.Sleep(time.Second)

Likely prints 3 3 3 — all goroutines capture the same variable i by reference. By the time they run, the loop has finished and i is 3. The fix: pass i as an argument so each goroutine receives its own copy of the value at launch time.

for i := 0; i < 3; i++ {
    go func(n int) { fmt.Println(n) }(i) // n is a copy — independent of the loop variable
}

Q27: Implement a concurrent-safe LRU cache sketch.

// LRU[K, V] is a generic concurrent-safe LRU cache
type LRU[K comparable, V any] struct {
    mu       sync.Mutex
    capacity int
    items    map[K]*list.Element // O(1) lookup
    order    *list.List          // doubly-linked list — tracks recency
}

type entry[K comparable, V any] struct{ key K; val V }

func (l *LRU[K, V]) Get(key K) (V, bool) {
    l.mu.Lock()
    defer l.mu.Unlock()
    if el, ok := l.items[key]; ok {
        l.order.MoveToFront(el) // mark as recently used
        return el.Value.(*entry[K, V]).val, true
    }
    var zero V
    return zero, false
}

Q28: Write a function that merges two sorted slices.

// mergeSorted merges two sorted int slices into one sorted slice in O(n+m) time
func mergeSorted(a, b []int) []int {
    result := make([]int, 0, len(a)+len(b)) // preallocate exact final size
    i, j := 0, 0
    for i < len(a) && j < len(b) {
        if a[i] <= b[j] {
            result = append(result, a[i]); i++
        } else {
            result = append(result, b[j]); j++
        }
    }
    // Append remaining elements from whichever slice isn't exhausted
    result = append(result, a[i:]...)
    result = append(result, b[j:]...)
    return result
}

Advanced Topics

Q29: What is a closure and how are closures used in Go?

A closure is a function that captures variables from its enclosing scope — the function can read and write those variables even after the outer function has returned. Closures are used extensively in Go for callbacks, middleware (wrapping handlers), memoization, iterators, and the functional options pattern. The important thing to remember: closures capture variables by reference, not by value — passing loop variables into goroutines as arguments avoids the classic capture bug.

Q30: What is the init() function?

init() runs automatically before main(), after all package-level variable declarations are evaluated. A package can have multiple init() functions across multiple files; they run in the order the files are passed to the compiler. They cannot be called explicitly. Used for one-time setup: registering database drivers, validating configuration, and initializing global state that cannot be expressed as a simple variable initializer.

Q31: What are Go modules and what problem do they solve?

Modules are the unit of dependency management. A go.mod file at the root defines the module path and records minimum required versions of all dependencies. Modules replaced the old GOPATH-based approach, enabling projects to exist anywhere on the filesystem and providing reproducible builds with version pinning. go.sum records cryptographic hashes of all dependencies to prevent supply chain tampering.

Q32: What is the context package used for?

context.Context carries three things through a call chain: a cancellation signal, a deadline, and a key-value store for request-scoped data (like a user ID). Pass ctx as the first argument to every function that does I/O. When a parent context is cancelled — because a request timed out or a client disconnected — all derived contexts are cancelled automatically, stopping all in-flight work cleanly without goroutine leaks.

Q33: What are the differences between defer, panic, and recover?

  • defer schedules a function call for when the surrounding function returns — used for cleanup (closing files, unlocking mutexes)
  • panic immediately stops the function, runs all deferred functions in LIFO order, then crashes the program if nothing recovers it
  • recover inside a defer catches a panic in progress and stops the crash — primarily used in library code and HTTP servers to prevent one bad request from crashing the process

Use defer constantly. Use panic only for unrecoverable invariant violations. Use recover sparingly.

Q34: What is the comparable constraint in generics?

comparable is a built-in interface constraint that allows only types supporting == and !=. It is required when a generic function needs to use a type as a map key or compare values with ==. Not all types are comparable — slices, maps, and functions cannot be compared with == and do not satisfy comparable. Structs are comparable only if all their fields are comparable.

Q35: How would you structure a large Go service?

service/
├── cmd/server/main.go      — wiring only: build deps, start server
├── internal/
│   ├── domain/             — pure business types (User, Order, errors)
│   ├── repository/         — data access interfaces + SQL implementations
│   ├── service/            — business logic (orchestration, validation)
│   └── handler/            — HTTP handlers (thin: validate, call service, respond)
├── pkg/                    — reusable packages (logger, config, validator)
└── migrations/             — SQL migration files

Dependencies flow inward: handler → service → repository → database. Each layer depends only on interfaces, not concrete types — making every layer testable in isolation. main.go wires the real implementations together. This structure scales from a few hundred lines to hundreds of thousands without becoming unmaintainable.

Frequently Asked Questions

What are the most important Go topics for interviews?
Goroutines and channels (how they work, common patterns), interfaces (implicit implementation, empty interface, type assertions), error handling (wrapping, errors.Is/As), memory (stack vs heap, GC), and the differences between slices and arrays.
Do Go interviews include coding challenges?
Yes. Common tasks include implementing concurrent patterns (worker pools, rate limiters), writing goroutine-safe data structures, and solving algorithm problems. Interviewers often ask candidates to find race conditions or goroutine leaks in code snippets.
Is knowledge of the Go scheduler important for interviews?
For senior roles, yes. Understanding the M:N threading model (M goroutines on N OS threads), GOMAXPROCS, and when goroutines block is expected at senior/staff level.