Bước 1 — xác nhận bằng số liệu. Xuất runtime.NumGoroutine() thành metric (Prometheus client_golang có sẵn go_goroutines). Leak có hình dạng đặc trưng: đường goroutine tăng đơn điệu theo lượng request và không tụt về nền khi hết tải. Nếu nó lên xuống theo traffic thì đó là tải cao, không phải leak.
Bước 2 — chụp goroutine profile. Nhúng net/http/pprof vào một cổng admin nội bộ (không mở ra ngoài):
import _ "net/http/pprof"
go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()# grouped stacks, easiest to eyeball
curl -s localhost:6060/debug/pprof/goroutine?debug=1 > g1.txt
# full per-goroutine stacks, includes how long each has been blocked
curl -s localhost:6060/debug/pprof/goroutine?debug=2 > g2.txt
go tool pprof -http=: localhost:6060/debug/pprof/goroutineBước 3 — đọc profile. Chỗ leak lộ ra ngay: một stack duy nhất chiếm hàng nghìn goroutine, tất cả dừng ở cùng một dòng với trạng thái chan send, chan receive, select hoặc semacquire. Bản debug=2 còn ghi số phút mỗi goroutine đã chặn — thứ chặn hàng giờ gần như chắc chắn là leak. Chụp hai profile cách nhau vài phút rồi so sánh sẽ tách được leak khỏi việc đang chạy bình thường.
Bước 4 — chặn tái phát trong test. go.uber.org/goleak kiểm tra không còn goroutine lạ sau khi test kết thúc:
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}Khi đã có ngăn xếp lỗi, cách sửa gần như luôn nằm trong ba hướng: cấp buffer để bên gửi không phải chờ, thêm nhánh <-ctx.Done() vào mọi thao tác channel, và bảo đảm close được gọi trên mọi đường thoát.