select là switch cho channel: chờ nhiều channel cùng lúc và xử lý cái nào sẵn sàng trước.
Cơ bản
select {
case v := <-a:
fmt.Println("nhận", v)
case v := <-b:
fmt.Println("nhận", v)
}
select chặn tới khi có ít nhất một nhánh sẵn sàng. Nhánh cũng có thể là lệnh gửi:
select {
case ch <- v:
// gửi được
case <-ctx.Done():
return ctx.Err()
}
Nhiều nhánh sẵn sàng: ngẫu nhiên
1000 lượt: x=477 y=523
Khi nhiều nhánh cùng sẵn sàng, Go chọn ngẫu nhiên đều. Đây là quyết định thiết kế, giống việc xáo trộn thứ tự duyệt map ở bài 10.
Lý do: nếu Go chọn theo thứ tự viết, nhánh đầu tiên sẽ luôn thắng và nhánh sau chết đói. Ngẫu nhiên bảo đảm mọi nhánh đều có cơ hội.
Hệ quả: đừng viết mã dựa vào thứ tự nhánh. Cần ưu tiên thì phải làm tường minh:
select {
case v := <-uuTien:
xuLy(v)
default:
select {
case v := <-uuTien: xuLy(v)
case v := <-thuong: xuLy(v)
}
}
default: không chặn
không có gì, đi tiếp ngay
Có default, select không bao giờ chặn — không nhánh nào sẵn sàng thì chạy default ngay.
Hai công dụng chính:
// thử nhận, không chờ
select {
case v := <-ch: xuLy(v)
default:
}
// thử gửi, bỏ qua nếu đầy — mẫu "vứt khi quá tải"
select {
case ch <- v:
default:
metrics.Bo++
}
Mẫu thứ hai hữu ích cho log hoặc số liệu giám sát: thà mất một điểm dữ liệu còn hơn chặn đường xử lý chính.
Hết giờ
select {
case v := <-ch:
xuLy(v)
case <-time.After(40 * time.Millisecond):
return errors.New("hết giờ")
}
hết giờ sau 40ms
Một cảnh báo về time.After: nó tạo một timer không được thu hồi cho tới khi hết hạn. Trong vòng lặp chạy nhiều lần, đó là rò rỉ:
for {
select {
case v := <-ch: xuLy(v)
case <-time.After(time.Second): // tạo timer MỚI mỗi vòng
}
}
Với vòng lặp nóng, dùng time.NewTimer và Reset, hoặc tốt hơn là dùng context.WithTimeout bên ngoài vòng lặp.
Từ Go 1.23, bộ thu gom rác dọn được timer chưa hết hạn nên vấn đề này nhẹ đi nhiều — nhưng thói quen vẫn nên giữ.
Mẹo: gán channel về nil để tắt nhánh
Bài 32 đã nói channel nil chặn vĩnh viễn. Trong select, điều đó nghĩa là nhánh ấy không bao giờ được chọn — và đó là cách tắt nó:
for con > 0 {
select {
case v, ok := <-d1:
if !ok { d1 = nil; con--; continue } // TẮT nhánh này
xuLy(v)
case v, ok := <-d2:
if !ok { d2 = nil; con--; continue }
xuLy(v)
}
}
d1: 1
d1 cạn -> tắt nhánh
d2: 2
d2 cạn -> tắt nhánh
Không có mẹo này, channel đã đóng sẽ luôn sẵn sàng (trả zero value ngay), và select sẽ quay tít trong vòng lặp bận.
Đây là mẫu chuẩn để hợp nhất nhiều nguồn có số lượng khác nhau, và nó là lý do chính khiến channel nil tồn tại.
select rỗng và vòng lặp vô hạn
select {} // chặn vĩnh viễn
Dùng ở cuối main khi mọi việc chạy trong goroutine. Nhưng nó cũng gây fatal error: all goroutines are asleep nếu không còn goroutine nào chạy — nên trong dịch vụ thật, hãy chờ tín hiệu hệ điều hành thay vì select{}. Bài 58 sẽ nói.
Mẫu kết hợp với context
Đây là mẫu bạn sẽ viết nhiều nhất trong dịch vụ:
for {
select {
case <-ctx.Done():
return ctx.Err()
case viec, ok := <-hangDoi:
if !ok { return nil }
xuLy(viec)
}
}
Nhánh ctx.Done() luôn ở đầu theo quy ước — dù thứ tự không ảnh hưởng chức năng, nó giúp người đọc thấy ngay vòng lặp này dừng được.
Thử ba mươi giây
ch := make(chan int)
close(ch)
for i := 0; i < 3; i++ {
select {
case v := <-ch: fmt.Println("nhận", v)
}
}
In ra nhận 0 ba lần, tức thì. Channel đã đóng luôn sẵn sàng — và nếu đây là vòng lặp vô hạn, CPU của bạn lên 100%.
Ba mươi giây đó giải thích vì sao mẹo gán nil tồn tại.
Ngày mai: sync.Mutex — khi nào khoá thắng channel.