Skip to main content

Golang engineer coding interview questions

11 questions on coding for golang engineer candidates. Expect prompts such as “Implement a bounded worker pool in Go” and “Implement a thread-safe LRU cache in Go”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 11 of 11 coding questions.

As asked

Write a function that takes a slice of jobs and a maximum concurrency limit, processes each job with a provided function, and returns all results. The worker count must not exceed the limit, and the function should propagate the first error encountered while still draining all remaining goroutines.

Sample answer outline

A strong answer creates a jobs channel, launches exactly N worker goroutines that range over it, sends results and errors to output channels, and uses a sync.WaitGroup to know when all workers finish. The key design decision is how to handle errors: if the goal is to process every job and collect all errors, workers should continue consuming after an error rather than returning early. If the goal is to stop on the first error, the jobs channel must be drained or a context must be cancelled so blocked senders do not hang. The first-error case uses a sync.Once to capture the error without a race. The candidate should close the jobs channel after all items are sent and the results channel after the WaitGroup is done, and should explain the tradeoff between fail-fast and drain-all semantics.

Reference implementation (go)

Go
func WorkerPool(jobs []Job, maxWorkers int, fn func(Job) (Result, error)) ([]Result, error) {
    jobCh := make(chan Job, len(jobs))
    resultCh := make(chan Result, len(jobs))
    errCh := make(chan error, 1)

    var wg sync.WaitGroup
    for i := 0; i < maxWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := range jobCh {
                r, err := fn(j)
                if err != nil {
                    select {
                    case errCh <- err:
                    default:
                    }
                    return
                }
                resultCh <- r
            }
        }()
    }

    for _, j := range jobs {
        jobCh <- j
    }
    close(jobCh)

    wg.Wait()
    close(resultCh)
    // collect results...
}

Expect these follow-ups

  • How would you add context cancellation so in-flight jobs are cancelled when the context is done?
  • What happens if a worker panics and you have not recovered it?
goroutineschannelsconcurrencyworker-poolerror-handling

As asked

Write a thread-safe LRU cache in Go with Get and Put operations, both O(1). The cache has a fixed capacity and evicts the least recently used entry when full. Use a doubly-linked list and a map.

Sample answer outline

A strong answer uses a map[int]*list.Element backed by a container/list. Each element stores the key-value pair so the map can be updated on eviction. Get moves the element to the front; Put inserts at the front and removes from the back when over capacity. A sync.RWMutex or Mutex guards all operations. The candidate should handle the case where Put updates an existing key without changing the size.

Reference implementation (go)

Go
type LRU struct {
    cap   int
    mu    sync.Mutex
    items map[int]*list.Element
    list  *list.List
}

type entry struct{ key, val int }

func (l *LRU) Get(key int) (int, bool) {
    l.mu.Lock()
    defer l.mu.Unlock()
    if el, ok := l.items[key]; ok {
        l.list.MoveToFront(el)
        return el.Value.(*entry).val, true
    }
    return 0, false
}

Expect these follow-ups

  • How would you extend this to support TTL-based expiration without a background goroutine?
  • What is the cache performance difference between a read-heavy and write-heavy workload for this implementation?
data-structureslrumaplinked-listconcurrency

As asked

The following Go code has a data race. Identify the bug, explain why it is a race even though only one goroutine writes at a time, and rewrite it to be safe using either sync.Map, a mutex, or channels.

Sample answer outline

The bug is concurrent reads and writes to the same map without synchronization. Go maps are not safe for concurrent use; even if only one goroutine writes, any concurrent read is a race condition the runtime can detect with -race. The fix using a mutex wraps all reads under RLock and writes under Lock. Alternatively sync.Map is suitable if reads dominate. The candidate should note that sync.Map has worse performance when writes are frequent because it uses an internal copy-on-write mechanism.

Reference implementation (go)

Go
var cache = map[string]int{}

func set(k string, v int) { cache[k] = v }
func get(k string) int   { return cache[k] }

func main() {
    go func() {
        for i := 0; i < 1000; i++ {
            set("key", i)
        }
    }()
    for i := 0; i < 1000; i++ {
        _ = get("key")
    }
}

Expect these follow-ups

  • What does the Go race detector report for this code, and how do you enable it in tests?
  • When is sync.Map a better choice than a plain map with a RWMutex?
data-racemapsconcurrencysyncdebugging

As asked

Design a Go API client method that respects request deadlines, cancellation and cleanup. What mistakes do you avoid?

Sample answer outline

