# การทำงานพร้อมกันใน Go: Goroutines และ Channels - คู่มือฉบับสมบูรณ์ > เชี่ยวชาญการทำงานพร้อมกันใน Go ด้วย goroutines และ channels รูปแบบขั้นสูง การซิงโครไนซ์ คำสั่ง select และแนวทางปฏิบัติที่ดีที่สุดพร้อมตัวอย่างโค้ดโดยละเอียด - Published: 2026-02-21 - Updated: 2026-04-29 - Author: SharpSkill - Tags: go, golang, concurrency, goroutines, channels - Reading time: 14 min --- การทำงานพร้อมกันถือเป็นจุดแข็งที่ยิ่งใหญ่ที่สุดของ Go แตกต่างจากภาษาอื่นที่มัลติเธรดยังคงซับซ้อน Go มอบโมเดลที่สง่างามบนพื้นฐานของ goroutines และ channels ที่ทำให้การพัฒนาแอปพลิเคชันที่ทำงานพร้อมกันง่ายขึ้นอย่างมีนัยสำคัญ > **ปรัชญาของ Go** > > "อย่าสื่อสารโดยการแชร์หน่วยความจำ; ให้แชร์หน่วยความจำโดยการสื่อสาร" หลักการพื้นฐานนี้ชี้นำการออกแบบการทำงานพร้อมกันทั้งหมดใน Go ## ทำความเข้าใจ Goroutines Goroutines เป็นเธรดน้ำหนักเบาที่จัดการโดย Go runtime ใช้พื้นที่ stack ประมาณ 2 KB (เทียบกับหลาย MB สำหรับเธรดของระบบปฏิบัติการ) และช่วยให้สามารถดำเนินงานพร้อมกันได้หลายพันงานโดยไม่ทำให้ระบบโอเวอร์โหลด การเรียกใช้ goroutine ต้องการเพียงการวางคีย์เวิร์ด `go` ก่อนการเรียกใช้ฟังก์ชัน Runtime จัดการการกำหนดเวลาและการกระจายระหว่างเธรดที่มีอยู่ ```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)) } ``` การดำเนินการแบบพร้อมกันลดเวลาทั้งหมดจาก 500ms เหลือประมาณ 100ms อย่างไรก็ตาม การใช้ `time.Sleep` เพื่อซิงโครไนซ์ goroutines ไม่ใช่แนวทางปฏิบัติที่ดี Channels เสนอวิธีแก้ไขที่สง่างาม ## Channels: การสื่อสารระหว่าง Goroutines Channel คือท่อนำส่งที่มีประเภทสำหรับส่งและรับค่าระหว่าง goroutines Channels รับประกันการซิงโครไนซ์: goroutine ที่ส่งจะรอจนกว่าอีกตัวจะรับ และในทางกลับกัน การสร้าง channel ใช้ฟังก์ชัน `make` ตัวดำเนินการ `<-` ส่งและรับข้อมูลขึ้นอยู่กับตำแหน่งที่สัมพันธ์กับ 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) } } ``` Channels แบบมีทิศทาง (`<-chan` สำหรับการรับ, `chan<-` สำหรับการส่ง) เสริมความปลอดภัยของโค้ดโดยจำกัดการดำเนินการที่เป็นไปได้ ## Channels แบบมี Buffer vs ไม่มี Buffer ความแตกต่างระหว่างประเภท channel เหล่านี้ส่งผลโดยตรงต่อพฤติกรรมการซิงโครไนซ์ระหว่าง goroutines Channels ที่ไม่มี buffer จะบล็อกผู้ส่งจนกว่าผู้รับจะพร้อม Channels ที่มี buffer อนุญาตให้ส่งได้ถึง N ค่าโดยไม่บล็อก โดย N แทนความจุของ 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)) } ``` Channels ที่มี buffer แยกผู้ผลิตและผู้บริโภคออกจากกัน ในขณะที่ที่ไม่มี buffer รับประกันการซิงโครไนซ์แบบจุดต่อจุด > **ระวัง Deadlocks** > > Deadlock เกิดขึ้นเมื่อ goroutines ทั้งหมดถูกบล็อกรออยู่ Go runtime ตรวจพบและยุติโปรแกรมพร้อมข้อความข้อผิดพลาดที่ชัดเจน ## Select: การมัลติเพล็กซ์ Channel คำสั่ง `select` รอการดำเนินการพร้อมกันบนหลาย channel มีลักษณะคล้ายคำสั่ง switch สำหรับการสื่อสารพร้อมกัน โครงสร้างนี้จำเป็นสำหรับการจัดการ timeout การยกเลิก และการสื่อสารหลายรายการโดยไม่บล็อกอย่างไม่มีกำหนดบน channel เดียว ```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` เลือก channel ตัวแรกที่พร้อม หากหลายตัวพร้อม การเลือกเป็นแบบสุ่มเทียมเพื่อป้องกันการอดอาหาร ## รูปแบบ Worker Pool รูปแบบ worker pool กระจายงานระหว่าง worker หลายตัว จำกัดการทำงานพร้อมกันและเพิ่มประสิทธิภาพการใช้ทรัพยากร รูปแบบนี้พิสูจน์แล้วว่าจำเป็นสำหรับการประมวลผลข้อมูลจำนวนมาก การนำไปใช้งานอาศัย channel งานที่แชร์ระหว่าง worker และ channel ผลลัพธ์สำหรับการรวบรวม ```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` ประสานการรอ worker ทั้งหมดให้เสร็จสิ้นก่อนปิด channel ผลลัพธ์ ## รูปแบบ Fan-Out/Fan-In รูปแบบนี้กระจายงานระหว่าง goroutines หลายตัว (fan-out) จากนั้นรวมผลลัพธ์ (fan-in) มันเพิ่มความขนานสูงสุดในขณะที่ทำให้การรวบรวมผลลัพธ์ง่ายขึ้น ```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) } } ``` รูปแบบนี้โดดเด่นสำหรับการดำเนินการที่ผูกกับ CPU ที่สามารถกระจายได้และไปป์ไลน์การประมวลผลข้อมูล ## Context สำหรับการยกเลิกและกำหนดเวลา แพ็คเกจ `context` ทำให้การจัดการการยกเลิก กำหนดเวลา และค่าระหว่าง goroutines เป็นมาตรฐาน goroutine ที่ทำงานนานควรยอมรับ context เป็นพารามิเตอร์แรก ```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) } } ``` > **แนวทางปฏิบัติที่ดีที่สุด** > > เรียก `defer cancel()` ทันทีหลังจากสร้าง context เสมอเพื่อหลีกเลี่ยงการรั่วไหลของทรัพยากร ## การซิงโครไนซ์ด้วย sync.Mutex แม้ว่า channels จะเป็นที่นิยมสำหรับการสื่อสาร แพ็คเกจ `sync` ยังคงจำเป็นสำหรับการปกป้องการเข้าถึงพร้อมกันไปยังโครงสร้างข้อมูลที่แชร์ ```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` เพิ่มประสิทธิภาพการอ่านพร้อมกันด้วย `RLock()`/`RUnlock()` สำหรับการดำเนินการอ่านอย่างเดียว ## ข้อผิดพลาดทั่วไปและแนวทางแก้ไข การทำงานพร้อมกันใน Go มีกับดักคลาสสิก นี่คือข้อผิดพลาดที่พบบ่อยที่สุดและวิธีหลีกเลี่ยง ```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") } } ``` การตรวจจับ race condition ใช้ flag `-race` ในระหว่างการคอมไพล์หรือการทดสอบ: `go test -race ./...`. ## บทสรุป การเชี่ยวชาญการทำงานพร้อมกันใน Go อาศัยแนวคิดสำคัญไม่กี่ประการที่เมื่อเข้าใจดีแล้ว จะช่วยให้สร้างแอปพลิเคชันที่มีประสิทธิภาพสูงได้ **ประเด็นสำคัญ:** ✅ Goroutines มีน้ำหนักเบาและราคาถูก - การสร้างหลายพันยังคงเป็นที่ยอมรับได้ ✅ Channels ซิงโครไนซ์และถ่ายโอนข้อมูลระหว่าง goroutines ✅ คำสั่ง `select` จัดการการสื่อสารหลายรายการและ timeout ✅ รูปแบบ worker pool จำกัดการทำงานพร้อมกันและเพิ่มประสิทธิภาพทรัพยากร ✅ แพ็คเกจ `context` ทำให้การยกเลิกและกำหนดเวลาเป็นมาตรฐาน ✅ Mutex ปกป้องข้อมูลที่แชร์เมื่อ channels ไม่เพียงพอ ✅ Flag `-race` ตรวจจับ race condition ในระหว่างการทดสอบ ปรัชญา "แชร์หน่วยความจำโดยการสื่อสาร" นำไปสู่การออกแบบที่ปลอดภัยและบำรุงรักษาได้ง่ายกว่ามัลติเธรดแบบดั้งเดิมที่มีการล็อก --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/go/go-concurrency-goroutines-channels