1. Go Channels & Go Context là gì?
Trong Go, goroutine cho phép chương trình chạy nhiều công việc đồng thời. Tuy nhiên, khi có nhiều goroutine cùng hoạt động, chúng cần một cách để trao đổi kết quả, phối hợp thời điểm thực thi và dừng công việc khi không còn cần thiết. Đây là lúc Channel và Context phát huy vai trò.
Channel là cơ chế cho phép các goroutine truyền dữ liệu và đồng bộ với nhau. Ví dụ, một goroutine tải dữ liệu từ API rồi gửi kết quả cho goroutine khác xử lý. Channel giúp bên nhận lấy được kết quả mà không cần tự quản lý một biến dùng chung theo cách dễ gây ra data race.
Context là cơ chế truyền thông tin về thời hạn, tín hiệu hủy và một số dữ liệu theo phạm vi công việc. Ví dụ, nếu một HTTP request bị hủy hoặc hết thời gian chờ, Context giúp những goroutine đang xử lý request đó nhận biết tình trạng này và chủ động dừng.
Hai cơ chế có liên quan nhưng không thay thế cho nhau. Channel tập trung vào giao tiếp và phối hợp giữa các goroutine, còn Context tập trung vào vòng đời của công việc. Đặc biệt, ctx.Done() trả về một channel dùng để báo hiệu khi Context bị hủy hoặc hết hạn.
2. Channel – Cách các goroutine giao tiếp với nhau
Channel hoạt động như thế nào?
Channel là một kiểu dữ liệu trong Go, được tạo bằng make và dùng để truyền những giá trị có kiểu xác định.
ví dụ:
numbers := make(chan int)
Channel trên chỉ nhận và truyền dữ liệu kiểu int. Nếu cần truyền chuỗi, có thể sử dụng chan string; nếu cần truyền một struct, có thể sử dụng channel của kiểu struct đó.
Để gửi dữ liệu vào channel, sử dụng toán tử <-:
numbers <- 10
Để nhận dữ liệu từ channel:
number := <-numbers
Điểm quan trọng là thao tác gửi và nhận có thể chờ nhau. Một goroutine có thể gửi kết quả vào channel, trong khi goroutine khác nhận kết quả đó để tiếp tục công việc. Cơ chế chờ này không chỉ giúp truyền dữ liệu mà còn có thể tạo ra sự đồng bộ giữa hai bên.
Ví dụ:
package main
import "fmt"
func worker(result chan<- int) {
result <- 100
}
func main() {
result := make(chan int)
go worker(result)
value := <-result
fmt.Println(value)
}
Trong ví dụ này, worker() gửi giá trị 100 qua channel. Goroutine chính nhận giá trị đó rồi in ra màn hình.
Unbuffered Channel và Buffered Channel
Go có hai kiểu sử dụng channel thường gặp: channel không đệm và channel có bộ đệm.
Unbuffered channel được tạo bằng:
ch := make(chan int)
Channel này không giữ sẵn các phần tử để bên nhận lấy sau. Thao tác gửi cần có một bên nhận tương ứng để hoàn thành. Vì vậy, unbuffered channel thường được dùng khi muốn hai goroutine phối hợp trực tiếp với nhau.
Ví dụ, goroutine A chỉ có thể hoàn thành thao tác gửi khi goroutine B thực hiện thao tác nhận tương ứng. Sự phối hợp này hữu ích trong những tình huống một công việc chỉ nên tiếp tục sau khi có bên khác sẵn sàng nhận dữ liệu.
Buffered channel được tạo bằng cách chỉ định dung lượng:
ch := make(chan int, 3)
Channel có thể giữ tối đa ba giá trị chưa được nhận. Khi bộ đệm còn chỗ, bên gửi có thể gửi dữ liệu mà không cần chờ bên nhận xử lý ngay. Khi bộ đệm đầy, thao tác gửi tiếp theo sẽ bị block cho đến khi có phần tử được lấy ra.
Channel Direction – Giới hạn hướng gửi và nhận
Go cho phép khai báo channel chỉ dùng để gửi hoặc chỉ dùng để nhận dữ liệu. Điều này giúp thể hiện rõ trách nhiệm của mỗi function.
func producer(out chan<- string) {
out <- "Hello"
}
func consumer(in <-chan string) {
message := <-in
fmt.Println(message)
}
Trong ví dụ trên, chan<- string biểu thị channel chỉ gửi chuỗi, còn <-chan string biểu thị channel chỉ nhận chuỗi.
3. Đọc dữ liệu và đóng Channel
Dùng range để nhận dữ liệu
Nếu một goroute cần đọc nhiều dữ liệu có thể sử dụng range.
ví dụ:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for value := range ch {
fmt.Println(value)
}
}
Goroutine bên trong gửi ba giá trị vào channel rồi đóng channel. Vòng range bên ngoài tiếp tục nhận các giá trị còn lại và kết thúc khi channel đã đóng, đồng thời không còn dữ liệu đang chờ nhận.
4. Select – Chờ nhiều Channel cùng lúc
Trong chương trình concurrency, một goroutine đôi khi phải chờ nhiều sự kiện: nhận kết quả từ channel, nhận thêm công việc hoặc nhận tín hiệu hủy từ Context. Nếu chỉ thực hiện một thao tác nhận rồi chờ, goroutine có thể bị block dù một sự kiện quan trọng khác đã xảy ra.
Go cung cấp select để chờ nhiều thao tác gửi hoặc nhận trên channel.
select {
case result := <-resultCh:
fmt.Println("Result:", result)
case <-doneCh:
fmt.Println("Task finished")
}
Trong ví dụ này, chương trình có thể phản ứng với kết quả hoặc tín hiệu hoàn thành, tùy case nào sẵn sàng trước. Nếu nhiều case cùng sẵn sàng, Go chọn một trong số đó; không nên giả định một case cụ thể sẽ luôn được ưu tiên.
5. Context – Quản lý vòng đời của một công việc
Channel giúp các goroutine trao đổi dữ liệu, nhưng nó không phải cơ chế tiêu chuẩn để truyền toàn bộ thông tin về deadline, cancellation và phạm vi request qua nhiều tầng xử lý. Go sử dụng package context cho mục đích này.
Kiểu context.Context
Go định nghĩa interface context.Context với bốn method quan trọng:
Deadline()cho biết thời điểm mà công việc nên kết thúc, nếu có deadline.Done()trả về một channel bị đóng khi Context bị hủy hoặc hết hạn.Err()trả về lý do hủy sau khi Context đã bị hủy.Value()lấy giá trị được gắn với Context.
6. Tạo Context: Background, TODO, WithCancel và WithTimeout
context.Background()
context.Background() tạo một Context gốc không có deadline, không có tín hiệu hủy và không chứa giá trị.
ctx := context.Background()
Nó thường được sử dụng ở điểm bắt đầu của chương trình, test hoặc những nơi thực sự cần tạo một phạm vi Context gốc. Từ đó, các Context khác có thể được tạo dựa trên Context ban đầu.
context.TODO()
context.TODO() được dùng khi code cần Context nhưng ở thời điểm hiện tại chưa xác định được phải sử dụng Context nào, hoặc phần code chưa được thiết kế để nhận Context đúng cách.
Đây thường là giá trị tạm thời trong quá trình phát triển hoặc chuyển đổi code. Trong application hoàn chỉnh, nên thay bằng Context phù hợp với vòng đời công việc.
context.WithCancel()
WithCancel() tạo một Context con có thể bị hủy bằng cách gọi hàm cancel.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Sau khi gọi cancel(), channel ctx.Done() sẽ được đóng và các function đang theo dõi Context có thể nhận biết yêu cầu dừng.
context.WithTimeout()
WithTimeout() tạo Context có thời gian chờ tối đa:
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
Context sẽ bị hủy khi hết hai giây, khi Context cha bị hủy hoặc khi cancel() được gọi sớm hơn.
6. Tổng kết
Go Channels & Go Context là hai phần quan trọng để quản lý chương trình có nhiều goroutine.
Channel giúp các goroutine trao đổi dữ liệu, phối hợp thời điểm thực thi và tạo nên những cấu trúc như producer-consumer hoặc worker pool. Khi sử dụng Channel, cần hiểu rõ sự khác biệt giữa buffered và unbuffered channel, hướng gửi/nhận, việc đóng channel và cách tránh deadlock.
Context quản lý phạm vi của một công việc bằng deadline, cancellation và những giá trị gắn với request. Các hàm WithCancel, WithTimeout và WithDeadline cho phép tạo những Context con có thể kết thúc sớm hơn Context cha. Tuy nhiên, Context không tự dừng goroutine; code phải chủ động phản ứng với tín hiệu hủy.








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




