Khai báo một channel
Channel được khai báo bằng từ khóa chan theo sau là kiểu dữ liệu mà nó truyền tải:
func main() {
var ch chan string
fmt.Println(ch)
}
$ go run main.go
<nil>
Giống như map và slice, giá trị zero của channel là nil — đó chính xác là những gì được in ra ở đây. Một channel nil được khai báo nhưng hoàn toàn không dùng được: bất kỳ thao tác gửi hay nhận nào trên nó cũng sẽ block mãi mãi.
Cẩn thận với channel nil: gửi vào (hoặc nhận từ) một channel nil sẽ block mãi mãi. Nếu tất cả goroutine trong chương trình đều bị block theo kiểu này, runtime sẽ báo
fatal error: all goroutines are asleep - deadlock!. Lưu ý đây là một lỗi fatal, không phải panic thông thường — nó không thể bắt được bằngrecover(); chương trình đơn giản là chết luôn.
Để có một channel dùng được, hãy khởi tạo nó bằng hàm dựng sẵn make:
func main() {
ch := make(chan string)
fmt.Println(ch)
}
$ go run main.go
0x1400010e060
Lần này, kết quả in ra là một địa chỉ bộ nhớ (địa chỉ của bạn sẽ khác), vì — cũng giống map và slice — channel là một tham chiếu đến cấu trúc dữ liệu bên dưới được tạo ra bởi make.
Gửi và nhận
Toán tử channel <- đảm nhiệm toàn bộ công việc, và hướng của mũi tên cho bạn biết điều gì đang diễn ra: ch <- value gửi vào channel, còn value := <-ch nhận ra từ channel.
func speak(arg string, ch chan string) {
ch <- arg // Send
}
func main() {
ch := make(chan string)
go speak("Hello World", ch)
data := <-ch // Receive
fmt.Println(data)
}
$ go run main.go
Hello World
Ở đây main khởi chạy speak như một goroutine, rồi ngay lập tức thử nhận từ ch. Thao tác nhận block cho đến khi goroutine gửi "Hello World" vào channel; khi giá trị đến nơi, main in nó ra rồi kết thúc. Chú ý là không có time.Sleep nào ở đây — chúng ta sẽ thấy chính xác tại sao điều này vẫn hoạt động đáng tin cậy ở phần nâng cao bên dưới.
Buffered channel
Mặc định, capacity của một channel là 0 — nó unbuffered (không có buffer), nghĩa là nó không thể giữ giá trị nào đang "trên đường đi" và mỗi lần gửi phải được khớp ngay bởi một lần nhận. Truyền thêm tham số thứ hai cho make sẽ cấp cho channel một buffer:
func main() {
ch := make(chan string, 2)
go speak("Hello World", ch)
go speak("Hi again", ch)
data1 := <-ch
fmt.Println(data1)
data2 := <-ch
fmt.Println(data2)
}
Với capacity 2, cả hai goroutine đều có thể gửi chuỗi của mình vào buffer mà không cần chờ ai nhận. Quy tắc rất đơn giản:
- Thao tác gửi chỉ block khi buffer đã đầy.
- Thao tác nhận chỉ block khi buffer đang rỗng.
Vậy nên một buffered channel giúp tách rời sender và receiver — trong giới hạn dung lượng buffer. Khi buffer đầy, việc gửi lại bắt đầu block, giống như một unbuffered channel.
Directional channel (channel có hướng)
Một biến channel có thể bị giới hạn chỉ để gửi hoặc chỉ để nhận, điều này hữu ích nhất trong signature của hàm — nó ghi rõ ý định và để compiler tự động kiểm tra:
func speak(arg string, ch chan<- string) {
ch <- arg // Send only
}
Ở đây chan<- string khai báo một channel chỉ để gửi (send-only): speak có thể đẩy giá trị vào nhưng không bao giờ đọc lại được. Ngược lại, <-chan string, khai báo một channel chỉ để nhận (receive-only).
Đóng một channel
Một channel có thể được đóng bằng hàm dựng sẵn close, báo hiệu rằng sẽ không còn giá trị nào được gửi vào nó nữa. Receiver có thể phát hiện điều này bằng dạng nhận hai giá trị ("comma ok"):
func main() {
ch := make(chan string, 2)
ch <- "Hello"
close(ch)
data1, ok := <-ch
fmt.Println(data1, ok)
data2, ok := <-ch
fmt.Println(data2, ok)
}
Lần nhận đầu tiên nhận được Hello true — vẫn còn giá trị trong buffer, nên ok là true. Lần nhận thứ hai nhận được một chuỗi rỗng (giá trị zero) và ok == false, nghĩa là channel đã đóng và đã cạn. Cờ ok đó là cách duy nhất đáng tin cậy để phân biệt "ai đó gửi cho tôi giá trị zero" với "channel không còn gì để cho nữa."
Các tính chất cần ghi nhớ của channel
Bốn quy tắc sau giải thích gần như mọi bug liên quan đến channel mà bạn sẽ gặp phải:
- Gửi vào channel nil sẽ block mãi mãi — dẫn đến deadlock nếu không còn gì khác có thể tiếp tục chạy.
- Nhận từ channel nil sẽ block mãi mãi — tương tự.
- Gửi vào channel đã đóng sẽ panic —
panic: send on closed channel. - Nhận từ channel đã đóng trả về giá trị zero ngay lập tức — sau khi buffer đã cạn, và nó không bao giờ block.
Tính chất số 4 trong thực tế:
func main() {
var c = make(chan int, 2)
c <- 5
c <- 4
close(c)
for i := 0; i < 4; i++ {
fmt.Printf("%d ", <-c)
}
}
$ go run main.go
5 4 0 0
Hai lần nhận đầu tiên rút cạn các giá trị đã buffer là 5 và 4. Channel đã đóng, nên lần nhận thứ ba và thứ tư không block — chúng trả về 0 ngay lập tức, giá trị zero của kiểu int. Không có panic, không có lỗi; nếu bạn không lường trước những số 0 đó, hành vi âm thầm này có thể che giấu bug, đó là lý do dạng comma-ok (hoặc range) thường tốt hơn so với việc nhận "mù."
Nhắc đến range — một vòng lặp for range trên channel sẽ nhận giá trị cho đến khi channel bị đóng và cạn:
func main() {
ch := make(chan string, 2)
ch <- "Hello"
ch <- "World"
close(ch)
for data := range ch {
fmt.Println(data)
}
}
$ go run main.go
Hello
World
Vòng lặp lấy ra Hello rồi World, sau đó thấy channel đã đóng và thoát ra gọn gàng. Dòng close(ch) đang thực sự làm việc ở đây — nếu không có nó, vòng lặp sẽ tiếp tục chờ giá trị thứ ba không bao giờ đến.
Channel nâng cao
Unbuffered channel là điểm hẹn đồng bộ hóa (synchronization rendezvous). Trên một unbuffered channel, việc gửi không chỉ đơn thuần là truyền dữ liệu — nó block cho đến khi có receiver sẵn sàng, và việc trao tay diễn ra ngay tại thời điểm đó. Đây là lý do ví dụ goroutine speak ở trên hoạt động mà không cần bất kỳ time.Sleep nào: thao tác nhận của main và thao tác gửi của goroutine đồng bộ hóa với nhau. Không bên nào có thể "chạy trước" bên kia; bản thân việc nhận trong main chính là sự đảm bảo rằng message của goroutine không bị mất. Vì vậy, unbuffered channel cho bạn cả giao tiếp lẫn đồng bộ hóa trong cùng một primitive.
Ai nên đóng channel? Luôn luôn là sender. Chỉ sender mới biết khi nào không còn gì để gửi nữa, và một receiver mà đóng channel có nguy cơ gây ra panic send on closed channel ở phía bên kia. Cũng cần nhớ rằng việc đóng là tùy chọn — channel không bị leak nếu bạn không bao giờ đóng nó; bạn chỉ cần close khi receiver cần được báo "hết rồi" (ví dụ, để kết thúc một vòng lặp range). Hai quy tắc cứng:
close(ch)
close(ch) // ❌ panic: close of closed channel
var nilCh chan int
close(nilCh) // ❌ panic: close of nil channel
range trên channel chỉ kết thúc nếu channel bị đóng. ⚠️ Nếu không có goroutine nào gọi close, vòng lặp sẽ block chờ giá trị tiếp theo, và một khi không còn goroutine nào khác có thể gửi, runtime sẽ giết chương trình với fatal error: all goroutines are asleep - deadlock!. Quên đóng channel trước khi range là một trong những bug channel phổ biến nhất.
Chuyển đổi hướng chỉ là con đường một chiều. Một channel hai chiều chuyển đổi ngầm định sang channel có hướng — đó chính là điều xảy ra khi bạn truyền make(chan string) cho một tham số kiểu chan<- string. Chiều ngược lại không bao giờ được phép: một khi channel đã là chỉ-để-gửi hoặc chỉ-để-nhận, bạn không thể lấy lại chiều còn lại. Điều này khiến tham số có hướng trở thành một giới hạn năng lực thực sự, chứ không chỉ là tài liệu mô tả.
Channel nil thực ra rất hữu ích — trong select. Vì các thao tác trên channel nil block mãi mãi, việc gán một biến channel thành nil bên trong vòng lặp select sẽ vô hiệu hóa case đó mà không cần xóa nó khỏi code. Đây là một mẹo tiêu chuẩn để tắt một nguồn dữ liệu khi nó đã cạn. (Nói thêm về select ở bài tiếp theo.)
Buffered channel như một semaphore. Hành vi block của buffered channel khiến nó trở thành một semaphore đếm tự nhiên để giới hạn mức độ concurrency:
sem := make(chan struct{}, 3) // tối đa 3 worker cùng lúc
for _, task := range tasks {
sem <- struct{}{} // chiếm một slot (block nếu đã có 3 worker bận)
go func(t Task) {
defer func() { <-sem }() // giải phóng slot
process(t)
}(task)
}
len và cap cũng hoạt động trên channel. len(ch) báo cho biết hiện có bao nhiêu giá trị đang nằm trong buffer, còn cap(ch) báo dung lượng của buffer. Chúng khá hữu ích để debug và đo lường — nhưng đừng xây dựng logic đồng bộ hóa dựa trên len, vì kết quả có thể thay đổi ngay giữa lúc bạn hỏi và lúc bạn hành động dựa trên nó.
DUKE
Bình luận