Go: Chuyên sâu cho Production Systems
File này bỏ qua syntax cơ bản. Nếu bạn đã viết Go đủ để làm CRUD, giờ là lúc lên level runtime-aware engineer.
1) Runtime Mental Model
Khi request vào service Go, 3 hệ thống sẽ cùng lúc tác động:
- Scheduler (G-M-P): quyết định goroutine nào được chạy.
- Garbage Collector: quyết định memory nào còn sống.
- Network Poller: đánh thức goroutine đang chờ I/O.
Nếu bạn chỉ nhìn source code mà không hiểu 3 thằng này, tối ưu theo "cảm giác học" rất dễ sai.
G-M-P trong 30 giây
- G (Goroutine): task cần chạy.
- M (Machine): OS thread thực thi.
- P (Processor): token để M được chạy Go code.
GOMAXPROCS ≈ số P. Nếu CPU-bound mà để quá thấp, throughput mất oan. Nếu để quá cao, context switch và contention tăng.
2) Concurrency Patterns: Dùng đúng thuốc, đúng bệnh
Worker Pool có backpressure
Dùng khi có stream jobs dài và cần giới hạn tài nguyên.
package workerpool
import (
"context"
"sync"
)
type Job func(context.Context) error
func Run(ctx context.Context, workers int, jobs <-chan Job) error {
var wg sync.WaitGroup
errCh := make(chan error, workers)
worker := func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
if err := job(ctx); err != nil {
select {
case errCh <- err:
default:
}
return
}
}
}
}
for i := 0; i < workers; i++ {
wg.Add(1)
go worker()
}
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
return nil
case err := <-errCh:
return err
case <-ctx.Done():
return ctx.Err()
}
}
Checklist production:
- Có giới hạn queue size để tránh RAM phình không?
- Có cancel propagation qua
context.Contextkhông? - Có metric queue depth + worker saturation không?
Pipeline có fan-out/fan-in
Dùng khi dữ liệu qua nhiều stage biến đổi. Mỗi stage cần:
- đóng channel đúng lúc,
- xử lý cancel,
- tránh goroutine leak nếu consumer dừng sớm.
Anti-pattern kinh điển: producer vẫn gửi vào channel trong khi consumer đã timeout.
3) Memory và Escape Analysis
Một biến "nhìn như local" nhưng vẫn có thể bị đẩy lên heap nếu compiler thấy nó cần sống sau stack frame.
Kiểm tra bằng:
go build -gcflags='-m=2' ./...
Tín hiệu cần chú ý:
escapes to heapmoved to heap
Không phải escape là xấu. Vấn đề là escape quá nhiều trong hot path gây:
- tăng pressure lên GC,
- tăng p99 latency,
- tăng CPU cho mark/sweep.
Rule thực dụng:
- Tối ưu dữ liệu trong đường nóng (hot loop),
- Không micro-optimize khi chưa có profile.
4) GC và Latency
GC của Go đã rất tốt, nhưng latency-sensitive service vẫn bị ảnh hưởng nếu allocation rate cao.
Theo dõi:
import "runtime"
func SnapshotMem() runtime.MemStats {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m
}
Metrics cần theo dõi:
HeapAlloc,HeapInuseNumGC- p95/p99 latency theo thời gian
Nếu thấy p99 rung cùng nhịp với GC cycles, thử:
- giảm allocations trong request path,
- tái sử dụng buffer có kiểm soát,
- tách object lớn khỏi hot path.
5) Profiling và Benchmark: Tin vào data
CPU/Heap profile với pprof
import _ "net/http/pprof"
import "net/http"
func StartDebugServer() {
go func() {
_ = http.ListenAndServe("127.0.0.1:6060", nil)
}()
}
Lệnh hay dùng:
go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30
go tool pprof http://127.0.0.1:6060/debug/pprof/heap
Benchmark đúng cách
func BenchmarkSerialize(b *testing.B) {
payload := []byte(`{"id":1,"name":"khoa"}`)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = fastPath(payload)
}
}
Nguyên tắc:
- Luôn bật
-benchmemđể thấy allocations. - Chạy benchmark on-demand trong môi trường ổn định.
- So sánh trước/sau bằng công cụ thống kê (
benchstat).
6) Data Races, Deadlocks, Goroutine Leaks
Race detector
go test -race ./...
Race detector không tìm được 100%, nhưng nó bắt được rất nhiều bug "ban ngày".
Goroutine leak pattern
Dấu hiệu:
- số goroutine tăng đều theo uptime,
- memory tăng nhẹ nhưng bền bỉ,
- eventually latency degrade.
Nguyên nhân phổ biến:
- channel không có consumer,
- retry loop không dừng,
- timeout không propagate context.
Fix mindset:
- Mỗi goroutine phải có điều kiện thoát rõ ràng.
- Mỗi I/O call phải có deadline/timeout.
7) API Design cho Package Go (Advanced)
Khi viết package cho team khác dùng:
- Export ít nhất có thể.
- Interface đặt ở phía consumer, không đặt ở package producer một cách vô tội vạ.
- Trả về concrete type khi có thể, interface khi cần abstraction cho caller.
Pattern thuong dung:
type Option func(*Client)
func WithTimeout(d time.Duration) Option {
return func(c *Client) { c.timeout = d }
}
func NewClient(opts ...Option) *Client {
c := &Client{timeout: 2 * time.Second}
for _, opt := range opts {
opt(c)
}
return c
}
Trade-off:
- Functional options linh hoạt,
- nhưng nếu quá nhiều option, API có thể trở nên mờ hồ.
8) Production Checklist (Go Service)
- Có
contexttimeout cho mỗi outbound call. - Có graceful shutdown (
SIGTERM, drain requests). - Có health endpoints (
/live,/ready) tách biệt. - Có metrics: QPS, error rate, latency histogram, goroutines, GC pause.
- Có profiling gate cho production troubleshooting.
- Có load test cho critical paths trước khi release.
Nếu thiếu checklist này, service vẫn chạy... cho đến lúc traffic tăng gấp 5.
9) Interview Deep-Dive — Các câu hỏi hay gặp
- Tại sao goroutine "rẻ" nhưng không "free"?
- Khi nào dùng mutex, khi nào dùng channel?
- Cách debug memory leak trong Go service đang chạy production?
- Làm sao biết bottleneck nằm ở CPU, lock contention hay I/O?
- Nếu p99 tăng sau release, quy trình điều tra của bạn là gì?
Nếu trả lời được các câu này bằng dữ liệu đo được, bạn đã ở level senior thật sự.
10) Tài liệu để đi sâu hơn
- Go Memory Model: https://go.dev/ref/mem
- Effective Go: https://go.dev/doc/effective_go
- Go Blog (scheduler, GC, pprof): https://go.dev/blog/
- High Performance Go (benchmarking notes): https://github.com/dgryski/go-perfbook