Goroutine và channel trong Go: lập trình đồng thời dễ hiểu hơn bạn nghĩ

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Go được thiết kế với khả năng xử lý đồng thời ngay trong ngôn ngữ, và đó là một trong những lý do nó được dùng nhiều cho dịch vụ backend. Chỉ với hai khái niệm là goroutine và channel, bạn đã có thể viết chương trình xử lý nhiều việc cùng lúc một cách gọn gàng.

Goroutine là gì?

Goroutine là một hàm chạy đồng thời với các hàm khác, được quản lý bởi runtime của Go chứ không phải trực tiếp bởi hệ điều hành. Nó rất nhẹ so với luồng hệ điều hành, nên bạn có thể tạo rất nhiều. Bắt đầu một goroutine chỉ cần thêm từ khóa go trước lời gọi hàm:

func main() {    go fmt.Println("chạy ở goroutine khác")    fmt.Println("chạy ở goroutine chính")    time.Sleep(100 * time.Millisecond) // chờ tạm, chỉ để demo}

Khi hàm main kết thúc, chương trình thoát, bất kể goroutine khác đã xong hay chưa. Vì vậy cần có cách chờ đúng đắn thay vì Sleep.

Chờ nhiều goroutine với WaitGroup

func main() {    var wg sync.WaitGroup    for i := 1; i <= 3; i++ {        wg.Add(1)        go func(id int) {            defer wg.Done()            fmt.Println("worker", id)        }(i)    }    wg.Wait() // chờ tất cả xong}

Lưu ý truyền i vào hàm dưới dạng tham số, để mỗi goroutine nhận đúng giá trị riêng. (Từ Go 1.22 biến vòng lặp đã có phạm vi theo từng lần lặp, nhưng truyền tham số vẫn là cách rõ ràng.)

Channel: giao tiếp giữa các goroutine

Go có phương châm: đừng giao tiếp bằng cách chia sẻ bộ nhớ, hãy chia sẻ bộ nhớ bằng cách giao tiếp. Channel là ống dẫn để gửi và nhận giá trị giữa các goroutine:

func square(n int, out chan<- int) {    out <- n * n}func main() {    ch := make(chan int)    for i := 1; i <= 3; i++ {        go square(i, ch)    }    for i := 0; i < 3; i++ {        fmt.Println(<-ch) // nhận kết quả    }}

Channel không có bộ đệm sẽ chặn người gửi cho đến khi có người nhận (và ngược lại), tạo ra sự đồng bộ tự nhiên. Channel có bộ đệm make(chan int, 10) cho phép gửi tối đa 10 giá trị mà không chặn.

Mẫu worker pool

Mẫu rất phổ biến: một số worker cố định cùng lấy việc từ một channel.

func worker(id int, jobs <-chan int, results chan<- int) {    for j := range jobs {        results <- j * 2    }}func main() {    jobs := make(chan int, 10)    results := make(chan int, 10)    for w := 1; w <= 3; w++ {        go worker(w, jobs, results)    }    for j := 1; j <= 5; j++ {        jobs <- j    }    close(jobs) // báo hết việc    for i := 0; i < 5; i++ {        fmt.Println(<-results)    }}

Đóng channel bằng close báo hiệu không còn dữ liệu, giúp vòng range kết thúc. Chỉ bên gửi nên đóng channel, vì gửi vào channel đã đóng sẽ gây panic.

Select, Context và Mutex

Câu lệnh select cho phép chờ nhiều channel cùng lúc, kèm thời hạn:

select {case v := <-ch:    fmt.Println("nhận", v)case <-time.After(2 * time.Second):    fmt.Println("quá thời gian")}

Trong server, hãy dùng context.Context để hủy các goroutine khi request kết thúc, tránh rò rỉ. Còn khi buộc phải chia sẻ biến giữa các goroutine, dùng sync.Mutex để bảo vệ, và chạy thử bằng cờ go run -race để phát hiện tranh chấp dữ liệu.

Những lỗi thường gặp

  • Rò rỉ goroutine: goroutine chờ mãi một channel không ai gửi.
  • Deadlock: mọi goroutine đều chờ nhau, runtime báo "all goroutines are asleep".
  • Tranh chấp dữ liệu: nhiều goroutine ghi cùng một biến không có bảo vệ.

Ví dụ thực tế: gọi nhiều API cùng lúc

Hãy hình dung bạn cần lấy dữ liệu từ ba dịch vụ khác nhau để ghép thành một phản hồi. Chạy nối tiếp thì thời gian bằng tổng ba lần gọi; chạy đồng thời thì chỉ bằng lần chậm nhất. Với goroutine, bạn mở ba goroutine, mỗi cái gọi một dịch vụ và gửi kết quả vào channel hoặc ghi vào vị trí riêng trong một slice (mỗi goroutine ghi một ô khác nhau nên an toàn). Dùng context.WithTimeout để toàn bộ việc này có giới hạn thời gian, nếu một dịch vụ chậm thì hủy phần còn lại và trả lỗi hợp lý.

Thư viện chuẩn còn có gói mở rộng errgroup trong golang.org/x/sync, kết hợp WaitGroup với việc thu thập lỗi và hủy bằng context, rất tiện cho đúng kiểu bài toán này. Khi đã quen các khái niệm cơ bản, đây là công cụ nên học tiếp.

Kết luận

Goroutine và channel biến xử lý đồng thời thành thứ dễ tiếp cận. Hãy tự viết một chương trình tải nhiều URL cùng lúc bằng worker pool, giới hạn số worker và dùng context để đặt thời hạn. Bài tập nhỏ này bao gồm hầu hết kiến thức nền bạn cần.

Nếu muốn luyện tập cùng bài tập thực tế, hãy ghé các khóa học tại codebem.com khi bạn sẵn sàng.

author_photo
Nguyễn Đình Cường

0 comment