Goroutine Leak trong Go — Leak chỗ nào, chữa bằng gì
Goroutine rẻ tới mức nhiều người xem nó là… miễn phí. Cái khổ là cái gì cũng có giá: một goroutine leak chỉ là một luồng kẹt mãi, nhưng vài ngàn luồng kẹt thì RAM tụt dần, GC chạy xịt, rồi app chết êm không kịp báo.
Leak trong Go không phải mất bộ nhớ cụ thể — nó là goroutine không bao giờ thoát, và vì thế goroutine giữ nguyên stack + các object nó đang trỏ, cơ mà GC có lượm cỡ nào cũng không bao giờ dọn nổi. Chữa = phải tìm ra chỗ kẹt rồi bịt dòng nó lại.
3 chỗ leak kinh điển
1. Channel không ai đọc / không ai ghi nữa.
func doJob(ctx context.Context) error {
ch := make(chan int)
go func() {
// block đợi ai đó gửi — mà không có ai bao giờ
v := <-ch
_ = v
}()
// ... các dòng khác, ai đó huỷ ctx, nhưng ta quên close(ch)
select {
case <-ctx.Done():
return ctx.Err()
}
}
Goroutine trên đậu ở <-ch vô thời hạn → leak.
2. Vòng for range muôn đời: goroutine chạy select {} hay vòng đọc input không có điều kiện thoát — thường gặp khi quên kiểm tra channel vẫn còn range open.
3. Connection leak chuyển hoá: request rời đi trước khi goroutine kịp trả về kết quả ghi log / gọi API ngoài — goroutine cứ mãi chờ timeout bên dưới.
Rule vàng: goroutine phải biết khi nào dừng
Cách sạch nhất là mọi goroutine dài hơi đều nhận context.Context, và luôn có path thoát: hoặc ctx.Done(), hoặc close(ch) đảm bảo từ phía gửi (nhớ nguyên tắc: only the sender should close), rồi dùng select thay vì block trần:
go func() {
select {
case v := <-ch:
handle(v)
case <-ctx.Done():
return // thoát đúng lúc, không leak
}
}()
Kèm luôn defer để dọn resource trong goroutine, giống hệt main goroutine.
Cách bắt leak trong thực tế
Đừng đoán mò — để runtime tự khai báo. Trong bài test hay endpoint debug, chụp baseline rồi bắt lần hai:
baseline := runtime.NumGoroutine()
// ... chạy logic
if now := runtime.NumGoroutine(); now > baseline+10 {
fmt.Printf("LEAK: %d goroutine dư\n", now-baseline)
}
Còn để in cái stack chỗ nào đang kẹt thì ném endpoint trên net/http/pprof, chạy vài phút rồi:
go tool pprof http://localhost:6060/debug/pprof/goroutine
top cho biết goroutine đậu nhiều nhất ở hàm nào — đó chính là ổ leak. Nghe quen? Đúng kiểu ta hay soi top -c trên Linux khi RAM tăng.
Kinh nghiệm thật
- Leak thường không nổ liền mà lặn mấy giờ — biểu đồ goroutine theo thời gian (Prometheus gauge
go_goroutines) mới là bạn, đừng nhìn snapshot tại một thời điểm. - Worker pool tự "tái chế" goroutine ít nguy cơ hơn là spawn vô tội vạ mỗi lần xử lý item.
- Sau mỗi thay đổi để dài hơi, mình hay chạy một "soak test" nửa tiếng chỉ để nhìn đồ thị goroutine có đi lên không — vài lần nó bắt bài ngay trước khi lên production.
Tóm lại: goroutine không phải đồ rẻ mạt để quăng. Cho nó ctx, cho nó path thoát, và để runtime + pprof chỉ chỗ nó kẹt. Làm đúng thì app "sống lâu" đúng nghĩa đen.