The method should accept context.Context as the first parameter and pass it into network, database or RPC calls rather than inventing a separate timeout mechanism. It should not store context in a struct for long-lived reuse, and it should always call cancel for derived contexts to release timers. Cleanup should happen even on cancellation, but expensive retries should stop when ctx.Done is closed. Strong candidates mention distinguishing context.Canceled from context.DeadlineExceeded in logs and metrics. The common failure is to create a timeout but then call an API that ignores it.

Reference implementation (go)

Go
func FetchUser(ctx context.Context, client *http.Client, baseURL, id string) (*User, error) {
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/users/"+id, nil)
	if err != nil {
		return nil, err
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("fetch user: status %d", resp.StatusCode)
	}

	var user User
	if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
		return nil, err
	}
	return &user, nil
}

Expect these follow-ups

  • When should a library create a child timeout versus trusting the caller's deadline?
  • How do you test cancellation deterministically?
  • What should be logged when the client disconnects?
contexthttpcancellation

As asked

Write a Go pipeline that reads integers from an input channel, fans out to N processing goroutines that square each number, then fans back in to a single output channel. The pipeline must clean up all goroutines when the input channel is closed.

Sample answer outline

A strong answer creates N worker goroutines each reading from the shared input channel and writing to their own output channel, then merges all output channels using a merge goroutine that selects across them with a WaitGroup to close the merged channel once all workers finish. The candidate should use sync.WaitGroup in the merge function, not outside it, to avoid a race on the channel close.

Reference implementation (go)

Go
func fanOut(in <-chan int, n int) []<-chan int {
    outs := make([]<-chan int, n)
    for i := 0; i < n; i++ {
        ch := make(chan int)
        outs[i] = ch
        go func() {
            defer close(ch)
            for v := range in {
                ch <- v * v
            }
        }()
    }
    return outs
}

func merge(cs []<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)
    for _, c := range cs {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(c)
    }
    go func() { wg.Wait(); close(out) }()
    return out
}

Expect these follow-ups

  • How does this pattern change when the order of results must match the order of inputs?
  • What is the overhead of having N=1000 goroutines versus N=GOMAXPROCS for a CPU-bound task?
pipelinegoroutineschannelsfan-outconcurrency

As asked

Implement a Trie in Go that supports Insert(word string) and StartsWith(prefix string) []string to return all words with that prefix. Each node holds its children and a flag marking whether it is the end of a word.

Sample answer outline

A strong answer defines a TrieNode with a map[rune]*TrieNode children and a bool isEnd field. Insert walks character by character, creating nodes as needed. StartsWith walks to the prefix node then does a DFS to collect all complete words. The candidate should handle empty string and prefix longer than any stored word. Bonus for noting that a map is flexible but a [26]byte array is faster for lowercase ASCII.

Reference implementation (go)

Go
type TrieNode struct {
    children map[rune]*TrieNode
    isEnd    bool
}

type Trie struct{ root *TrieNode }

func (t *Trie) Insert(word string) {
    node := t.root
    for _, ch := range word {
        if _, ok := node.children[ch]; !ok {
            node.children[ch] = &TrieNode{children: make(map[rune]*TrieNode)}
        }
        node = node.children[ch]
    }
    node.isEnd = true
}

Expect these follow-ups

  • How would you add Delete support without leaving orphan nodes?
  • How would you serialize and deserialize the trie for persistence?
triedata-structuresalgorithmsstringdfs

As asked

Write a thread-safe token bucket rate limiter in Go. It should allow up to N requests per second with a burst capacity of B tokens. Provide an Allow() bool method that returns true if a request is permitted.

Sample answer outline

A strong answer stores the current token count as a float64, the last refill time, the refill rate, and the burst cap. Allow computes elapsed time since last refill, adds rate*elapsed tokens up to the burst cap, and if tokens >= 1 decrements and returns true. A sync.Mutex protects the state. The candidate should note that golang.org/x/time/rate provides a production-ready implementation using the same algorithm with additional features like Wait and Reserve.

Reference implementation (go)

Go
type TokenBucket struct {
    mu       sync.Mutex
    tokens   float64
    maxBurst float64
    rate     float64 // tokens per second
    last     time.Time
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()
    now := time.Now()
    elapsed := now.Sub(tb.last).Seconds()
    tb.tokens = min(tb.maxBurst, tb.tokens+elapsed*tb.rate)
    tb.last = now
    if tb.tokens >= 1 {
        tb.tokens--
        return true
    }
    return false
}

Expect these follow-ups

  • How does a sliding window rate limiter differ from a token bucket, and when would you choose it?
  • How would you implement a distributed rate limiter using Redis?
rate-limitertoken-bucketconcurrencyalgorithmsstdlib

As asked

Given a slice of K sorted linked lists, write a Go function that merges them into one sorted list. Use container/heap for an efficient O(N log K) solution where N is the total number of nodes.

