Skip to main content

Go Interview Preparation

This chapter covers the most common Go interview topics, coding challenges, and system design questions you will encounter. The key to Go interviews is demonstrating that you understand why Go makes certain design choices, not just how to use its features. Interviewers at companies like Google, Uber, and Cloudflare want to see that you think about trade-offs: “Go chose X because Y, and the cost is Z.”

Language Fundamentals

Common Interview Questions

Arrays:
  • Fixed size, part of the type: [5]int[10]int
  • Value type - copied when passed
  • Size known at compile time
Slices:
  • Dynamic size, backed by an array
  • Reference type - contains pointer to underlying array
  • Has length and capacity
  • Can grow with append()
  • Deferred calls are executed in LIFO order when the function returns
  • Arguments are evaluated immediately, not when deferred call runs
  • Commonly used for cleanup (closing files, unlocking mutexes)
  • new(T): Allocates zeroed storage for type T, returns *T
  • make(T, args): Creates and initializes slices, maps, channels only
interface{} (or any since Go 1.18) is the empty interface that all types implement. Used for:
  • Generic containers before generics
  • JSON unmarshaling
  • Printf-style functions
Requires type assertions to use:
Methods have a receiver - they’re bound to a type:
Use pointer receivers when:
  • You need to modify the receiver
  • The receiver is large (avoid copying)
  • Consistency with other methods on the type
  • Go uses garbage collection (concurrent, tri-color mark-and-sweep)
  • Stack: Local variables, function calls (fast, automatic)
  • Heap: Escaped variables, dynamically sized data (GC managed)
  • Escape analysis determines stack vs heap allocation

Concurrency Questions

Goroutines:
  • Lightweight (2KB initial stack)
  • Managed by Go runtime
  • M:N scheduling (many goroutines on few OS threads)
  • Fast context switching
  • Can have thousands running
OS Threads:
  • Heavy (1-2MB stack)
  • Managed by OS kernel
  • Expensive context switches
  • Limited by OS resources
Channels are typed conduits for communication between goroutines:
Data race occurs when multiple goroutines access shared data concurrently with at least one write.Prevention methods:
select waits on multiple channel operations:
  • Blocks until one case can proceed
  • Random selection if multiple ready
  • default makes it non-blocking
Context carries deadlines, cancellation signals, and request-scoped values:
Goroutine leaks occur when goroutines are blocked forever. Prevention:

Coding Challenges

Implement a LRU Cache

An LRU (Least Recently Used) cache is one of the most common interview coding challenges. The key insight is using a hash map for O(1) lookups combined with a doubly linked list for O(1) insertion, deletion, and reordering. Every Get moves the accessed node to the front; when capacity is exceeded, the node at the back (least recently used) is evicted.

Implement Rate Limiter

Implement Worker Pool

Implement Concurrent Map


System Design Topics

Design a URL Shortener

Scalability considerations:
  • Distributed ID generation (Snowflake, UUID)
  • Database sharding by short code prefix
  • Caching (Redis) for hot URLs
  • Rate limiting per user/IP

Design a Pub/Sub System


Performance Questions

  • Pre-allocate slices: make([]T, 0, expectedSize)
  • Use sync.Pool for temporary objects
  • Avoid string concatenation in loops (use strings.Builder)
  • Reuse buffers
  • Use value types instead of pointers when appropriate
  • Align struct fields properly
Escape analysis determines whether a variable can stay on the stack or must “escape” to the heap:
Stack is faster (no GC), heap is necessary for escaped values.

Best Practices Checklist

Code Quality

  • Use go fmt and go vet
  • Run golangci-lint
  • Write table-driven tests
  • Use meaningful variable names
  • Keep functions small and focused
  • Handle all errors
  • Use interfaces for dependencies

Concurrency

  • Prefer channels for communication
  • Always use context for cancellation
  • Run race detector (-race)
  • Close channels from sender side
  • Use sync.WaitGroup for goroutine synchronization

Performance

  • Profile before optimizing
  • Pre-allocate slices when size is known
  • Use sync.Pool for frequent allocations
  • Avoid defer in hot loops
  • Use buffered I/O

Production

  • Structured logging
  • Health checks
  • Graceful shutdown
  • Configuration management
  • Metrics and monitoring
  • Proper error handling with stack traces

Quick Reference Card


Summary

Mastering Go for interviews requires:
  1. Strong fundamentals: Types, interfaces, error handling
  2. Concurrency expertise: Goroutines, channels, sync primitives
  3. Performance awareness: Profiling, memory management
  4. Production experience: Logging, config, deployment
  5. Coding practice: LeetCode-style problems in Go
  6. System design: Distributed systems patterns
Good luck with your interviews!

Interview Deep-Dive

Strong Answer:
  • The core data structure is a mapping from short codes to long URLs. At 100K RPS, a single mutex-protected map becomes a bottleneck due to lock contention. I would use a sharded concurrent map: 256 shards, each with its own sync.RWMutex. The shard is selected by hashing the short code. Reads use RLock (multiple concurrent readers), writes use Lock. This reduces contention by 256x.
  • For ID generation, I would use atomic.Uint64.Add(1) to generate sequential IDs without locks, then encode to base62 for the short code. At 100K RPS, a uint64 counter would not overflow for millions of years.
  • The concurrency model: the HTTP server handles requests with the standard net/http server (one goroutine per connection, multiplexed by the Go runtime). For writes (creating short URLs), the sharded map handles the concurrency. For reads (resolving short codes), the sharded map with RLock allows full concurrent reads.
  • Bottlenecks: at 100K RPS, the in-memory map is fast, but persistence becomes the bottleneck. I would write to a buffered channel and have a pool of database writer goroutines batch-insert to PostgreSQL asynchronously. For reads, I would put a Redis cache in front of the database, with the in-memory sharded map as an L1 cache.
  • Edge cases: URL validation (reject malformed URLs), rate limiting per IP to prevent abuse, TTL for short URLs (periodic cleanup goroutine), and handling the case where two concurrent requests try to shorten the same long URL (use the long URL as a secondary key to deduplicate).
