Go Zero to Hero - Bài 57: Đóng Channel (close) và duyệt Channel bằng for-range
Trong lập trình đồng thời (concurrency) với Golang, việc báo hiệu kết thúc công việc giữa các Goroutines là cực kỳ quan trọng. Hàm close() và vòng lặp for range chính là bộ đôi hoàn hảo để giải quyết bài toán này.
1. Hàm close() và Nguyên tắc vàng của Gopher
Hàm close(ch) được dùng để báo hiệu rằng: Sẽ không có bất kỳ dữ liệu nào được gửi thêm vào cái Channel này nữa.
Nguyên tắc sinh tử (Đóng dấu đỏ):
- CHỈ CÓ NGƯỜI GỬI (Sender) mới được phép đóng Channel. NGƯỜI NHẬN (Receiver) tuyệt đối không bao giờ được đóng.
- Tại sao? Vì nếu người nhận tự ý đóng ống, mà người gửi ở bên ký vẫn tiếp tục cố nhét dữ liệu vào một cái ống đã bị khóa van, chương trình của bạn sẽ bị
PANIC(Sập server) ngay tắp lự.
2. Rút cạn ống nước với val, ok := <-ch
Khi một ống bị đóng, điều kỳ diệu của Go là: Các dữ liệu CÒN TỒN ĐỌNG bên trong ống (đối với Buffered Channel) vẫn sẽ nằm nguyên đó cho người nhận từ từ lấy ra hết.
Nhưng làm sao người nhận phân biệt được đâu là "Dữ liệu mang giá trị 0" và đâu là "Ống đã bị đóng"? Chúng ta dùng lại thủ thuật val, ok quen thuộc.
package main
import "fmt"
func main() {
// Ống chứa tối đa 3 chiếc vé
ch := make(chan string, 3)
ch <- "Vé 01"
ch <- "Vé 02"
// Đóng ống ngay lập tức (Người gửi quyết định)
close(ch)
// 🚨 KÍCH HOẠT PANIC NẾU CỐ GỬI THÊM:
// ch <- "Vé 03" // Sẽ báo lỗi "send on closed channel"
// RÚT DỮ LIỆU
for {
ticket, ok := <-ch
if !ok {
// Khi ok == false, nghĩa là ống đã ĐÓNG và TRỐNG RỖNG
fmt.Println("🚫 Ống đã đóng và cạn kiệt. Dừng công việc!")
break
}
fmt.Println("Đang xử lý:", ticket)
}
}
Kết quả in ra:
Đang xử lý: Vé 01
Đang xử lý: Vé 02
🚫 Ống đã đóng và cạn kiệt. Dừng công việc!
3. Vòng lặp thần thánh: for range trên Channel
Viết vòng lặp vô tận for { ... } rồi kẹp thêm lệnh if !ok { break } trông khá cồng kềnh. Cha đẻ của Go đã ưu ái thiết kế riêng một cú pháp for range dành riêng cho Channel để code ngắn gọn đi một nửa.
Vòng lặp for range sẽ liên tục rút dữ liệu từ ống ra. Nó tự động Block chờ nếu ống chưa có đồ, và tự động break (thoát vòng lặp) khi ống bị đóng.
Hãy xem kịch bản thực chiến: Một Worker chuyên đẩy file log lên Cloud.
package main
import (
"fmt"
"time"
)
// Luồng tạo dữ liệu (Người gửi)
func logGenerator(ch chan<- int) {
fmt.Println("[Generator] Bắt đầu gom log...")
for i := 1; i <= 5; i++ {
ch <- i // Đẩy từng lô log vào ống
time.Sleep(500 * time.Millisecond)
}
fmt.Println("[Generator] Đã gom xong. Tiến hành đóng van!")
close(ch) // Rất quan trọng! Nếu không close, Consumer sẽ bị kẹt vĩnh viễn
}
func main() {
logQueue := make(chan int, 10)
// Khởi động luồng gom log
go logGenerator(logQueue)
fmt.Println("[Consumer] Luồng xử lý bắt đầu trực ban...")
// CÚ PHÁP THẦN THÁNH: for range
// Vòng lặp này sẽ tự động TẮT khi logQueue bị close()
for logBatch := range logQueue {
fmt.Printf("[Consumer] ☁️ Đã đẩy lô log số %d lên Cloud\n", logBatch)
}
fmt.Println("[Consumer] Hết việc, tắt máy đi ngủ!")
}
Nhờ for range, luồng Consumer không cần biết khi nào luồng Generator gửi xong. Nó cứ tận tụy làm việc, và tự động "nghỉ hưu" khi cái ống nước tuyên bố đóng cửa. Đây là mô hình Producer-Consumer kinh điển và an toàn nhất trong kiến trúc Backend.
4. Những cái bẫy PANIC chí mạng với Channel
Làm việc với Channel, nếu bạn sơ sẩy để xảy ra 2 tình huống sau, Go sẽ không thương xót mà đánh sập luôn cả cái Server của bạn:
- Gửi dữ liệu vào một ống đã đóng (Send on closed channel): Như đã nhắc ở phần 1, ống đóng rồi mà cứ nhét đồ vào là "bùm".
- Đóng một cái ống đã bị đóng trước đó (Close of closed channel): Bạn gọi
close(ch)2 lần trên cùng một biến. Điều này thường xảy ra khi có nhiều Goroutine (nhiều Sender) cùng cố gắng đóng chung một cái ống.
Tuyệt kỹ phòng tránh:
- Chỉ dùng
close()khi bạn có ĐÚNG 1 Sender. - Nếu hệ thống của bạn có N Sender và 1 Receiver (Nhiều máy quét vé cùng gửi dữ liệu về 1 Server), ĐỪNG BAO GIỜ đóng Channel từ phía các Sender. Hãy dùng
sync.WaitGroupđể đếm xem bao giờ tất cả các Sender làm xong việc, rồi mới cho một luồng trung gian gọiclose().
Tổng kết
Đóng ống và duyệt ống là thao tác sinh tử để hệ thống của bạn không bị "lệch nhịp" bộ nhớ:
- Ai đóng? Chỉ có Người Gửi (Sender) mới có quyền gọi
close(ch). - Duyệt ống rảnh tay: Luôn dùng
for item := range chcho Người Nhận (Receiver) để xử lý liên tục và tự động thoát khi ống đóng. - Lấy sạch tồn kho: Ống dù đã đóng thì dữ liệu tồn bên trong vẫn có thể được lấy ra trọn vẹn. Gọi lấy dữ liệu trên ống đã cạn sẽ nhận về Zero Value và
ok = false.
All rights reserved