← Back to DevBytes

Top 50 Go Interview Questions for Mid-Level Developers

Top 50 Go Interview Questions for Mid-Level Developers

Go (Golang) has become one of the most popular programming languages for building scalable backend services, cloud-native applications, and distributed systems. For mid-level developers, interviews typically focus on a deep understanding of Go's concurrency model, memory management, type system, and idiomatic patterns. This tutorial covers the top 50 Go interview questions, complete with practical code examples, explanations, and best practices to help you ace your next interview.

Why Go Matters for Mid-Level Developers

Mid-level Go developers are expected to move beyond basic syntax and demonstrate a working knowledge of goroutines, channels, interfaces, garbage collection, and the standard library. Companies hiring for Go roles want engineers who can write efficient, idiomatic, and maintainable code. Understanding these 50 questions will help you articulate trade-offs, debug tricky concurrency bugs, and design robust systems.

Go Fundamentals

1. What is Go, and what are its key features?

Go is a statically typed, compiled language designed at Google. Key features include simplicity, fast compilation, built-in concurrency via goroutines and channels, garbage collection, strong standard library, and cross-platform compilation.

2. What is the difference between var, short variable declaration :=, and const?

var declares variables with explicit type or inference, usable at package level. := is short declaration usable only inside functions. const declares compile-time constants.

var x int = 10
var y = 20
const Pi = 3.14

func main() {
    z := 30
    fmt.Println(x, y, z, Pi)
}

3. What are the zero values in Go?

Variables declared without explicit initialization receive zero values: 0 for numeric types, false for booleans, "" for strings, and nil for pointers, slices, maps, channels, functions, and interfaces.

4. What is the difference between an array and a slice?

Arrays have a fixed size that is part of their type, while slices are dynamic, reference-based views over arrays. Slices carry a pointer, length, and capacity.

var arr [3]int = [3]int{1, 2, 3}
s := arr[:2]
fmt.Println(len(s), cap(s)) // 2 3

5. How does slice growth work?

When a slice's capacity is exceeded, Go allocates a new underlying array. For small slices, capacity typically doubles; for larger ones (over 1024 elements), it grows by ~25%.

6. What is the difference between make and new?

new(T) allocates zero-valued *T. make is used only for slices, maps, and channels and initializes internal structures, returning T (not a pointer).

p := new(int)    // *int, value 0
s := make([]int, 0, 5)
m := make(map[string]int)

7. How do you copy a slice?

Use the built-in copy function to avoid shared underlying arrays.

src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)

8. What is a map in Go, and is it thread-safe?

A map is a built-in hash table. It is not thread-safe for concurrent reads and writes. Use sync.RWMutex or sync.Map for concurrent access.

9. How do you check if a key exists in a map?

m := map[string]int{"a": 1}
v, ok := m["a"]
if ok {
    fmt.Println("found", v)
}

10. What is the difference between a string and a byte slice?

A string is immutable, while []byte is mutable. Conversions between them copy data unless the compiler can optimize. Strings are UTF-8 encoded by convention.

Functions and Methods

11. What is the difference between a function and a method in Go?

A method is a function with a receiver argument. Receivers can be value or pointer types, affecting mutability and copying behavior.

type Counter struct{ n int }

func (c Counter) Value() int { return c.n }
func (c *Counter) Inc()      { c.n++ }

12. When should you use a pointer receiver vs a value receiver?

Use pointer receivers when the method mutates the receiver, the struct is large, or you need consistency across methods. Use value receivers for small, immutable types.

13. What are variadic functions?

Functions that accept a variable number of arguments of the same type, treated as a slice inside the function.

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
sum(1, 2, 3)

14. What are deferred functions, and how do they execute?

defer schedules a function call to run when the surrounding function returns. Deferred calls execute in LIFO order and are commonly used for resource cleanup.

func main() {
    defer fmt.Println("third")
    defer fmt.Println("second")
    fmt.Println("first")
}
// Output: first, second, third

