# Go (Golang) Interview Questions: Concurrency, Interfaces, and Performance

Source: https://www.techinterview.org/post/3233460401/golang-interview-questions/
Updated: 2026-04-16 · techinterview.org

Go is the language of cloud infrastructure — Kubernetes, Docker, etcd, Terraform, and Prometheus are all written in Go. This guide covers the Go interview questions you will face at companies using Go for backend services and infrastructure.

## Goroutines and the Go Scheduler


```
package main

import (
    "fmt"
    "sync"
    "time"
    "runtime"
)

// Goroutine: lightweight thread managed by the Go runtime.
// Go scheduler: M:N threading — multiplexes G goroutines onto M OS threads.
// Default: GOMAXPROCS = number of CPUs (can be changed at runtime).

func goroutineBasics() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {  // Capture id by value — avoid closure over loop var
            defer wg.Done()
            fmt.Printf("Worker %d runningn", id)
        }(i)
    }

    wg.Wait()  // Block until all goroutines finish
}

// Goroutine leak: goroutine blocked forever on channel with no sender
// Always pair goroutine creation with a way to stop it

func withTimeout(ctx context.Context, work func() error) error {
    done := make(chan error, 1)  // Buffered: goroutine can always send
    go func() {
        done <- work()
    }()
    select {
    case err := <-done:
        return err
    case <-ctx.Done():
        return ctx.Err()
    }
}
```


## Channels — Communication and Synchronization


```
package main

import (
    "fmt"
    "time"
)

// Channel: typed conduit between goroutines.
// Unbuffered: send blocks until receiver is ready (synchronous)
// Buffered:   send blocks only when buffer is full

func channelPatterns() {
    // Fan-out: one sender, multiple receivers
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start 3 workers
    for w := 0; w < 3; w++ {
        go func() {
            for job := range jobs {  // Range over channel until closed
                results <- job * job
            }
        }()
    }

    // Send jobs
    for j := 0; j < 9; j++ {
        jobs <- j
    }
    close(jobs)  // Signal workers: no more jobs

    // Collect results
    for a := 0; a < 9; a++ {
        fmt.Println(<-results)
    }
}

// Pipeline: chain of goroutines connected by channels
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

// select: multiplex across multiple channel operations
func rateLimiter() {
    requests := make(chan int, 5)
    ticker := time.NewTicker(200 * time.Millisecond)
    defer ticker.Stop()
    quit := make(chan struct{})

    for {
        select {
        case req := <-requests:
            <-ticker.C  // Wait for next tick before processing
            fmt.Println("Processed:", req)
        case <-quit:
            return
        }
    }
}
```


## Interfaces and Composition


```
package main

import (
    "fmt"
    "io"
    "strings"
)

// Interfaces: satisfied implicitly (structural typing, not nominal)
// Go encourages small, focused interfaces

type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface { Reader; Writer }  // Interface embedding

// Empty interface vs any:
// interface{} / any: holds any value; requires type assertion to use
func printType(v any) {
    switch val := v.(type) {  // Type switch
    case int:    fmt.Printf("int: %dn", val)
    case string: fmt.Printf("string: %qn", val)
    case []byte: fmt.Printf("bytes: %d bytesn", len(val))
    default:     fmt.Printf("unknown: %Tn", val)
    }
}

// Practical interface design: dependency injection + testability
type Storage interface {
    Get(key string) ([]byte, error)
    Set(key string, val []byte) error
    Delete(key string) error
}

type UserService struct {
    store Storage
    cache Storage
}

func NewUserService(store, cache Storage) *UserService {
    return &UserService{store: store, cache: cache}
}

func (s *UserService) GetUser(id string) ([]byte, error) {
    // Try cache first
    if data, err := s.cache.Get(id); err == nil {
        return data, nil
    }
    // Fall back to store
    data, err := s.store.Get(id)
    if err != nil {
        return nil, err
    }
    _ = s.cache.Set(id, data)  // Populate cache
    return data, nil
}

// In tests: swap in an in-memory Storage implementation
type MemStore struct{ m map[string][]byte }
func (ms *MemStore) Get(k string) ([]byte, error) {
    v, ok := ms.m[k]
    if !ok { return nil, fmt.Errorf("not found") }
    return v, nil
}
func (ms *MemStore) Set(k string, v []byte) error { ms.m[k] = v; return nil }
func (ms *MemStore) Delete(k string) error         { delete(ms.m, k); return nil }
```


## Error Handling and Defer