Follow-up: How would you handle the transition from a single-node in-memory store to a distributed system with multiple instances?With multiple instances, the in-memory map becomes a local cache, and the source of truth moves to a shared database (PostgreSQL with the short code as primary key) and a shared cache (Redis). ID generation must be distributed — options include: Snowflake-style IDs (combining timestamp, instance ID, and sequence), a centralized counter service, or random UUIDs with collision detection. I would use Redis INCR for atomic distributed counters — it is fast enough for 100K RPS and guarantees uniqueness. Each instance would still maintain an in-memory LRU cache for the hottest URLs to reduce Redis/database load. The cache invalidation strategy is simple: short URL mappings are immutable once created, so the cache never needs invalidation, only TTL-based expiry.
Strong Answer:
  • The standard LRU cache uses a doubly-linked list (for O(1) move-to-front and remove-from-back) combined with a hash map (for O(1) key lookup). On Get: look up the key in the map, if found, move the node to the front of the list and return the value. On Put: if the key exists, update the value and move to front. If it does not exist, create a new node at the front. If capacity is exceeded, remove the tail node (least recently used) and delete it from the map.
  • For thread safety, I would wrap the entire data structure with a sync.RWMutex. Reads (Get) use RLock and writes (Put) use Lock. However, since Get also modifies the list (move to front), a pure RLock is not sufficient for Get — you need a Lock for both read and write operations. This means all operations are mutually exclusive.
  • To improve concurrency, you could shard the cache: 16 independent LRU caches, each with its own lock. The shard is selected by hashing the key. This allows 16 concurrent operations on different shards.
  • Time complexity: Get is O(1) (hash map lookup + linked list move). Put is O(1) (hash map insert + linked list insert + possible tail removal). Space complexity: O(capacity) for both the map and the list.
  • In production, I would consider using sync.Map for read-heavy workloads, or a purpose-built library like github.com/hashicorp/golang-lru which is battle-tested. For very high concurrency, a clock-based approximation (CLOCK or CLOCK-Pro) avoids the linked list manipulation on every access.
Follow-up: What race condition could occur if you used sync.RWMutex with RLock for Get and Lock for Put?Since Get moves the accessed node to the front of the doubly-linked list, it is a write operation on the list structure even though it is logically a “read” on the cache. If two goroutines call Get concurrently with RLock, they both try to modify the linked list pointers simultaneously — a data race. The list node’s prev and next pointers would be corrupted, potentially creating a cycle or dangling pointer. The fix is to use Lock (exclusive) for Get as well, accepting that all operations are serialized per shard. An alternative is a lock-free design: use an atomic counter for access frequency instead of a linked list for recency, approximating LRU with LFU-like behavior (as some high-performance caches like Ristretto do).
Strong Answer:
  • If the heap profile shows steady allocations but RSS keeps growing, there are several possibilities beyond heap leaks.
  • First, goroutine leak: check runtime.NumGoroutine(). Each goroutine consumes at least 2KB of stack, and stacks can grow. 100,000 leaked goroutines could consume 200MB+. Take a goroutine profile (/debug/pprof/goroutine?debug=2) and look for goroutines blocked on channel operations or sleeping forever.
  • Second, stack growth: goroutine stacks start at 2KB but grow dynamically (up to 1GB by default). If your code has deep call stacks or large stack-allocated arrays, a few thousand goroutines could use hundreds of MB. Check the goroutine profile for unusually deep stacks.
  • Third, memory fragmentation: the Go runtime requests memory from the OS in chunks and may not return it promptly. HeapInuse might be reasonable but HeapSys (memory obtained from OS) could be much higher due to fragmentation. Check runtime.MemStats — if HeapReleased is low relative to HeapSys, the runtime is holding memory it does not actively use. Force return with debug.FreeOSMemory() as a test.
  • Fourth, cgo memory: if any dependency uses cgo, memory allocated by C code is invisible to the Go heap profiler. It does not show up in pprof but does show up in RSS. Use OS-level tools (pmap on Linux) to see where the memory is allocated.
  • Fifth, mmap’d files or shared memory: if the service memory-maps large files, these appear in RSS but not in the Go heap.
  • Investigation order: check goroutine count, check MemStats, compare HeapInuse vs HeapSys vs RSS, check for cgo usage, and finally use OS-level memory analysis tools.
Follow-up: How does Go’s garbage collector handle long-lived objects differently from short-lived ones, and does Go have generational GC?Go does NOT use generational garbage collection (unlike Java’s JVM). Go’s GC treats all objects equally — there is no young generation or old generation. Every GC cycle scans all live objects. This means long-lived objects are not “promoted” to a less-frequently-scanned heap area; they are scanned on every cycle. The consequence is that Go’s GC overhead scales with the number of live objects (pointers to scan), not just the allocation rate. If you have millions of long-lived pointers in memory (like a large in-memory cache), each GC cycle must trace through all of them. The mitigation strategies are: reduce the number of pointer-containing objects (use value types, use slices of values instead of slices of pointers), use off-heap storage for large caches (memory-mapped files, or store data as []byte which the GC treats as a single opaque blob), or tune GOGC and GOMEMLIMIT to control GC frequency and memory budget.