Explore all courses in this category.
Explore all courses in this category.
Nguyễn Đình Cường
0 comment
09 Nov, 2026
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à 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.
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.)
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 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.
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.
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.
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.
Nguyễn Đình Cường
0 comment