Sample answer outline

A strong answer implements the heap.Interface on a type holding *ListNode pointers, initializing the heap with the head of each non-nil list. The main loop pops the minimum node, appends it to the result, and pushes its Next if non-nil. The candidate should explain the heap.Interface requirements: Len, Less, Swap, Push, Pop. Time complexity is O(N log K) because each of the N nodes is pushed and popped once with log K heap operations.

Reference implementation (go)

Go
type MinHeap []*ListNode
func (h MinHeap) Len() int            { return len(h) }
func (h MinHeap) Less(i, j int) bool  { return h[i].Val < h[j].Val }
func (h MinHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x any)         { *h = append(*h, x.(*ListNode)) }
func (h *MinHeap) Pop() any {
    old := *h; n := len(old)
    x := old[n-1]; *h = old[:n-1]
    return x
}

Expect these follow-ups

  • What is the space complexity of this approach?
  • How would you solve this problem if you could not use extra memory beyond the output list?
heaplinked-listalgorithmscontainer-heapsorting

As asked

You have a function that fetches user data from a database. Under load, many goroutines call it simultaneously for the same user ID, causing a thundering herd. Write a wrapper using golang.org/x/sync/singleflight that ensures only one database call is in flight per user ID at a time.

Sample answer outline

A strong answer wraps the database call in group.Do(key, func() (interface{}, error) {...}) where key is the user ID. All callers with the same key receive the same result once the single in-flight call completes. The candidate should address the case where the database call fails: all callers receive the error, so a negative result is not cached. They should also note that singleflight.Group.DoChan is the non-blocking variant useful with context cancellation.

Reference implementation (go)

Go
var g singleflight.Group

func fetchUser(ctx context.Context, id string) (*User, error) {
    v, err, _ := g.Do(id, func() (interface{}, error) {
        return db.QueryUser(ctx, id)
    })
    if err != nil {
        return nil, err
    }
    return v.(*User), nil
}

Expect these follow-ups

  • What is the difference between singleflight and a simple mutex around the cache lookup?
  • How would you combine singleflight with a local in-memory cache and a Redis layer?
singleflightcachingconcurrencythundering-herdgolang-x-sync

As asked

You receive a JSON file with millions of objects in a top-level array. Write Go code using json.Decoder to process each object one at a time without loading the whole array into memory. Handle parse errors gracefully.

Sample answer outline

A strong answer creates a json.Decoder from the reader, calls Decode to consume the opening '[' token, then loops calling dec.Decode(&obj) until io.EOF or an error is returned. The loop checks for ']' token to stop. The candidate should mention checking the token type explicitly for robustness and wrapping errors with context. Bonus for noting that using json.NewDecoder is far more memory-efficient than json.Unmarshal for large files.

Reference implementation (go)

Go
func processLargeJSON(r io.Reader) error {
    dec := json.NewDecoder(r)
    // read opening '['
    if _, err := dec.Token(); err != nil {
        return err
    }
    for dec.More() {
        var obj MyStruct
        if err := dec.Decode(&obj); err != nil {
            return fmt.Errorf("decode item: %w", err)
        }
        process(obj)
    }
    return nil
}

Expect these follow-ups

  • What is the difference between json.Decoder.Decode and json.Unmarshal in terms of memory usage?
  • How would you parallelize the processing of each decoded object while preserving order?
jsonstreamingiomemorystdlib

As asked

Implement a bounded worker pool in Go that processes jobs, propagates cancellation and returns when all workers have stopped.

Sample answer outline

The core is a fixed number of workers reading from a jobs channel and a WaitGroup that represents worker lifetime. The producer should stop enqueueing when the context is cancelled and close the jobs channel when no more work will be sent. Workers should select on ctx.Done as well as the jobs channel so shutdown does not depend on new jobs arriving. Results and errors need their own bounded path or a clear aggregation strategy. Candidates often create leaks by returning early without closing channels or waiting for workers.

Reference implementation (go)

Go
func RunPool(ctx context.Context, jobs <-chan Job, workers int, handle func(context.Context, Job) error) error {
	g, ctx := errgroup.WithContext(ctx)
	for i := 0; i < workers; i++ {
		g.Go(func() error {
			for {
				select {
				case <-ctx.Done():
					return ctx.Err()
				case job, ok := <-jobs:
					if !ok {
						return nil
					}
					if err := handle(ctx, job); err != nil {
						return err
					}
				}
			}
		})
	}
	return g.Wait()
}

Expect these follow-ups

  • How would you return the first error and cancel the rest?
  • What happens if processing blocks forever inside a worker?
  • When would errgroup be a better fit?
channelsworker-poolconcurrency