Chờ trên nhiều channel
select trông giống switch, nhưng mỗi case là một thao tác gửi hoặc nhận trên channel. Nó chặn cho đến khi có một case thực hiện được rồi chạy case đó.
Với độ trễ như trên, select đầu tiên nhận kết quả nhanh và select thứ hai nhận kết quả chậm. Không thao tác nhận nào phải chờ channel còn lại. Nếu viết <-slow rồi đến <-fast, chúng sẽ được xử lý theo thứ tự cố định, bất kể cái nào đến trước.
Cách select đánh giá:
- Mọi biểu thức channel và giá trị cần gửi được tính một lần, theo thứ tự trong mã nguồn, khi
selectbắt đầu. - Nếu có một hoặc nhiều case sẵn sàng, một trong số đó được chọn ngẫu nhiên.
- Nếu không case nào sẵn sàng và có
default,defaultsẽ chạy. - Nếu không, goroutine bị chặn cho đến khi có case sẵn sàng.
Một select {} rỗng chặn mãi mãi. Thỉnh thoảng bạn gặp nó ở cuối main trong những chương trình mà công việc thật diễn ra ở các goroutine khác.
Chọn ngẫu nhiên giữa các case sẵn sàng
Khi nhiều case sẵn sàng cùng lúc, select không ưu tiên case được viết đầu tiên. Chương trình này lấp đầy hai channel có bộ đệm rồi chạy select 1000 lần:
Tỷ lệ giữa countA và countB thay đổi sau mỗi lần chạy và xoay quanh 500 mỗi bên. Việc chọn ngẫu nhiên là có chủ đích: nó ngăn một channel bận rộn làm các channel khác bị bỏ đói. Nếu cần ưu tiên, hãy xem mẫu ở phần dưới.
Thao tác không chặn với default
Có case default, select không bao giờ chặn. Nhờ đó một thao tác gửi hay nhận trở thành thao tác "thử":
Bỏ bớt công việc khi bộ đệm đầy là cách giảm tải hoặc gửi số liệu đo mà không bao giờ làm nơi gọi bị đứng.
Đừng đặt default vào một select nằm trong vòng for chỉ để "kiểm tra" các channel liên tục. Khi không có gì sẵn sàng, vòng lặp quay liên tục và chiếm 100% CPU. Hãy để nó chặn, và thêm một case timeout nếu cần thức dậy định kỳ.
Timeout
time.After(d) trả về một channel nhận được giá trị một lần sau khoảng d. Cho nó chạy đua với công việc thật:
Lần gọi đầu trả về "data". Lần gọi thứ hai trả về lỗi timeout sau 50 ms, rất lâu trước khi worker kịp xong. Channel kết quả có bộ đệm 1 là có chủ đích. Khi timeout thắng, không ai nhận từ result nữa; nếu channel không có bộ đệm, goroutine worker sẽ bị chặn mãi ở thao tác gửi và bị rò rỉ.
Trong vòng lặp, time.After tạo một bộ hẹn giờ mới ở mỗi lần lặp, đúng như cần cho timeout chờ theo từng tin nhắn ("không có tin nhắn trong 1 giây"). Với một deadline chung cho nhiều thao tác, hãy tạo một bộ hẹn giờ hoặc context duy nhất trước vòng lặp. Từ Go 1.23, các bộ hẹn giờ không còn được tham chiếu sẽ được thu gom rác dù chưa kích hoạt, nên time.After trong vòng lặp không còn giữ bộ nhớ cho đến khi từng bộ hẹn giờ kích hoạt như ở các phiên bản cũ (cần khai báo go 1.23 trở lên trong go.mod).
Vòng lặp for-select và kênh quit
Một goroutine chạy cho đến khi được bảo dừng là một vòng for bao quanh select, với một case cho công việc và một case để dừng:
Đóng quit thay vì gửi vào nó là cách viết chuẩn: lệnh đóng được mọi bên nhận nhìn thấy, cả bây giờ lẫn sau này, nên một lệnh close dừng được bao nhiêu worker cũng được. Channel done cho phép main chờ đến khi worker thật sự kết thúc.
Trong code thực tế, kênh quit thường là một context.Context: case <-ctx.Done():. Nó hoạt động y như vậy (Done() trả về một channel bị đóng khi bị hủy) và còn mang theo deadline cùng lý do dừng. Trang về context sẽ giải thích chi tiết.
break bên trong select
break trong một case của select chỉ thoát khỏi select, không thoát khỏi vòng for bao quanh. Đây là nguyên nhân phổ biến khiến vòng lặp không bao giờ kết thúc. Hãy dùng return, hoặc gắn nhãn cho vòng lặp:
Ưu tiên giữa các channel
Vì select chọn ngẫu nhiên, bạn không thể xếp hạng các case trong cùng một câu lệnh. Để một channel luôn thắng mỗi khi có dữ liệu, hãy kiểm tra riêng nó trước:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
select đầu tiên trả về ngay nếu việc hủy đã xảy ra. Nếu không có nó, một dòng công việc đều đặn có thể tiếp tục thắng lượt chọn ngẫu nhiên trong một lúc sau khi ctx đã bị hủy.
Channel nil vô hiệu hóa một case
Gửi hay nhận trên một channel nil không bao giờ sẵn sàng, nên case trên channel nil coi như bị tắt. Gán nil cho một đầu vào đã đóng là cách ngừng select trên nó mà vẫn tiếp tục với các channel khác; trang về channel có một vòng lặp gộp được viết theo cách này. Mẹo này cũng dùng để bật tắt timeout: giữ var timeout <-chan time.Time ở giá trị nil cho đến khi cần, rồi gán time.After(d).
Lỗi thường gặp
- Tưởng select theo thứ tự trong mã nguồn. Case viết đầu tiên không được ưu tiên.
- Vòng lặp bận với
default. Mộtfor { select { ... default: } }không có việc gì làm sẽ đốt cạn một nhân CPU. - Rò rỉ bên thua. Khi timeout thắng, goroutine lẽ ra gửi kết quả vẫn phải kết thúc được. Hãy cho channel của nó bộ đệm 1.
breakchỉ thoát khỏiselect. Dùng nhãn hoặcreturn.- Mỗi lần lặp một
time.Afternhưng lại dùng làm deadline chung. Nó bắt đầu lại ở mỗi lần lặp; hãy tạo deadline một lần, bên ngoài vòng lặp.
Câu hỏi thường gặp
select trong Go dùng để làm gì?
select chờ đến khi một trong các thao tác channel của nó (gửi hoặc nhận) có thể thực hiện, rồi chạy case đó. Nếu nhiều case sẵn sàng cùng lúc, nó chọn ngẫu nhiên một case. Không có case default thì nó chặn cho đến khi có case sẵn sàng; có default thì nó không bao giờ chặn.
Làm sao thêm timeout khi nhận từ channel trong Go?
Đặt thao tác nhận và một bộ hẹn giờ vào cùng một select: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. Việc nào xảy ra trước thì thắng. Trong vòng lặp, hoặc khi nơi gọi đã có sẵn deadline, hãy dùng context.Context với context.WithTimeout và select trên ctx.Done().
select trong Go có chọn case theo thứ tự không?
Không. Khi có nhiều case sẵn sàng, Go chọn ngẫu nhiên đều nhau, nên không case nào làm các case khác bị bỏ đói. Nếu cần ưu tiên, hãy kiểm tra channel ưu tiên cao trước trong một select riêng có default, rồi mới đến select trên tất cả channel.
Vì sao break không thoát khỏi vòng lặp for-select?
Bên trong select, break chỉ thoát khỏi câu lệnh select, không thoát khỏi vòng for bao quanh. Hãy dùng return, hoặc gắn nhãn cho vòng lặp (loop: for { select { case <-done: break loop } }).