การทำงานพร้อมกันใน Go: Goroutines และ Channels - คู่มือฉบับสมบูรณ์
เชี่ยวชาญการทำงานพร้อมกันใน Go ด้วย goroutines และ channels รูปแบบขั้นสูง การซิงโครไนซ์ คำสั่ง select และแนวทางปฏิบัติที่ดีที่สุดพร้อมตัวอย่างโค้ดโดยละเอียด

การทำงานพร้อมกันถือเป็นจุดแข็งที่ยิ่งใหญ่ที่สุดของ Go แตกต่างจากภาษาอื่นที่มัลติเธรดยังคงซับซ้อน Go มอบโมเดลที่สง่างามบนพื้นฐานของ goroutines และ channels ที่ทำให้การพัฒนาแอปพลิเคชันที่ทำงานพร้อมกันง่ายขึ้นอย่างมีนัยสำคัญ
"อย่าสื่อสารโดยการแชร์หน่วยความจำ; ให้แชร์หน่วยความจำโดยการสื่อสาร" หลักการพื้นฐานนี้ชี้นำการออกแบบการทำงานพร้อมกันทั้งหมดใน Go
ทำความเข้าใจ Goroutines
Goroutines เป็นเธรดน้ำหนักเบาที่จัดการโดย Go runtime ใช้พื้นที่ stack ประมาณ 2 KB (เทียบกับหลาย MB สำหรับเธรดของระบบปฏิบัติการ) และช่วยให้สามารถดำเนินงานพร้อมกันได้หลายพันงานโดยไม่ทำให้ระบบโอเวอร์โหลด
การเรียกใช้ goroutine ต้องการเพียงการวางคีย์เวิร์ด go ก่อนการเรียกใช้ฟังก์ชัน Runtime จัดการการกำหนดเวลาและการกระจายระหว่างเธรดที่มีอยู่
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
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
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 รับประกันการซิงโครไนซ์แบบจุดต่อจุด
Deadlock เกิดขึ้นเมื่อ goroutines ทั้งหมดถูกบล็อกรออยู่ Go runtime ตรวจพบและยุติโปรแกรมพร้อมข้อความข้อผิดพลาดที่ชัดเจน
Select: การมัลติเพล็กซ์ Channel
คำสั่ง select รอการดำเนินการพร้อมกันบนหลาย channel มีลักษณะคล้ายคำสั่ง switch สำหรับการสื่อสารพร้อมกัน
โครงสร้างนี้จำเป็นสำหรับการจัดการ timeout การยกเลิก และการสื่อสารหลายรายการโดยไม่บล็อกอย่างไม่มีกำหนดบน channel เดียว
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 ตัวแรกที่พร้อม หากหลายตัวพร้อม การเลือกเป็นแบบสุ่มเทียมเพื่อป้องกันการอดอาหาร
พร้อมที่จะพิชิตการสัมภาษณ์ Go แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
รูปแบบ Worker Pool
รูปแบบ worker pool กระจายงานระหว่าง worker หลายตัว จำกัดการทำงานพร้อมกันและเพิ่มประสิทธิภาพการใช้ทรัพยากร รูปแบบนี้พิสูจน์แล้วว่าจำเป็นสำหรับการประมวลผลข้อมูลจำนวนมาก
การนำไปใช้งานอาศัย channel งานที่แชร์ระหว่าง worker และ channel ผลลัพธ์สำหรับการรวบรวม
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) มันเพิ่มความขนานสูงสุดในขณะที่ทำให้การรวบรวมผลลัพธ์ง่ายขึ้น
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 เป็นพารามิเตอร์แรก
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 ยังคงจำเป็นสำหรับการปกป้องการเข้าถึงพร้อมกันไปยังโครงสร้างข้อมูลที่แชร์
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 มีกับดักคลาสสิก นี่คือข้อผิดพลาดที่พบบ่อยที่สุดและวิธีหลีกเลี่ยง
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 ในระหว่างการทดสอบ
ปรัชญา "แชร์หน่วยความจำโดยการสื่อสาร" นำไปสู่การออกแบบที่ปลอดภัยและบำรุงรักษาได้ง่ายกว่ามัลติเธรดแบบดั้งเดิมที่มีการล็อก
แท็ก
แชร์
บทความที่เกี่ยวข้อง

สัมภาษณ์เทคนิค Go: Goroutine, Channel และ Concurrency
คำถามสัมภาษณ์เทคนิค Go เกี่ยวกับ goroutine, channel และ concurrency pattern ต่างๆ ตัวอย่างโค้ด ข้อผิดพลาดที่พบบ่อย และคำตอบระดับผู้เชี่ยวชาญสำหรับเตรียมสัมภาษณ์เทคนิค Go ปี 2026

25 คำถามสัมภาษณ์ Go ยอดนิยม: คู่มือฉบับสมบูรณ์สำหรับนักพัฒนา
พิชิตการสัมภาษณ์ Go ด้วย 25 คำถามที่ถูกถามบ่อย goroutine, channel, interface และรูปแบบการทำงานพร้อมกันพร้อมตัวอย่างโค้ด

Go: พื้นฐานสำหรับนักพัฒนา Java/Python ในปี 2026
เรียนรู้ Go อย่างรวดเร็วโดยใช้ประสบการณ์จาก Java หรือ Python Goroutine, channel, interface และ pattern สำคัญสำหรับการเปลี่ยนผ่านอย่างราบรื่น