Go Interview: Deep Dive Questions & Answers
Các câu hỏi interview Go level senior thường test hiểu biết về runtime, concurrency, và production experience. File này chứa câu hỏi + cách trả lời có chiều sâu.
💡 Interviewer không chỉ muốn nghe câu trả lời đúng — họ muốn thấy bạn think like an engineer: trade-offs, edge cases, real-world context.
Câu hỏi Runtime & Scheduler
Q1: Goroutine vs OS thread — sự khác biệt?
Trả lời structure:
1. Cost khác biệt lớn:
- OS thread: 1-2 MB stack, ~1-2µs để tạo
- Goroutine: 2 KB stack (grow dynamically), ~200ns để tạo
2. Scheduling:
- OS thread: Kernel-level, preemptive, expensive context switch (~1-2µs)
- Goroutine: User-level, cooperative + preemptive (Go 1.14+), cheap (~20ns)
3. Practical implication:
- Có thể tạo hàng triệu goroutines (try tạo hàng triệu threads → OOM)
- M goroutines được multiplex lên N OS threads (M:N model)
Follow-up Q: "Tại sao goroutine rẻ nhưng không free?"
Answer: Goroutine vẫn có overhead:
- Stack memory (minimum 2 KB)
- Struct metadata (~200 bytes)
- Scheduling overhead
- GC scan time
Nên vẫn cần limit số goroutines trong production (worker pool pattern).
Q2: Giải thích G-M-P model
Trả lời:
G (Goroutine): Task cần execute
M (Machine): OS thread thực thi
P (Processor): Execution context — token để M có thể chạy Go code
Key insight: GOMAXPROCS = số P (default = số CPU cores)
Flow:
M phải có P mới chạy G
M + P + G → Execute code
Work stealing: Khi P idle, steal goroutines từ P khác để cân bằng load.
Follow-up Q: "Khi nào cần tune GOMAXPROCS?"
Answer: Hiếm khi cần tune. Default (số cores) đã tối ưu cho majority workloads.
Ngoại lệ:
- Container environment: set = CPU quota, không phải host cores
- CPU-bound + I/O-bound mixed: có thể tăng nhẹ (1.5-2x cores)
- Latency-sensitive: thử giảm để reduce context switch
Q3: Async preemption là gì? Tại sao quan trọng?
Trả lời:
Trước Go 1.14 (cooperative preemption):
- Compiler inject preemption checks tại function calls
- Tight loop không có function call → không bao giờ preempt
- Vấn đề: Goroutine monopolize CPU, starve others
// Trước 1.14: starve other goroutines
for {
i++ // No function call, no preemption
}
Go 1.14+ (async preemption):
- Runtime gửi signal (
SIGURG) để interrupt goroutine - Signal handler saves state, switches to scheduler
- Tight loops giờ có thể preempt
Impact: Latency predictability tốt hơn — không có goroutine chạy > 10ms.
Câu hỏi Memory & GC
Q4: Escape analysis — biến nào escape lên heap?
Trả lời:
Escape khi:
- Return pointer to local variable
- Assign to interface{} (type không biết compile-time)
- Send to channel
- Closure captures variable và outlive function
- Size không biết compile-time
Example:
// Escapes
func newUser() *User {
u := User{}
return &u // u escapes
}
// Doesn't escape
func processUser() {
u := User{}
// Use u locally
}
Check:
go build -gcflags='-m' main.go
Follow-up Q: "Escape có hại không?"
Answer: Không phải lúc nào cũng xấu. Trade-off:
- Escape → GC overhead, heap fragmentation
- Not escape → stack overflow nếu object lớn
Chỉ optimize escape khi profile chỉ ra đây là bottleneck.
Q5: GC của Go hoạt động như thế nào?
Trả lời:
Algorithm: Concurrent tri-color mark & sweep
Phases:
- Mark Setup (STW ~100µs): Scan roots (stack, globals)
- Concurrent Mark: Trace reachable objects (concurrent với user code)
- Mark Termination (STW ~100µs): Finalize marking
- Concurrent Sweep: Free unreachable objects
Tri-color:
- White: Chưa scan (initially all objects)
- Gray: Marked, chưa scan children
- Black: Scanned completely
Write barrier: Track pointer writes trong concurrent mark, đảm bảo correctness.
Target: STW pause < 1ms cho majority workloads.
Follow-up Q: "Làm sao tune GC?"
Answer:
GOGC: Default 100 (GC khi heap double). Tăng → ít GC hơn, nhiều RAM hơn.GOMEMLIMIT(Go 1.19+): Soft limit cho heap size.- Giảm allocations trong hot path (pre-allocate, reuse với sync.Pool).
Q6: Làm sao debug memory leak?
Trả lời systematic:
1. Detect leak:
// Monitor goroutine count
go func() {
for range time.Tick(10 * time.Second) {
fmt.Println("Goroutines:", runtime.NumGoroutine())
}
}()
Nếu tăng liên tục → investigate.
2. Heap profile diff:
curl http://localhost:6060/debug/pprof/heap > heap1.prof
# Wait...
curl http://localhost:6060/debug/pprof/heap > heap2.prof
# Diff
go tool pprof -base heap1.prof heap2.prof
3. Goroutine profile:
curl http://localhost:6060/debug/pprof/goroutine > goroutine.prof
go tool pprof goroutine.prof
(pprof) top
(pprof) list <function>
4. Common causes:
- Goroutine leak (channel không có receiver)
- Forgotten callbacks/timers
- Large slice holding reference to entire array
Fix: Context cancellation, proper cleanup, copy slices.
Câu hỏi Concurrency
Q7: Khi nào dùng mutex, khi nào dùng channel?
Trả lời:
Mutex: Protect shared state
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
Channel: Communicate between goroutines
jobs := make(chan Job)
// Producer
go func() {
for _, job := range allJobs {
jobs <- job
}
close(jobs)
}()
// Consumer
for job := range jobs {
process(job)
}
Rule of thumb:
- Ownership: Nếu 1 goroutine owns data → mutex
- Communication: Nếu data flow giữa goroutines → channel
- Performance: Mutex nhanh hơn channel cho simple cases
Go proverb: "Don't communicate by sharing memory; share memory by communicating."
Q8: Race condition — cách detect và fix?
Trả lời:
Detect:
go test -race ./...
go build -race
Example race:
// BAD: Race on counter
counter := 0
for i := 0; i < 1000; i++ {
go func() {
counter++ // Race!
}()
}
Race detector output:
WARNING: DATA RACE
Write at 0x... by goroutine 6:
main.main.func1()
/path/to/file.go:10 +0x3e
Previous write at 0x... by goroutine 5:
main.main.func1()
/path/to/file.go:10 +0x3e
Fix options:
1. Mutex:
var mu sync.Mutex
var counter int
mu.Lock()
counter++
mu.Unlock()
2. Atomic:
var counter int64
atomic.AddInt64(&counter, 1)
3. Channel:
counterCh := make(chan int)
go func() {
count := 0
for range counterCh {
count++
}
}()
// Increment
counterCh <- 1
Q9: Deadlock xảy ra khi nào? Cách avoid?
Trả lời:
Deadlock conditions:
- Mutual exclusion
- Hold and wait
- No preemption
- Circular wait
Example:
var mu1, mu2 sync.Mutex
// Goroutine 1
mu1.Lock()
mu2.Lock() // Wait for mu2
mu2.Unlock()
mu1.Unlock()
// Goroutine 2
mu2.Lock()
mu1.Lock() // Wait for mu1 → Deadlock!
mu1.Unlock()
mu2.Unlock()
Fix: Lock ordering
// Always lock in same order
lockInOrder(&mu1, &mu2)
defer mu2.Unlock()
defer mu1.Unlock()
Channel deadlock:
ch := make(chan int)
ch <- 42 // Block forever (no receiver)
Fix: Buffered channel hoặc goroutine receiver.
Câu hỏi Performance
Q10: Cách profile production service để tìm bottleneck?
Trả lời systematic:
1. Enable pprof:
import _ "net/http/pprof"
go http.ListenAndServe("localhost:6060", nil)
2. Collect profiles:
# CPU profile (30s)
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof
# Heap profile
curl http://localhost:6060/debug/pprof/heap > heap.prof
# Goroutine profile
curl http://localhost:6060/debug/pprof/goroutine > goroutine.prof
3. Analyze:
go tool pprof cpu.prof
(pprof) top # Top CPU consumers
(pprof) list <func> # Line-by-line breakdown
(pprof) web # Visualize
4. Execution tracer (for latency spikes):
curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out
go tool trace trace.out
5. Identify bottleneck type:
- High CPU + low latency → CPU-bound, optimize algorithm
- Low CPU + high latency → I/O-bound, add concurrency
- High GC time → reduce allocations
- Many goroutines blocked → lock contention
Q11: Làm thế nào giảm allocations?
Trả lời với examples:
1. Pre-allocate slices:
// Before: Multiple reallocations
result := []int{}
for i := 0; i < 1000; i++ {
result = append(result, i)
}
// After: Single allocation
result := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
result = append(result, i)
}
2. Reuse với sync.Pool:
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufferPool.Put(buf)
3. Avoid string concatenation:
// Before
s := ""
for _, str := range strings {
s += str // Allocates mỗi lần
}
// After
var b strings.Builder
for _, str := range strings {
b.WriteString(str)
}
s := b.String()
4. Avoid []byte ↔ string conversion:
// Use unsafe (careful!)
import "unsafe"
func bytesToString(b []byte) string {
return unsafe.String(&b[0], len(b))
}
Câu hỏi System Design với Go
Q12: Design một rate limiter
Trả lời:
Token bucket algorithm:
type RateLimiter struct {
tokens chan struct{}
rate int
}
func NewRateLimiter(rate, burst int) *RateLimiter {
rl := &RateLimiter{
tokens: make(chan struct{}, burst),
rate: rate,
}
// Fill tokens initially
for i := 0; i < burst; i++ {
rl.tokens <- struct{}{}
}
// Refill tokens
go func() {
ticker := time.NewTicker(time.Second / time.Duration(rate))
for range ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
}
}
}()
return rl
}
func (rl *RateLimiter) Allow() bool {
select {
case <-rl.tokens:
return true
default:
return false
}
}
Trade-offs:
- Pro: Simple, fast, in-memory
- Con: Không distributed (mỗi instance có limit riêng)
Distributed version: Dùng Redis với sliding window hoặc token bucket.
Q13: Design worker pool với graceful shutdown
Trả lời:
type WorkerPool struct {
workers int
jobs chan func()
wg sync.WaitGroup
quit chan struct{}
}
func NewWorkerPool(workers int) *WorkerPool {
wp := &WorkerPool{
workers: workers,
jobs: make(chan func(), 100),
quit: make(chan struct{}),
}
for i := 0; i < workers; i++ {
wp.wg.Add(1)
go wp.worker()
}
return wp
}
func (wp *WorkerPool) worker() {
defer wp.wg.Done()
for {
select {
case job := <-wp.jobs:
job()
case <-wp.quit:
// Drain remaining jobs
for job := range wp.jobs {
job()
}
return
}
}
}
func (wp *WorkerPool) Submit(job func()) {
select {
case wp.jobs <- job:
case <-wp.quit:
// Pool is shutting down
}
}
func (wp *WorkerPool) Shutdown() {
close(wp.quit)
close(wp.jobs)
wp.wg.Wait()
}
Key points:
- Graceful shutdown: drain remaining jobs
- Context propagation cho cancellation
- Backpressure: bounded channel
Behavioral Questions
Q14: "Kể về lần bạn optimize performance của Go service"
Answer structure (STAR):
Situation: "Service có p99 latency 500ms, target là < 100ms."
Task: "Profile để tìm bottleneck."
Action:
- "Enable pprof, collect CPU profile"
- "Phát hiện 60% CPU trong JSON serialization"
- "Switch từ encoding/json sang easyjson (code generation)"
- "Reduce allocations: pre-allocate slices, reuse buffers với sync.Pool"
- "Add benchmarks để track regressions"
Result: "p99 giảm từ 500ms → 80ms (84% improvement). CPU usage giảm 40%."
Lesson learned: "Measure first, optimize hot paths, benchmark để verify."
Q15: "Kể về bug khó debug nhất liên quan đến Go"
Example answer:
Situation: "Production service có memory leak, RAM tăng từ 500MB → 4GB trong 24h."
Investigation:
- "Heap profile không thấy allocations spike"
- "Goroutine profile → 100k goroutines (normal: 1k)"
- "Trace → goroutines block trên channel receive"
Root cause:
// Goroutine leak
for _, item := range items {
ch := make(chan Result)
go process(item, ch)
// Forgot to receive from ch → goroutine leak
}
Fix:
for _, item := range items {
ch := make(chan Result, 1) // Buffered
go process(item, ch)
<-ch // Receive result
}
Lesson: "Luôn ensure goroutines có exit path. Monitor NumGoroutine() trong production."
Tóm tắt: Cách trả lời tốt
✅ Structure: Situation → Technical details → Trade-offs → Result
✅ Code examples: Show, don't just tell
✅ Production context: Mention real-world constraints (latency targets, scale)
✅ Trade-offs: Nothing is free — discuss pros/cons
✅ Measurement: "I profiled..." not "I guessed..."
✅ Follow-up prepared: Anticipate deeper questions
Tài liệu ôn thêm
- Go Interview Questions: https://github.com/shomali11/go-interview
- Gophercises: https://gophercises.com/
- Effective Go: https://go.dev/doc/effective_go
- Go Code Review Comments: https://go.dev/wiki/CodeReviewComments