🧠 Programming✍️ Khoa📅 19/04/2026☕ 10 phút đọc

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:

  1. Return pointer to local variable
  2. Assign to interface{} (type không biết compile-time)
  3. Send to channel
  4. Closure captures variable và outlive function
  5. 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:

  1. Mark Setup (STW ~100µs): Scan roots (stack, globals)
  2. Concurrent Mark: Trace reachable objects (concurrent với user code)
  3. Mark Termination (STW ~100µs): Finalize marking
  4. 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:

  1. Mutual exclusion
  2. Hold and wait
  3. No preemption
  4. 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:

  1. "Enable pprof, collect CPU profile"
  2. "Phát hiện 60% CPU trong JSON serialization"
  3. "Switch từ encoding/json sang easyjson (code generation)"
  4. "Reduce allocations: pre-allocate slices, reuse buffers với sync.Pool"
  5. "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:

  1. "Heap profile không thấy allocations spike"
  2. "Goroutine profile → 100k goroutines (normal: 1k)"
  3. "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