Lỗi Gặp Phải
Chương trình của bạn bị crash với thông báo:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/home/user/project/main.go:12 +0x58
exit status 2
Runtime của Go đã phát hiện rằng mọi goroutine đều bị chặn — không có gì có thể tiếp tục. Vì vậy nó kết thúc chương trình thay vì treo mãi mãi. Hãy coi đây là Go đang giúp bạn một việc.
Nguyên Nhân
Deadlock xảy ra khi tất cả các goroutine đều bị kẹt, chờ đợi một thứ gì đó sẽ không bao giờ đến. Một channel không có đối tác. Một mutex bị khóa bởi chính goroutine đang cố khóa lại lần nữa. Một WaitGroup mà bộ đếm của nó sẽ không bao giờ về zero.
Những nguyên nhân phổ biến nhất:
- Gửi vào hoặc nhận từ một nil channel
- Chờ đợi trên một channel mà không ai ghi vào (hoặc ngược lại)
- Quên gọi
wg.Done()bên trong goroutine - Khóa một mutex không hỗ trợ reentrant hai lần từ cùng một goroutine
- Unbuffered channel mà cả sender và receiver đều nằm trong cùng một goroutine
Các Cách Khắc Phục
1. Gửi mà không có receiver (hoặc nhận mà không có sender)
Lỗi này sớm muộn gì ai cũng gặp. Bạn ghi vào một channel nhưng không có ai đọc từ nó — hoặc channel là nil:
// SAI: không có goroutine nào đọc từ ch
ch := make(chan int)
ch <- 42 // chặn mãi mãi → deadlock
fmt.Println(<-ch)
Cách sửa: sender và receiver phải chạy trong các goroutine riêng biệt.
ch := make(chan int)
go func() {
ch <- 42
}()
fmt.Println(<-ch) // được giải phóng khi goroutine gửi dữ liệu
Nếu receiver đến sau và bạn chỉ cần buffer một giá trị, buffered channel cũng hoạt động được:
ch := make(chan int, 1) // dung lượng 1
ch <- 42 // trả về ngay lập tức
fmt.Println(<-ch)
2. Quên đóng channel (vòng lặp range bị treo)
// SAI
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
// quên: close(ch)
}()
for v := range ch { // nhận 0–4, rồi chờ mãi mãi
fmt.Println(v)
}
Vòng lặp range trên một channel sẽ tiếp tục cho đến khi channel được đóng. Không có close(ch) — vòng lặp không bao giờ kết thúc.
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // báo hiệu cho range: chúng ta đã xong
}()
3. Bộ đếm WaitGroup không bao giờ về zero
// SAI: nếu processItem panic hoặc return sớm, Done không bao giờ được gọi
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
processItem(n)
// quên wg.Done()
}(i)
}
wg.Wait() // deadlock
Dùng defer wg.Done() ngay ở dòng đầu tiên bên trong goroutine. Nó sẽ chạy ngay cả khi hàm bị panic:
go func(n int) {
defer wg.Done() // đảm bảo luôn được gọi
processItem(n)
}(i)
4. Khóa cùng một mutex hai lần trong một goroutine
sync.Mutex không hỗ trợ reentrant. Khóa nó hai lần từ cùng một goroutine và bạn sẽ bị treo:
// SAI
var mu sync.Mutex
func doWork() {
mu.Lock()
defer mu.Unlock()
doMoreWork() // hàm này cũng gọi mu.Lock() → deadlock
}
func doMoreWork() {
mu.Lock() // goroutine đang giữ lock rồi
defer mu.Unlock()
}
Cấu trúc lại để hàm bên trong giả định rằng lock đã được giữ — không để nó tự gọi Lock():
func doWork() {
mu.Lock()
defer mu.Unlock()
doMoreWorkLocked() // quy ước: hậu tố "Locked" = caller đã giữ lock
}
func doMoreWorkLocked() {
// không có mu.Lock() ở đây
}
5. Nil channel
Gửi vào hoặc nhận từ một nil channel sẽ bị chặn mãi mãi — không panic, chỉ im lặng:
// SAI
var ch chan int // nil
ch <- 1 // chặn mãi mãi
Luôn khởi tạo bằng make:
ch := make(chan int) // unbuffered
ch := make(chan int, 10) // buffered, dung lượng 10
Đọc Hiểu Goroutine Stack Dump
Khi deadlock xảy ra, Go in ra toàn bộ goroutine dump. Đừng bỏ qua nó — nó cho bạn biết chính xác mỗi goroutine đang bị kẹt ở đâu:
goroutine 1 [chan receive]:
main.main()
/home/user/project/main.go:12 +0x58
[chan receive] — đang chặn, chờ nhận từ một channel.
[semacquire] — đang chặn trên một mutex lock.
[sleep] — đang trong time.Sleep (bản thân nó không gây deadlock).
Đối chiếu thẻ trạng thái và số dòng với code của bạn — đó chính là vị trí deadlock.
Cũng nên chạy với race detector, vì data race và deadlock thường xuất hiện cùng nhau:
go run -race main.go
go test -race ./...
Kiểm Tra Sau Khi Sửa
Thoát sạch nghĩa là deadlock đã được giải quyết:
# Phải thoát với code 0, không có output panic
go run main.go
# Kiểm thử + phát hiện race condition
go test -race ./...
# Với chương trình chạy lâu dài, kiểm tra goroutine leak qua pprof
import _ "net/http/pprof"
// curl http://localhost:6060/debug/pprof/goroutine?debug=1
Không có fatal error trong output và exit code là 0 — bạn đã xong.
Checklist Nhanh
- Mỗi lần gửi vào channel đều có receiver chạy trong goroutine khác
- Các channel dùng với
rangephải đượcclose()bởi producer - Mỗi
wg.Add(1)đều códefer wg.Done()tương ứng - Không có goroutine nào khóa cùng một
sync.Mutexhai lần - Không có biến channel nào là nil tại thời điểm gửi/nhận
- Không có
selectnào thiếudefaultcase làm chặn tất cả goroutine cùng lúc
Mẹo Hay
- Bộ phát hiện deadlock của Go chỉ kích hoạt khi tất cả goroutine bị chặn. Nếu ngay cả một goroutine vẫn đang chạy (ví dụ: một vòng lặp spinner), runtime sẽ không phát hiện được. Hãy dùng
pprofđể phát hiện goroutine leak trong trường hợp đó. - Đối với logic hủy bỏ, hãy dùng
context.Contextvới timeout thay vì tự tạo channel protocol riêng. Cách này gọn gàng hơn và tránh goroutine bị leak âm thầm. - Thêm
goleak(github.com/uber-go/goleak) vào test suite của bạn. Nó sẽ làm test thất bại nếu có goroutine nào vẫn đang chạy khi test kết thúc — bắt được leak trước khi lên production.

