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.

 

Bài viết khác

Goroutine & Concurrency

1. Concurrency là gì? Concurrency là khả năng tổ chức nhiều công việc để chúng có thể tiến triển xen kẽ hoặc đồng thời trong cùng một khoảng thời gian. Thay vì phải thực hiện tất cả công việc theo một trình tự duy nhất, chương trình có thể chuyển đổi giữa nhiều công việc […]

Go Fundamentals

1. Go là gì? Go (Golang) là ngôn ngữ lập trình go thường được sử dụng trong backend, REST API, hệ thống mạng, công cụ dòng lệnh, cloud infrastructure và các dịch vụ chạy đồng thời. Go là ngôn ngữ biên dịch và có hệ thống kiểu dữ liệu tĩnh (statically typed). Điều này có […]

Laravel Routing & Controllers

1. Laravel Routing & Controllers là gì? Khi xây dựng backend bằng Laravel, client gửi HTTP request đến server để thực hiện một thao tác như lấy danh sách user, xem chi tiết sản phẩm hoặc tạo đơn hàng. Laravel cần xác định request đó sẽ được xử lý ở đâu và logic xử lý […]

Series 2 — Frontend Fundamentals: Responsive Web Design – Bootstrap

Series 2 — Frontend Fundamentals   HTML → Sematic HTML → Forms & Validation → CSS  Flexbox & Grid → Responsive Web Design → Bootstrap   Bài 6. Responsive Web Design – Giao diện đa thiết bị 1. Responsive Web Design là gì? Khi xây dựng một website, chúng ta không thể giả định rằng […]

HTTP & HTTPS

Giao thức HTTP và HTTPS là gì? 1. HTTP là gì? HTTP (Hypertext Transfer Protocol) là giao thức truyền tải siêu văn bản, được sử dụng để trao đổi dữ liệu giữa client (máy khách) và server (máy chủ). Ban đầu, HTTP được phát triển để trao đổi các tài liệu trên Web. Ngày nay, […]

Series 2 — Frontend Fundamentals: CSS — Flexbox-Grid

Series 2 — Frontend Fundamentals   HTML → Sematic HTML → Forms & Validation → CSS  Flexbox & Grid → Responsive Web Design → Bootstrap   Bài 4. CSS Fundamentals – CSS là gì?   CSS Fundamentals – CSS là gì và hoạt động như thế nào? Ở những bài trước, chúng ta đã tìm […]

Leave a Reply

Your email address will not be published. Required fields are marked *