Vấn đề của pipeline là stage phía sau có thể dừng sớm (đủ dữ liệu, người dùng huỷ request, lỗi), còn stage phía trước thì vẫn đang chặn ở lệnh gửi.
Muốn dừng sạch, mọi thao tác gửi phải đi kèm một nhánh thoát.
func gen(ctx context.Context, nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
select {
case out <- n:
case <-ctx.Done(): // downstream gave up: exit instead of blocking
return
}
}
}()
return out
}done channel là cơ chế thô: một chan struct{}, ai cũng chỉ đọc, và close(done) phát tín hiệu broadcast tới mọi bên đang đọc cùng lúc. Đơn giản, không phụ thuộc package nào.
context.Context là done channel cộng thêm ba thứ: deadline (tự huỷ khi hết giờ), Err() cho biết lý do (context.Canceled hay DeadlineExceeded), và khả năng lan truyền qua biên API — net/http, database/sql, gRPC đều nhận ctx nên huỷ được lan xuống tận query đang chạy.
Thực tế trong service: dùng context ở mọi hàm có I/O (quy ước ctx là tham số đầu tiên); done channel chỉ còn hợp lý cho helper nội bộ nhỏ, không đi qua biên package nào.