Channel là kiểu dữ liệu có cú pháp riêng trong Go — không phải thư viện. Bài này về ngữ nghĩa của nó.

Không đệm: một cuộc bắt tay

ch := make(chan int)          // cap = 0
go func() { time.Sleep(50*time.Millisecond); ch <- 1 }()
<-ch
  người nhận chờ 50ms -> gửi và nhận GẶP NHAU

Channel không đệm không chứa gì cả. Lệnh gửi chặn tới khi có người nhận, và ngược lại. Hai goroutine gặp nhau tại đúng một thời điểm.

Đây là điểm khác BlockingQueue của Java: nó không phải hàng đợi, nó là điểm hẹn. Và vì thế nó cho bạn một bảo đảm mạnh — khi lệnh gửi trả về, bạn biết chắc người nhận đã cầm được giá trị.

Có đệm: hàng đợi

cb := make(chan int, 2)
cb <- 1
cb <- 2       // vẫn không chặn
  gửi 2 vào đệm 2: 0s (không chặn), len=2 cap=2

Gửi chỉ chặn khi đệm đầy; nhận chỉ chặn khi đệm rỗng. len() là số phần tử đang có, cap() là sức chứa.

Chọn kích thước đệm thế nào? Ba nguyên tắc:

Đệm 0 khi bạn cần bảo đảm bên nhận đã xử lý — bàn giao đồng bộ.

Đệm nhỏ cố định để hấp thụ dao động ngắn, ví dụ make(chan Viec, 100).

Đừng dùng đệm rất lớn để "cho chắc". Nó chỉ giấu vấn đề: bên sản xuất nhanh hơn bên tiêu thụ thì đệm nào cũng đầy, chỉ là muộn hơn. Và đệm lớn nghĩa là nhiều dữ liệu bị mất khi chương trình dừng đột ngột. Đây đúng bài học LinkedBlockingQueue không giới hạn ở bài 73 sê-ri Java.

Đóng channel

close(cc)
v, ok := <-cc
  nhận sau close : v=7 ok=true   (còn dữ liệu trong đệm)
  nhận tiếp      : v=0 ok=false  (đã cạn)

Đóng không vứt dữ liệu — người nhận vẫn lấy hết những gì còn trong đệm, rồi mới nhận zero value với ok=false.

range trên channel dừng tự động khi đóng:

  1 2 3

Đây là mẫu chuẩn cho bên tiêu thụ:

for v := range ch {
	xuLy(v)
}
// tới đây nghĩa là channel đã đóng và cạn

Không đóng channel thì vòng range này chặn vĩnh viễn — và đó là nguồn rò rỉ goroutine phổ biến nhất, bài 38 sẽ nói.

Bốn thao tác gây panic

  gửi vào channel đã đóng  -> panic: send on closed channel
  đóng channel đã đóng     -> panic: close of closed channel

Cộng hai cái nữa: đóng channel nil, và gửi vào channel nil (cái sau chặn vĩnh viễn chứ không panic).

close là thao tác nguy hiểm, Go có một quy ước rất chặt:

Chỉ bên GỬI được đóng channel, và chỉ khi có đúng một bên gửi. Bên nhận không bao giờ đóng. Nhiều bên gửi thì không ai đóng — dùng sync.WaitGroup để biết khi nào xong rồi mới đóng ở một goroutine điều phối.

Lý do: bên nhận không biết bên gửi còn gửi nữa không, nên đóng từ phía nhận sẽ gây panic cho bên gửi.

Mẫu chuẩn cho nhiều bên gửi:

var wg sync.WaitGroup
for i := 0; i < n; i++ {
	wg.Add(1)
	go func() { defer wg.Done(); ch <- lam() }()
}
go func() { wg.Wait(); close(ch) }()   // goroutine riêng chỉ để đóng
for v := range ch { ... }

Channel một chiều

func sanXuat(out chan<- int)   // chỉ gửi
func tieuThu(in <-chan int)    // chỉ nhận

Mũi tên trong kiểu giới hạn hướng. Trình biên dịch chặn nếu bạn nhận từ chan<-.

Đây là công cụ tài liệu tốt: nhìn chữ ký là biết hàm này sản xuất hay tiêu thụ. Và nó chặn luôn việc đóng channel từ phía nhận — close trên <-chan là lỗi biên dịch.

Channel nil chặn vĩnh viễn

var c chan int
<-c        // chặn mãi mãi

Nghe vô dụng, nhưng nó là mẹo quan trọng trong select — bài mai sẽ dùng: gán một channel về nil để "tắt" nhánh đó.

Deadlock được phát hiện

Nếu mọi goroutine đều chặn, Go phát hiện và báo:

  fatal error: all goroutines are asleep - deadlock!

Đây là fatal error, không recover được. Nó chỉ bắt được trường hợp toàn bộ chương trình đứng — nếu còn một goroutine chạy (kể cả vòng lặp vô ích), Go không báo gì.

Thử ba mươi giây

func main() {
	ch := make(chan int)
	ch <- 1
}
  fatal error: all goroutines are asleep - deadlock!

Channel không đệm cần người nhận đang chờ. Thêm make(chan int, 1) và nó chạy. Ba mươi giây đó là khác biệt giữa điểm hẹn và hàng đợi.

Ngày mai: select — chờ nhiều channel cùng lúc.