```
package main

import (
    "errors"
    "fmt"
)

// Sentinel errors: package-level for comparison
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")

// Custom error types for structured error data
type ValidationError struct {
    Field   string
    Message string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: %s — %s", e.Field, e.Message)
}

// Error wrapping: %w for Is/As unwrapping
func fetchUser(id string) error {
    err := dbQuery(id)
    if err != nil {
        return fmt.Errorf("fetchUser %s: %w", id, err)  // Wrap with context
    }
    return nil
}

func handleError(err error) {
    if errors.Is(err, ErrNotFound) {  // Works through wrapping chains
        fmt.Println("User not found")
    }
    var ve *ValidationError
    if errors.As(err, &ve) {  // Type assertion through wrapping
        fmt.Println("Bad field:", ve.Field)
    }
}

// defer: runs when function returns (LIFO order)
func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("open: %w", err)
    }
    defer f.Close()  // Always runs — no need to close in every return path

    // defer for cleanup pairs well with acquire/release patterns
    mu.Lock()
    defer mu.Unlock()  // Always unlocked, even on panic

    return doWork(f)
}
```


## Memory Model and Garbage Collection


```
/*
Go GC: concurrent tri-color mark-and-sweep.
  Tri-color: white (unreachable), gray (reachable, children not scanned), black (done)
  Concurrent: GC runs alongside user goroutines (not stop-the-world)
  STW pauses: < 1ms in Go 1.17+ (just for root marking and termination)

Memory layout:
  Stack: per-goroutine, grows/shrinks dynamically (starts 2KB, max 1GB)
  Heap:  shared; GC managed
  Escape analysis: compiler decides stack vs heap allocation
    go build -gcflags="-m" shows escape decisions

Reducing GC pressure:
  - sync.Pool: reuse temporary objects (byte buffers, scratch space)
  - Avoid excessive small allocations (use slices with pre-allocated capacity)
  - Struct embedding over pointer-to-struct for small, non-interface types
*/

package main

import (
    "sync"
    "bytes"
)

// sync.Pool: reuse buffers across goroutines, reducing allocation
var bufPool = sync.Pool{
    New: func() interface{} { return new(bytes.Buffer) },
}

func processRequest(data []byte) []byte {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufPool.Put(buf)
    }()

    buf.Write(data)
    // ... process ...
    return buf.Bytes()
}

// Make vs New:
// make: creates slices, maps, channels (initializes internal structure)
// new:  allocates zeroed memory, returns pointer
s := make([]int, 0, 100)   // slice with len=0, cap=100
m := make(map[string]int)   // initialized map
p := new(int)               // *int pointing to zero int
```


## Go Interview Quick Reference

| Topic | Key Points |
| --- | --- |
| Goroutine vs thread | Goroutines: ~2KB stack, 100k+ concurrent; threads: ~2MB, OS-scheduled |
| Channel directions | chan writes |
| init() functions | Run before main(), once per package; order within package is top-to-bottom |
| Panic vs error | Error: expected failure (return); panic: programmer error or unrecoverable |
| Module proxy | GOPROXY=proxy.golang.org fetches modules; GONOSUMCHECK for private |

## Frequently Asked Questions

### What is a goroutine and how does it differ from a thread?

A goroutine is a lightweight function managed by the Go runtime. Goroutines start with ~2KB stack (vs ~2MB for OS threads) and are multiplexed onto OS threads by the Go scheduler (M:N threading). You can run hundreds of thousands of goroutines concurrently. Threads are managed by the OS; goroutines are managed by Go runtime.

### What is a channel in Go and when would you use it?

Channels are typed conduits for communication between goroutines. Unbuffered channels block the sender until a receiver is ready (synchronous handoff). Buffered channels allow sending without blocking until the buffer fills. Use channels to signal completion, distribute work (fan-out), collect results (fan-in), or implement pipelines between goroutines.

### How does Go handle errors compared to exceptions?

Go returns errors as values (multiple return values: result, error). There are no exceptions for expected failures. This makes error handling explicit — callers must check errors. Use fmt.Errorf with %w to wrap errors with context while preserving the ability to unwrap with errors.Is/errors.As. Panic is reserved for programmer errors and unrecoverable states.

## Companies That Ask This Question

- [Cloudflare Engineering Interview Guide](/post/3233460270/cloudflare-interview-guide-2026-networking-edge-computing-and-cdn-design/)

- [HashiCorp Engineering Interview Guide](/post/3233460312/hashicorp-interview-guide-2026-terraform-vault-raft-consensus-and-infrastructure-engineering/)

- [Uber Engineering Interview Guide](/post/3233460273/uber-interview-guide-2026-dispatch-systems-geospatial-algorithms-and-marketplace-engineering/)