15. Are deferred function arguments evaluated at defer time or execution time?

Arguments are evaluated immediately when the defer statement runs, not when the deferred function executes. This is a common source of bugs.

i := 1
defer fmt.Println(i) // prints 1
i = 2
fmt.Println(i)       // prints 2

16. What are closures in Go?

Closures are functions that capture variables from their surrounding scope. They are useful for callbacks, generators, and encapsulation.

func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

Interfaces and Type System

17. What is an interface in Go?

An interface is a type that specifies a set of method signatures. Go interfaces are satisfied implicitly—no explicit declaration is needed.

type Speaker interface {
    Speak() string
}

type Dog struct{}
func (d Dog) Speak() string { return "Woof" }

var s Speaker = Dog{}

18. What is the empty interface interface{}?

The empty interface has no methods, so every type satisfies it. Pre-Go 1.18, it was used for generic containers. In modern Go, prefer any (an alias) or generics.

19. What is a type assertion?

A type assertion extracts the underlying concrete value from an interface.

var i interface{} = "hello"
s, ok := i.(string)

20. What is a type switch?

A type switch allows branching on the concrete type of an interface value.

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Println("int", v)
    case string:
        fmt.Println("string", v)
    default:
        fmt.Println("unknown")
    }
}

21. What is the difference between interface{} and generics?

Generics (Go 1.18+) provide compile-time type safety without boxing. interface{} requires runtime type assertions and loses type information.

func Max[T int | float64](a, b T) T {
    if a > b {
        return a
    }
    return b
}

22. What are type constraints?

Constraints define the set of types a type parameter can accept. The constraints package (now cmp) provides common ones like Ordered.

23. Can you compare two interface values?

Yes, if the underlying types are comparable. Comparing interfaces with non-comparable underlying types (like slices or maps) panics at runtime.

Concurrency

24. What is a goroutine?

A goroutine is a lightweight thread of execution managed by the Go runtime. They have small initial stacks (2KB) that grow as needed.

go func() {
    fmt.Println("running in goroutine")
}()

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

Goroutines are multiplexed onto a small number of OS threads by the Go scheduler. They are cheaper, with smaller stacks and faster creation. OS threads are heavier and managed by the OS.

26. What is a channel, and what are its types?

Channels are typed conduits for communication between goroutines. They can be unbuffered (synchronous) or buffered (asynchronous until full).

unbuf := make(chan int)
buf := make(chan int, 5)

27. What happens when you send to a closed channel?

Sending to a closed channel panics. Receiving from a closed channel returns the zero value immediately without blocking.

28. How do you gracefully close a channel?

Only the sender should close a channel, and only when no more values will be sent. Multiple closings or closing from the receiver cause panics.

29. What is the select statement?

select lets a goroutine wait on multiple channel operations, choosing the first one that is ready. It is essential for multiplexing and timeouts.

select {
case msg := <-ch1:
    fmt.Println(msg)
case ch2 <- 42:
    fmt.Println("sent")
case <-time.After(time.Second):
    fmt.Println("timeout")
}

30. What is a data race, and how do you detect it?

A data race occurs when two goroutines access the same variable concurrently, with at least one writing. Use the -race flag to detect races: go run -race main.go.

31. What is sync.Mutex vs sync.RWMutex?

Mutex provides exclusive locking. RWMutex allows multiple concurrent readers or one writer. Use RWMutex when reads vastly outnumber writes.

var mu sync.RWMutex
mu.RLock()
defer mu.RUnlock()

32. What is sync.WaitGroup?

It waits for a collection of goroutines to finish. Use Add, Done, and Wait.

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}
wg.Wait()

33. What is sync.Once?

It ensures a function executes exactly once, even across goroutines. Useful for lazy initialization.

var once sync.Once
once.Do(initConfig)

34. What is sync.Map, and when should you use it?

