Go Channels là gì? Vì sao cần Channel khi sử dụng Goroutine?
1. Channel là gì? Vì sao cần Channel?

Trong Go, Channel là cơ chế cho phép các Goroutine trao đổi dữ liệu và phối hợp hoạt động với nhau.
Ở bài viết trước(Goroutine & Concurrency), chúng ta đã tìm hiểu Goroutine — một đơn vị thực thi nhẹ được Go runtime quản lý, cho phép chương trình xử lý nhiều công việc theo mô hình concurrency.
Tuy nhiên, khi có nhiều Goroutine cùng thực hiện các công việc khác nhau, một vấn đề xuất hiện: làm thế nào để chúng trao đổi kết quả với nhau?
Ví dụ, Goroutine A thực hiện phép tính 10 + 20, còn Goroutine B cần lấy kết quả để tiếp tục xử lý. Nếu A và B hoạt động độc lập, chúng ta cần một cơ chế để A gửi kết quả và B nhận được kết quả đó.
Channel giải quyết vấn đề này bằng cách cung cấp một phương thức gửi và nhận dữ liệu giữa các Goroutine.

Có thể hình dung Channel như một đường truyền dữ liệu: một Goroutine gửi giá trị vào Channel, còn Goroutine khác nhận giá trị từ Channel.
Cần lưu ý rằng Channel không phải là Goroutine. Goroutine thực hiện công việc, còn Channel hỗ trợ chúng trao đổi dữ liệu và đồng bộ hóa khi cần thiết.
2. Khởi tạo Channel bằng make(chan T)
Trong Go, Channel được tạo bằng hàm dựng sẵn make().
Cú pháp:
ch := make(chan T)
Trong đó:
chlà biến tham chiếu đến Channel.chanlà từ khóa dùng để khai báo kiểu Channel.Tlà kiểu dữ liệu mà Channel có thể truyền tải, chẳng hạnint,stringhoặc một kiểu dữ liệu do người dùng định nghĩa.
Ví dụ:
ch := make(chan int)
Lệnh này tạo một Channel có thể truyền các giá trị kiểu int.
Nếu muốn truyền chuỗi, chúng ta có thể sử dụng:
ch := make(chan string)
Channel có kiểu dữ liệu xác định, vì vậy một Channel kiểu chan int không thể trực tiếp truyền một giá trị string.
Trong ví dụ trên, Channel được tạo mà không chỉ định dung lượng bộ đệm. Đây là unbuffered channel — loại Channel không có bộ đệm, sẽ được giải thích ở phần sau.
3. Gửi và nhận dữ liệu qua Channel
Go sử dụng toán tử <- để thực hiện thao tác gửi và nhận dữ liệu qua Channel.
Gửi dữ liệu
Cú pháp:
ch <- value
Lệnh này gửi giá trị value vào Channel ch.
Nhận dữ liệu
Cú pháp:
value := <-ch
Lệnh này nhận một giá trị từ Channel ch và gán giá trị nhận được cho biến value.
Hãy xem ví dụ hoàn chỉnh sau:
package main
import "fmt"
func calculate(ch chan int) {
result := 10 + 20
ch <- result
}
func main() {
ch := make(chan int)
go calculate(ch)
result := <-ch
fmt.Println(result)
}
Kết quả:
30
Quá trình hoạt động diễn ra như sau:
- Hàm
main()tạo một Channel kiểuchan int. - Câu lệnh
go calculate(ch)khởi chạy hàmcalculate()trong một Goroutine. - Goroutine này tính tổng hai số và gửi kết quả
30vào Channel bằngch <- result. - Trong khi đó, Goroutine thực hiện
main()nhận dữ liệu quaresult := <-ch. - Sau khi nhận được kết quả, chương trình in ra
30.
Trong ví dụ này, Channel đóng vai trò trung gian để Goroutine thực hiện phép tính gửi kết quả cho Goroutine đang chạy main().
4. Channel phối hợp các Goroutine như thế nào?
Channel không chỉ truyền dữ liệu mà còn có thể giúp các Goroutine phối hợp thời điểm thực hiện công việc.
Với unbuffered channel, thao tác gửi và nhận cần gặp nhau để hoàn tất. Nếu chưa có bên nhận, Goroutine gửi sẽ chờ. Nếu chưa có bên gửi, Goroutine nhận cũng sẽ chờ.
Ví dụ:
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "Hello from Goroutine"
}()
message := <-ch
fmt.Println(message)
}
Kết quả:
Hello from Goroutine
Goroutine ẩn danh gửi chuỗi "Hello from Goroutine" vào Channel. Goroutine thực hiện main() nhận chuỗi đó rồi in ra màn hình.
Việc gửi và nhận tạo ra một điểm phối hợp giữa hai Goroutine. Goroutine gửi không thể hoàn tất thao tác gửi trên unbuffered channel cho đến khi có bên nhận tương ứng.
Cơ chế chờ này giúp chương trình phối hợp hoạt động mà không cần tự kiểm tra liên tục xem dữ liệu đã sẵn sàng hay chưa.
5. Unbuffered Channel hoạt động ra sao?
Unbuffered channel là Channel không có bộ đệm lưu trữ các giá trị đang chờ được nhận.
Khi sử dụng loại Channel này, thao tác gửi cần có một thao tác nhận tương ứng để hoàn tất.
Ví dụ:
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 100
fmt.Println("Finished")
}
Đoạn code trên sẽ bị chặn tại dòng:
ch <- 100
Nguyên nhân là main() đang gửi giá trị 100 vào một unbuffered channel nhưng không có Goroutine nào nhận dữ liệu. Vì thao tác gửi không thể hoàn tất, chương trình không thể đi tiếp đến câu lệnh fmt.Println("Finished").
Nếu không còn Goroutine nào có thể tiếp tục thực thi do đang chờ các sự kiện không thể xảy ra, Go runtime sẽ phát hiện tình trạng deadlock và kết thúc chương trình bằng thông báo lỗi.
Có thể sửa ví dụ bằng cách tạo một Goroutine nhận dữ liệu:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
value := <-ch
fmt.Println(value)
}()
ch <- 100
}
Kết quả:
100
Lúc này, Goroutine trong hàm ẩn danh chờ nhận dữ liệu. Khi main() gửi 100, thao tác gửi và nhận phối hợp với nhau, cho phép cả hai tiếp tục thực thi.
Điểm quan trọng cần ghi nhớ là unbuffered channel không phải nơi lưu dữ liệu để nhận sau; nó tạo ra sự phối hợp trực tiếp giữa bên gửi và bên nhận.
6. Những lưu ý khi sử dụng Channel
Channel có thể khiến Goroutine phải chờ
Thao tác gửi hoặc nhận có thể bị chặn nếu chưa có thao tác tương ứng đáp ứng điều kiện của Channel.
Điều này là hành vi bình thường, nhưng nếu chương trình tổ chức gửi và nhận không đúng cách, một hoặc nhiều Goroutine có thể chờ mãi và dẫn đến deadlock.
Channel có kiểu dữ liệu xác định
Một Channel chỉ truyền được các giá trị phù hợp với kiểu dữ liệu mà nó khai báo. Chẳng hạn, chan int truyền số nguyên, còn chan string truyền chuỗi.
Channel có thể được tạo với bộ đệm
Ngoài unbuffered channel, Go còn hỗ trợ buffered channel, được tạo bằng cú pháp:
ch := make(chan int, 3)
Số 3 là dung lượng bộ đệm. Channel này có thể lưu tối đa ba giá trị chưa được nhận trước khi thao tác gửi phải chờ, với điều kiện Channel chưa bị đóng.
Buffered channel có quy tắc chờ khác unbuffered channel. Vì vậy, cần phân biệt hai loại này khi thiết kế luồng xử lý dữ liệu.
Channel có thể được đóng
Go cung cấp hàm close(ch) để thông báo rằng sẽ không có thêm dữ liệu nào được gửi vào Channel.
Đóng Channel không có nghĩa là xóa ngay các giá trị đã gửi vào đó. Với buffered channel, bên nhận vẫn có thể đọc các giá trị còn lại trước khi nhận trạng thái Channel đã đóng.
Thông thường, bên gửi chịu trách nhiệm đóng Channel khi đã hoàn tất việc gửi dữ liệu. Không nên đóng Channel một cách tùy tiện, đặc biệt khi vẫn còn Goroutine khác có thể gửi dữ liệu vào đó.
Không phải mọi trường hợp đều cần Channel
Channel phù hợp khi các Goroutine cần trao đổi dữ liệu hoặc phối hợp hoạt động. Tuy nhiên, không phải mọi bài toán concurrency đều bắt buộc phải sử dụng Channel.
Việc lựa chọn Channel hay một cơ chế đồng bộ khác phụ thuộc vào mục tiêu của chương trình và cách các công việc tương tác với nhau.
Kết luận
Channel là một thành phần quan trọng trong mô hình concurrency của Go, cho phép các Goroutine gửi và nhận dữ liệu, đồng thời phối hợp hoạt động thông qua các thao tác truyền dữ liệu.
Để sử dụng Channel hiệu quả, cần nắm vững cách khởi tạo, gửi và nhận dữ liệu, cũng như hiểu rõ hành vi chờ của unbuffered channel. Việc tổ chức luồng gửi và nhận hợp lý giúp chương trình tránh các tình trạng chờ không cần thiết và deadlock.
Nắm vững những kiến thức này là nền tảng để tiếp tục tìm hiểu các cách sử dụng Channel trong những chương trình Go có nhiều Goroutine và luồng xử lý phức tạp hơn.








Khoá học lập trình game con rắn cho trẻ em




