# Tính Đồng Thời trong Go: Goroutine và Channel - Hướng Dẫn Hoàn Chỉnh > Làm chủ tính đồng thời trong Go với goroutine và channel. Các mẫu nâng cao, đồng bộ hóa, câu lệnh select và các phương pháp hay nhất với ví dụ mã chi tiết. - Published: 2026-02-21 - Updated: 2026-04-29 - Author: SharpSkill - Tags: go, golang, concurrency, goroutines, channels - Reading time: 14 min --- Tính đồng thời là một trong những điểm mạnh lớn nhất của Go. Khác với các ngôn ngữ khác nơi đa luồng vẫn phức tạp, Go cung cấp một mô hình thanh lịch dựa trên goroutine và channel giúp đơn giản hóa đáng kể việc phát triển ứng dụng đồng thời. > **Triết Lý Go** > > "Đừ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." Nguyên tắc cơ bản này định hướng toàn bộ thiết kế đồng thời trong Go. ## Hiểu về Goroutine Goroutine là các luồng nhẹ được quản lý bởi runtime của Go. Chúng tiêu thụ khoảng 2 KB stack (so với vài MB cho luồng hệ điều hành) và cho phép thực thi hàng nghìn tác vụ đồng thời mà không gây quá tải hệ thống. Khởi chạy một goroutine chỉ cần đặt từ khóa `go` trước một lệnh gọi hàm. Runtime đảm nhận việc lập lịch và phân phối giữa các luồng có sẵn. ```go // goroutines_basic.go package main import ( "fmt" "time" ) // fetchData simulates a network request func fetchData(id int) { // Simulates network delay time.Sleep(100 * time.Millisecond) fmt.Printf("Data %d fetched\n", id) } func main() { // Sequential execution - 500ms total start := time.Now() for i := 1; i <= 5; i++ { fetchData(i) } fmt.Printf("Sequential: %v\n", time.Since(start)) // Concurrent execution - ~100ms total start = time.Now() for i := 1; i <= 5; i++ { go fetchData(i) // Execute as goroutine } time.Sleep(150 * time.Millisecond) // Wait for completion fmt.Printf("Concurrent: %v\n", time.Since(start)) } ``` Thực thi đồng thời giảm tổng thời gian từ 500ms xuống khoảng 100ms. Tuy nhiên, sử dụng `time.Sleep` để đồng bộ hóa goroutine không phải là phương pháp tốt. Channel cung cấp một giải pháp thanh lịch. ## Channel: Giao Tiếp Giữa Các Goroutine Một channel là một ống dẫn có kiểu để gửi và nhận giá trị giữa các goroutine. Channel đảm bảo đồng bộ hóa: một goroutine gửi sẽ chờ cho đến khi goroutine khác nhận, và ngược lại. Việc tạo channel sử dụng hàm `make`. Toán tử `<-` gửi và nhận dữ liệu tùy thuộc vào vị trí của nó so với channel. ```go // channels_basic.go package main import "fmt" // worker performs computation and returns result via channel func worker(id int, jobs <-chan int, results chan<- int) { // Receives jobs until channel closes for job := range jobs { result := job * 2 // Processing results <- result // Send result } } func main() { // Create channels jobs := make(chan int, 10) // Buffered channel results := make(chan int, 10) // Start 3 workers for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // Send 5 jobs for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // Signal end of jobs // Collect results for r := 1; r <= 5; r++ { result := <-results fmt.Printf("Result: %d\n", result) } } ``` Channel có hướng (`<-chan` để nhận, `chan<-` để gửi) tăng cường an toàn mã bằng cách giới hạn các thao tác có thể. ## Channel Có Buffer vs Không Buffer Sự phân biệt giữa các loại channel này ảnh hưởng trực tiếp đến hành vi đồng bộ hóa giữa các goroutine. Channel không buffer chặn người gửi cho đến khi người nhận sẵn sàng. Channel có buffer cho phép gửi tối đa N giá trị mà không bị chặn, trong đó N đại diện cho dung lượng buffer. ```go // buffered_channels.go package main import "fmt" func main() { // Unbuffered channel - strict synchronization unbuffered := make(chan string) go func() { unbuffered <- "message" // Blocks until received }() msg := <-unbuffered // Unblocks the send fmt.Println(msg) // Buffered channel - capacity of 2 buffered := make(chan int, 2) // These sends don't block buffered <- 1 buffered <- 2 // buffered <- 3 // Would block because buffer is full fmt.Println(<-buffered) // 1 fmt.Println(<-buffered) // 2 // Check capacity fmt.Printf("Length: %d, Capacity: %d\n", len(buffered), cap(buffered)) } ``` Channel có buffer tách biệt nhà sản xuất và người tiêu dùng, trong khi không buffer đảm bảo đồng bộ hóa điểm-tới-điểm. > **Cẩn Thận với Deadlock** > > Deadlock xảy ra khi tất cả các goroutine bị chặn chờ đợi. Runtime của Go phát hiện điều này và kết thúc chương trình với thông báo lỗi rõ ràng. ## Select: Đa Hợp Channel Câu lệnh `select` chờ các thao tác đồng thời trên nhiều channel. Nó tương tự như câu lệnh switch cho giao tiếp đồng thời. Cấu trúc này cần thiết để quản lý timeout, hủy bỏ và nhiều giao tiếp mà không bị chặn vô hạn trên một channel duy nhất. ```go // select_example.go package main import ( "fmt" "time" ) // fetchAPI simulates an API call with variable delay func fetchAPI(name string, delay time.Duration, ch chan<- string) { time.Sleep(delay) ch <- fmt.Sprintf("%s: data received", name) } func main() { api1 := make(chan string) api2 := make(chan string) // Launch two API calls in parallel go fetchAPI("API-1", 100*time.Millisecond, api1) go fetchAPI("API-2", 200*time.Millisecond, api2) // Global timeout of 150ms timeout := time.After(150 * time.Millisecond) // Collect results with timeout for i := 0; i < 2; i++ { select { case result := <-api1: fmt.Println(result) case result := <-api2: fmt.Println(result) case <-timeout: fmt.Println("Timeout - operation cancelled") return } } } ``` `select` chọn channel sẵn sàng đầu tiên. Nếu nhiều channel sẵn sàng, lựa chọn là giả ngẫu nhiên để tránh tình trạng đói. ## Mẫu Worker Pool Mẫu worker pool phân phối các tác vụ giữa nhiều worker, hạn chế tính đồng thời và tối ưu hóa việc sử dụng tài nguyên. Mẫu này không thể thiếu để xử lý lượng lớn dữ liệu. Việc triển khai dựa trên một channel tác vụ được chia sẻ giữa các worker và một channel kết quả để thu thập. ```go // worker_pool.go package main import ( "fmt" "sync" "time" ) // Task represents a unit of work type Task struct { ID int Data string } // Result contains the processing result type Result struct { TaskID int Output string } // worker processes received tasks func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) { defer wg.Done() for task := range tasks { // Simulate processing time.Sleep(50 * time.Millisecond) results <- Result{ TaskID: task.ID, Output: fmt.Sprintf("Worker %d processed: %s", id, task.Data), } } } func main() { const numWorkers = 3 const numTasks = 10 tasks := make(chan Task, numTasks) results := make(chan Result, numTasks) var wg sync.WaitGroup // Start workers for w := 1; w <= numWorkers; w++ { wg.Add(1) go worker(w, tasks, results, &wg) } // Send tasks for i := 1; i <= numTasks; i++ { tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)} } close(tasks) // Close results channel after workers finish go func() { wg.Wait() close(results) }() // Collect results for result := range results { fmt.Printf("Task %d: %s\n", result.TaskID, result.Output) } } ``` `sync.WaitGroup` điều phối việc chờ tất cả worker hoàn thành trước khi đóng channel kết quả. ## Mẫu Fan-Out/Fan-In Mẫu này phân phối công việc giữa nhiều goroutine (fan-out) sau đó tổng hợp kết quả (fan-in). Nó tối đa hóa tính song song trong khi đơn giản hóa việc thu thập kết quả. ```go // fan_out_fan_in.go package main import ( "fmt" "sync" ) // generate produces numbers on a channel func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // square computes the square of received numbers func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // merge combines multiple channels into one (fan-in) func merge(channels ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup // Output function for each channel output := func(c <-chan int) { defer wg.Done() for n := range c { out <- n } } // Launch a goroutine per channel wg.Add(len(channels)) for _, c := range channels { go output(c) } // Close after all goroutines finish go func() { wg.Wait() close(out) }() return out } func main() { // Generate data numbers := generate(1, 2, 3, 4, 5, 6, 7, 8) // Fan-out: distribute to 3 workers sq1 := square(numbers) sq2 := square(numbers) sq3 := square(numbers) // Fan-in: aggregate results for result := range merge(sq1, sq2, sq3) { fmt.Println(result) } } ``` Mẫu này xuất sắc cho các thao tác giới hạn CPU có thể phân phối và pipeline xử lý dữ liệu. ## Context cho Hủy Bỏ và Deadline Gói `context` chuẩn hóa việc quản lý hủy bỏ, deadline và giá trị giữa các goroutine. Bất kỳ goroutine chạy lâu nào cũng nên chấp nhận một context làm tham số đầu tiên. ```go // context_example.go package main import ( "context" "fmt" "time" ) // fetchWithContext simulates a cancellable request func fetchWithContext(ctx context.Context, url string) (string, error) { // Simulates a long operation select { case <-time.After(2 * time.Second): return fmt.Sprintf("Data from %s", url), nil case <-ctx.Done(): return "", ctx.Err() // context.Canceled or context.DeadlineExceeded } } func main() { // Context with 500ms timeout ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() // Release resources result, err := fetchWithContext(ctx, "https://api.example.com") if err != nil { fmt.Printf("Error: %v\n", err) return } fmt.Println(result) // Context with manual cancellation ctx2, cancel2 := context.WithCancel(context.Background()) go func() { time.Sleep(100 * time.Millisecond) cancel2() // Explicit cancellation }() result, err = fetchWithContext(ctx2, "https://api2.example.com") if err != nil { fmt.Printf("Request cancelled: %v\n", err) } } ``` > **Phương Pháp Hay Nhất** > > Luôn gọi `defer cancel()` ngay sau khi tạo một context để tránh rò rỉ tài nguyên. ## Đồng Bộ Hóa với sync.Mutex Mặc dù channel được ưa chuộng cho giao tiếp, gói `sync` vẫn cần thiết để bảo vệ truy cập đồng thời vào các cấu trúc dữ liệu được chia sẻ. ```go // mutex_example.go package main import ( "fmt" "sync" ) // SafeCounter is a thread-safe counter type SafeCounter struct { mu sync.Mutex value map[string]int } // Increment increments the value for a given key func (c *SafeCounter) Increment(key string) { c.mu.Lock() // Exclusive lock defer c.mu.Unlock() // Guaranteed unlock c.value[key]++ } // Value returns the current value func (c *SafeCounter) Value(key string) int { c.mu.Lock() defer c.mu.Unlock() return c.value[key] } func main() { counter := SafeCounter{value: make(map[string]int)} var wg sync.WaitGroup // 1000 concurrent increments for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment("visits") }() } wg.Wait() fmt.Printf("Total: %d\n", counter.Value("visits")) // 1000 } ``` `sync.RWMutex` tối ưu hóa các đọc đồng thời với `RLock()`/`RUnlock()` cho các thao tác chỉ đọc. ## Lỗi Phổ Biến và Giải Pháp Tính đồng thời trong Go có những cạm bẫy cổ điển. Đây là những lỗi phổ biến nhất và cách tránh chúng. ```go // common_mistakes.go package main import ( "fmt" "sync" ) func main() { // ERROR: Loop variable capture // All goroutines would print the same value for i := 0; i < 3; i++ { go func() { fmt.Println(i) // Capture by reference - BUG }() } // SOLUTION: Pass value as parameter var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(n int) { defer wg.Done() fmt.Println(n) // Local copy - CORRECT }(i) } wg.Wait() // ERROR: Send on nil channel var ch chan int // ch <- 1 // Blocks forever // SOLUTION: Always initialize with make ch = make(chan int, 1) ch <- 1 fmt.Println(<-ch) // ERROR: Send on closed channel done := make(chan bool) close(done) // done <- true // Panic! // SOLUTION: Check before send or use sync.Once select { case done <- true: fmt.Println("Sent") default: fmt.Println("Channel closed or full") } } ``` Phát hiện race condition sử dụng cờ `-race` trong quá trình biên dịch hoặc kiểm thử: `go test -race ./...`. ## Kết Luận Làm chủ tính đồng thời trong Go dựa trên một số khái niệm chính mà khi được hiểu rõ, cho phép xây dựng các ứng dụng hiệu suất cao. **Điểm chính:** ✅ Goroutine nhẹ và rẻ - tạo hàng nghìn vẫn có thể chấp nhận được ✅ Channel đồng bộ hóa và truyền dữ liệu giữa các goroutine ✅ Câu lệnh `select` quản lý nhiều giao tiếp và timeout ✅ Mẫu worker pool hạn chế tính đồng thời và tối ưu hóa tài nguyên ✅ Gói `context` chuẩn hóa hủy bỏ và deadline ✅ Mutex bảo vệ dữ liệu được chia sẻ khi channel không đủ ✅ Cờ `-race` phát hiện race condition trong quá trình kiểm thử Triết lý "Chia sẻ bộ nhớ bằng cách giao tiếp" hướng tới các thiết kế an toàn và dễ bảo trì hơn so với đa luồng truyền thống với khóa. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/go/go-concurrency-goroutines-channels