sync.Map is a concurrent map safe for use by multiple goroutines. Use it when keys are write-once, read-many, or when goroutines maintain disjoint key sets. Otherwise, a regular map with a mutex is often faster.

35. What is a worker pool pattern?

A worker pool limits concurrency by spawning a fixed number of goroutines that pull jobs from a channel.

jobs := make(chan int, 100)
results := make(chan int, 100)

for w := 0; w < 3; w++ {
    go func() {
        for j := range jobs {
            results <- j * 2
        }
    }()
}

36. What is the fan-out/fan-in pattern?

Fan-out distributes work across multiple goroutines; fan-in merges their results into a single channel. It is useful for parallelizing independent tasks.

37. How do you implement a timeout in Go?

Use context.WithTimeout or time.After with select.

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-ctx.Done():
    fmt.Println("timed out")
}

38. What is the context package used for?

Contexts carry deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines. Always pass ctx as the first argument in functions that may block.

39. What is goroutine leakage?

It occurs when goroutines block forever (e.g., waiting on a channel nobody writes to) and are never cleaned up. Use contexts, close channels, and ensure all paths exit.

Memory Management and Performance

40. How does Go's garbage collector work?

Go uses a concurrent, tri-color mark-and-sweep garbage collector. It is designed for low latency, with sub-millisecond pause times in recent versions. The GC runs concurrently with the application.

41. What is escape analysis?

The compiler decides whether a variable lives on the stack or escapes to the heap. Stack allocations are cheap and freed automatically; heap allocations require GC. Use go build -gcflags="-m" to see escape decisions.

42. What is the difference between stack and heap allocation?

Stack allocations are fast, scoped to function calls, and don't need GC. Heap allocations outlive the function that creates them and are managed by the GC.

43. How do you reduce allocations in Go?

44. What is sync.Pool?

It is a pool of temporary objects that can be reused to reduce GC pressure. Objects in the pool may be deallocated at any time, so don't store long-lived state.

var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)

Error Handling

45. How does Go handle errors?

Go uses explicit error values rather than exceptions. Functions return errors as the last return value, and callers check them.

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

46. What is the difference between errors.New and fmt.Errorf?

errors.New creates a simple error with a static message. fmt.Errorf formats the message and, with %w, wraps another error to preserve the chain.

err := fmt.Errorf("query failed: %w", dbErr)

47. How do you unwrap and inspect errors?

Use errors.Is to check identity and errors.As to extract a typed error from the chain.

if errors.Is(err, os.ErrNotExist) {
    // handle missing file
}

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println(pathErr.Path)
}

48. Should you use panic and recover for error handling?

No. Reserve panic for truly unrecoverable conditions (programmer errors, invariant violations). recover is mainly used in libraries that expose a safe API boundary, such as HTTP handlers.

Testing and Tooling

49. How do you write unit tests in Go?

Place test files ending in _test.go and use functions named TestXxx(t *testing.T). Run with go test.

func TestAdd(t *testing.T) {
    got := Add(1, 2)
    want := 3
    if got != want {
        t.Errorf("Add(1,2) = %d, want %d", got, want)
    }
}

50. What are table-driven tests, and why are they idiomatic in Go?

Table-driven tests define inputs and expected outputs in a slice, then loop over them. They keep tests concise, easy to extend, and consistent.

func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
    }
    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf("Add(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

Best Practices for Mid-Level Go Developers

Conclusion

Mastering these 50 Go interview questions gives you a solid foundation for mid-level roles. The key themes interviewers probe are concurrency correctness, memory and allocation awareness, idiomatic error handling, interface design, and testing discipline. Practice writing small programs that exercise goroutines, channels, contexts, and generics, and always run your code with the -race flag during development. Combine conceptual understanding with hands-on coding, and you will be well-prepared to demonstrate both depth and pragmatism in your next Go interview.

— Ad —

Google AdSense will appear here after approval

← Back to all articles