Go Zero to Hero - Bài 56: Câu lệnh select - Bộ điều hướng Channels
Nếu câu lệnh switch dùng để rẽ nhánh dựa trên giá trị của biến, thì câu lệnh select dùng để rẽ nhánh dựa trên trạng thái của các Channels. select cho phép một Goroutine đứng ở ngã tư đường, lắng nghe (hoặc gửi) dữ liệu trên NHIỀU Channel cùng một lúc.
1. Bản chất của select (Bộ điều hướng Multiplexer)
select sẽ Block (khóa luồng) cho đến khi có ít nhất một Channel sẵn sàng để giao dịch.
Quy luật sinh tử của select:
- Mỗi một
casebên trongselectBẮT BUỘC phải là một thao tác gửi (ch <-) hoặc nhận (<-ch) Channel. - Nếu không có Channel nào sẵn sàng,
selectsẽ chờ mãi mãi. - Nếu có một Channel sẵn sàng,
selectsẽ thực thi khối code củacaseđó. - Nếu có nhiều Channel cùng sẵn sàng một lúc,
selectsẽ chọn mộtcaseHOÀN TOÀN NGẪU NHIÊN để thực thi. (Tính năng này giúp tránh tình trạng "đói tài nguyên" - Starvation, nơi một Channel quá nhanh chiếm hết CPU của các Channel khác).
2. Thực chiến: Trạm kiểm soát đa kết nối (RFID & QR Code)
Hãy viết logic cho một cổng xoay AFC. Khách hàng có thể quẹt thẻ cứng hoặc giơ mã QR trên điện thoại. Cổng xoay phải xử lý bất cứ thiết bị nào gửi dữ liệu về trước.
package main
import (
"fmt"
"time"
)
// Giả lập thiết bị quét thẻ từ
func rfidScanner(ch chan<- string) {
time.Sleep(2 * time.Second) // Quét tốn 2s
ch <- "THẺ-RFID-999"
}
// Giả lập camera quét mã QR
func qrScanner(ch chan<- string) {
time.Sleep(1 * time.Second) // Quét QR tốn 1s
ch <- "MÃ-QR-XYZ"
}
func main() {
rfidChan := make(chan string)
qrChan := make(chan string)
go rfidScanner(rfidChan)
go qrScanner(qrChan)
fmt.Println("[Cổng xoay] Đang chờ hành khách quét vé...")
// Lệnh select sẽ chặn luồng main lại, chờ 1 trong 2 ống nước có dữ liệu
select {
case cardID := <-rfidChan:
fmt.Println("✅ [Thành công] Đã nhận tín hiệu từ thẻ từ:", cardID)
case qrCode := <-qrChan:
fmt.Println("✅ [Thành công] Đã nhận tín hiệu từ mã vạch:", qrCode)
}
fmt.Println("[Cổng xoay] Xử lý xong, trở về trạng thái chờ.")
}
Khi chạy đoạn code này, qrScanner nhanh hơn (1s) nên case qrCode sẽ "thắng" và được thực thi. Luồng chính thoát khỏi select và bỏ qua luôn tín hiệu của rfidChan.
3. Tuyệt kỹ Backend: Pattern Timeout (Ngắt kết nối quá hạn)
Đây là ứng dụng đáng giá ngàn vàng của select. Trong kiến trúc Microservices, đừng bao giờ để một API bị treo vĩnh viễn chỉ vì Server Database hoặc Service bên thứ 3 không phản hồi.
Chúng ta sẽ kết hợp select với hàm time.After(). Hàm này trả về một Channel, và nó sẽ tự động gửi một tín hiệu vào Channel đó sau một khoảng thời gian bạn quy định.
func callPaymentGateway(ch chan<- string) {
// Giả lập cổng thanh toán bị lag nặng, mất 5 giây mới xử lý xong
time.Sleep(5 * time.Second)
ch <- "GIAO_DỊCH_THÀNH_CÔNG"
}
func main() {
resultChan := make(chan string)
go callPaymentGateway(resultChan)
fmt.Println("Đang gọi cổng thanh toán VNPay...")
select {
// Đợi kết quả từ cổng thanh toán
case result := <-resultChan:
fmt.Println("Giao dịch hoàn tất:", result)
// Đặt chốt chặn Timeout: 3 giây
// Nếu quá 3 giây mà resultChan chưa trả về, case này sẽ được kích hoạt!
case <-time.After(3 * time.Second):
fmt.Println("🚨 [TIMEOUT] Cổng thanh toán quá tải! Hủy giao dịch, hoàn tiền cho khách.")
}
}
Bằng mẫu thiết kế (Pattern) cực kỳ thanh lịch này, API của bạn được bảo vệ tuyệt đối khỏi các sự cố treo (Hanging) hệ thống. Luồng xử lý vé sẽ tự động bị cắt đứt sau 3 giây và trả về báo lỗi cho Frontend.
4. Chế độ Non-blocking với default
Như đã nói, select mặc định là Blocking (khóa luồng). Nhưng nếu bạn muốn thiết kế một cấu trúc: "Thử kiểm tra xem trong ống có dữ liệu không, có thì lấy, không có thì bỏ đi làm việc khác ngay lập tức, không chờ đợi!"
Chỉ cần ném thêm nhánh default vào cuối lệnh select, toàn bộ lệnh select đó sẽ biến thành Non-blocking.
func main() {
ch := make(chan string, 1) // Buffered channel
// 🚨 Chúng ta cố tình KHÔNG gửi dữ liệu vào ống
select {
case msg := <-ch:
fmt.Println("Nhận được tin nhắn:", msg)
default:
// Vì ống rỗng, và có nhánh default, select sẽ lập tức nhảy vào đây
// thay vì đứng chờ và gây ra Deadlock!
fmt.Println("Ống đang rỗng. Không rảnh đứng chờ, đi làm việc khác đây!")
}
}
Nhánh default thường được dùng trong các Worker chạy vòng lặp vô tận (for { select { ... } }) để liên tục polling (hỏi thăm) trạng thái của các Channel mà không bắt CPU phải đứng lại.
Tổng kết
Bạn đã nắm trong tay bộ điều khiển giao thông quyền lực nhất của Golang:
- Đa nhiệm:
selectgiúp một Goroutine lắng nghe nhiều Channel cùng lúc mà không bị kẹt ở một Channel nào. - Ngẫu nhiên: Nếu nhiều Channel cùng sẵn sàng, Go chọn ngẫu nhiên để chống chết đói (Starvation).
- Timeout: Kết hợp
selectvàtime.Afterlà "cứu cánh" bắt buộc phải có cho mọi cuộc gọi mạng (Network I/O) hoặc thao tác Database. - Non-blocking: Dùng nhánh
defaultđể biếnselectthành công tắc rẽ nhánh ngay tức thì.
All rights reserved