# สัมภาษณ์เทคนิค Go: Goroutine, Channel และ Concurrency > คำถามสัมภาษณ์เทคนิค Go เกี่ยวกับ goroutine, channel และ concurrency pattern ต่างๆ ตัวอย่างโค้ด ข้อผิดพลาดที่พบบ่อย และคำตอบระดับผู้เชี่ยวชาญสำหรับเตรียมสัมภาษณ์เทคนิค Go ปี 2026 - Published: 2026-04-01 - Updated: 2026-04-01 - Author: Anthony Fillion-Maillet - Tags: go, golang, interview, goroutines, channels, concurrency - Reading time: 9 min --- คำถามสัมภาษณ์ Go เกี่ยวกับ goroutine, channel และ concurrency เป็นหัวข้อที่ท้าทายที่สุดสำหรับผู้สมัครอย่างสม่ำเสมอ ความเข้าใจอย่างลึกซึ้งในแนวคิดเหล่านี้คือสิ่งที่แยกวิศวกร Go ระดับอาวุโสออกจากผู้ที่ยังเรียนรู้ภาษาอยู่ คู่มือนี้รวบรวมคำถามที่ผู้สัมภาษณ์มักถามในปี 2026 พร้อมตัวอย่างโค้ดระดับ production และเหตุผลเบื้องหลังแต่ละคำตอบ > **ผู้สัมภาษณ์ทดสอบอะไรจริงๆ** > > การสัมภาษณ์ concurrency ของ Go เน้นสามด้าน: การจัดการวงจรชีวิต goroutine, ความหมายของ channel (buffered กับ unbuffered, ชนิด directional) และการประกอบ pattern (fan-out/fan-in, worker pool, context cancellation) การท่องจำ syntax เพียงอย่างเดียวไม่เพียงพอ — ผู้สัมภาษณ์คาดหวังให้ผู้สมัครสามารถวิเคราะห์ race condition และ deadlock ได้ ## พื้นฐาน Goroutine: คำถามที่ถูกถามเสมอ รอบแรกของการสัมภาษณ์มักทดสอบว่าผู้สมัครเข้าใจ goroutine จริงๆ หรือไม่ — ไม่ใช่แค่วิธีสร้างมัน **ถ: Goroutine คืออะไร และแตกต่างจาก OS thread อย่างไร?** Goroutine คือฟังก์ชัน concurrent ที่มีน้ำหนักเบา จัดการโดย Go runtime scheduler ไม่ใช่ระบบปฏิบัติการ Go runtime ทำ multiplexing goroutine หลายพันตัวลงบน OS thread จำนวนน้อยโดยใช้โมเดลการจัดตาราง M:N (M goroutine ถูกแมปลงบน N OS thread) ```go // goroutine_basics.go package main import ( "fmt" "runtime" "sync" ) func main() { // Print the number of OS threads available fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) var wg sync.WaitGroup for i := 0; i < 10000; i++ { wg.Add(1) go func(id int) { defer wg.Done() // Each goroutine starts with ~2-8KB stack // OS threads typically start with 1-8MB _ = id }(i) } wg.Wait() fmt.Println("All 10,000 goroutines completed") } ``` ความแตกต่างหลักที่ควรกล่าวถึงในการสัมภาษณ์: goroutine เริ่มต้นด้วย stack 2-8KB ที่ขยายได้แบบไดนามิก เทียบกับ stack คงที่ 1-8MB ของ OS thread การสลับบริบทระระหว่าง goroutine จัดการใน user space โดย Go scheduler หลีกเลี่ยงการสลับบริบทระระดับ kernel ที่มีต้นทุนสูงของ OS thread ทำให้การสร้าง goroutine 100,000 ตัวเป็นเรื่องปกติ ในขณะที่ OS thread 100,000 ตัวจะใช้ทรัพยากรระบบจนหมด **ถ: เกิดอะไรขึ้นเมื่อ goroutine เกิด panic?** Panic ที่ไม่ถูก recover ใน goroutine ใดก็ตามจะทำให้โปรแกรมทั้งหมดหยุดทำงาน ต่างจาก exception ใน Java หรือ Python ตรงที่ panic แพร่ขึ้นตาม call stack ของ goroutine ตัวเอง — ไม่ใช่ stack ของ goroutine ที่สร้างมันขึ้นมา วิธีเดียวที่จะดักจับได้คือใช้ `recover()` ภายใน deferred function ของ goroutine เดียวกัน ```go // panic_recovery.go package main import "fmt" func safeGo(fn func()) { go func() { defer func() { if r := recover(); r != nil { fmt.Println("recovered from panic:", r) } }() fn() // execute the actual work }() } func main() { safeGo(func() { panic("something went wrong") }) // Give goroutine time to complete select {} } ``` ผู้สัมภาษณ์มองหาความตระหนักรู้ว่าบริการ Go ในสภาพแวดล้อม production จะห่อหุ้มการเริ่มทำงาน goroutine ด้วย recovery pattern ไลบรารีอย่าง `errgroup` จัดการเรื่องนี้ได้อย่างเรียบร้อยกว่า ## ความหมายของ Channel: Buffered, Unbuffered และ Directional คำถามเกี่ยวกับ channel เปิดเผยว่าผู้สมัครเข้าใจโมเดล concurrency ของ Go จริงๆ หรือแค่ท่องจำ pattern **ถ: ความแตกต่างระหว่าง buffered และ unbuffered channel คืออะไร?** Unbuffered channel (`make(chan T)`) ต้องการให้ทั้งผู้ส่งและผู้รับพร้อมพร้อมกัน — การส่งจะบล็อกจนกว่า goroutine อื่นจะรับค่า Buffered channel (`make(chan T, n)`) อนุญาตให้ส่งได้สูงสุด `n` ค่าโดยไม่บล็อก ```go // channel_semantics.go package main import "fmt" func main() { // Unbuffered: send blocks until receive is ready ch := make(chan string) go func() { ch <- "hello" // blocks here until main reads }() msg := <-ch fmt.Println(msg) // Buffered: send does not block until buffer is full buf := make(chan int, 3) buf <- 1 // does not block (buffer has space) buf <- 2 // does not block buf <- 3 // does not block // buf <- 4 would block — buffer is full fmt.Println(<-buf, <-buf, <-buf) // 1 2 3 } ``` คำถามต่อเนื่องที่ผู้สัมภาษณ์มักถาม: "เมื่อไหร่ควรเลือกใช้แบบไหน?" Unbuffered channel บังคับการซิงโครไนซ์ — มีประโยชน์เมื่อผู้ส่งต้องรู้ว่าผู้รับประมวลผลค่าแล้ว Buffered channel แยกเวลาของผู้ส่งและผู้รับออกจากกัน — มีประโยชน์สำหรับ work queue หรือ rate limiting ที่ยอมรับความหย่อนได้ระดับหนึ่ง **ถ: เกิดอะไรขึ้นเมื่อปิด channel?** การปิด channel ส่งสัญญาณว่าไม่มีค่าจะถูกส่งเข้ามาอีก การ receive จาก channel ที่ปิดแล้วจะคืนค่าทันทีพร้อม zero value การส่งค่าไปยัง channel ที่ปิดแล้วจะทำให้เกิด panic ลูป `range` บน channel จะออกโดยอัตโนมัติเมื่อ channel ถูกปิด ```go // close_channel.go package main import "fmt" func producer(ch chan<- int, count int) { for i := 0; i < count; i++ { ch <- i } close(ch) // signal: no more values } func main() { ch := make(chan int, 5) go producer(ch, 5) // range exits automatically when channel closes for val := range ch { fmt.Println("received:", val) } // Reading from closed channel returns zero value + false val, ok := <-ch fmt.Printf("after close: val=%d, ok=%v\n", val, ok) } ``` จุดสำคัญ: มีเพียงผู้ส่งเท่านั้นที่ควรปิด channel ไม่ใช่ผู้รับ การปิด channel ที่ goroutine อื่นยังเขียนอยู่จะทำให้เกิด panic ## คำสั่ง Select: การ Multiplexing Channel **ถ: `select` ทำงานอย่างไร และเกิดอะไรเมื่อหลาย case พร้อมพร้อมกัน?** คำสั่ง `select` จะบล็อกจนกว่าหนึ่งในการดำเนินการ channel สามารถดำเนินการต่อได้ เมื่อหลาย case พร้อมพร้อมกัน Go จะสุ่มเลือก — เพื่อป้องกันไม่ให้ case ใดถูกละเลย ```go // select_multiplex.go package main import ( "context" "fmt" "time" ) func fetchFromAPI(ctx context.Context, url string) (string, error) { resultCh := make(chan string, 1) errCh := make(chan error, 1) go func() { // Simulate API call time.Sleep(200 * time.Millisecond) resultCh <- fmt.Sprintf("data from %s", url) }() select { case result := <-resultCh: return result, nil case err := <-errCh: return "", err case <-ctx.Done(): // Context cancelled or timed out return "", ctx.Err() } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() result, err := fetchFromAPI(ctx, "https://api.example.com/data") if err != nil { fmt.Println("error:", err) return } fmt.Println(result) } ``` ผู้สัมภาษณ์ทดสอบสองอย่างด้วย `select`: ความเข้าใจกฎการเลือกแบบสุ่ม และความสามารถในการรวม channel กับ `context.Context` สำหรับ pattern timeout และการยกเลิก ## Concurrency Pattern ที่พบบ่อยในการสัมภาษณ์ การสัมภาษณ์ Go ระดับ senior แทบทุกครั้งมักมีคำถามเกี่ยวกับการเขียน pattern ต่อไปนี้จากศูนย์ ### Pattern Fan-Out/Fan-In **ถ: เขียน pipeline fan-out/fan-in ที่ประมวลผลรายการแบบขนานกัน** Fan-out กระจายงานไปยังหลาย goroutine Fan-in รวบรวมผลลัพธ์จากหลาย goroutine เข้าสู่ channel เดียว ```go // fanout_fanin.go package main import ( "fmt" "sync" ) // generator produces values on a channel func generator(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // square reads from input, squares each value func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // fanIn merges multiple channels into one func fanIn(channels ...<-chan int) <-chan int { var wg sync.WaitGroup merged := make(chan int) for _, ch := range channels { wg.Add(1) go func(c <-chan int) { defer wg.Done() for val := range c { merged <- val } }(ch) } go func() { wg.Wait() close(merged) // close after all inputs are drained }() return merged } func main() { in := generator(2, 3, 4, 5, 6) // Fan out: two goroutines reading from same channel c1 := square(in) c2 := square(in) // Fan in: merge results for result := range fanIn(c1, c2) { fmt.Println(result) } } ``` Insight หลักที่ผู้สัมภาษณ์ต้องการ: channel `generator` ถูกแชร์ระหว่าง `c1` และ `c2` ดังนั้นแต่ละค่าจะถูกประมวลผลโดย worker เพียงตัวเดียว (ไม่ซ้ำกัน) ฟังก์ชัน `fanIn` ใช้ `WaitGroup` เพื่อทราบว่า input channel ทั้งหมดถูกอ่านจนหมดก่อนปิด channel รวม ### Worker Pool ด้วย errgroup **ถ: เขียน bounded worker pool ที่มีการจัดการ error อย่างไร?** Package `golang.org/x/sync/errgroup` (ส่วนหนึ่งของ Go extended standard library) แก้ปัญหานี้ได้อย่างเรียบร้อย โดยจัดการวงจรชีวิต goroutine รวบรวม error แรก และทำงานร่วมกับ `context` สำหรับการยกเลิก ```go // worker_pool.go package main import ( "context" "fmt" "golang.org/x/sync/errgroup" ) func processItem(ctx context.Context, id int) error { // Check for cancellation before heavy work select { case <-ctx.Done(): return ctx.Err() default: } if id == 7 { return fmt.Errorf("failed to process item %d", id) } fmt.Printf("processed item %d\n", id) return nil } func main() { g, ctx := errgroup.WithContext(context.Background()) g.SetLimit(3) // maximum 3 concurrent goroutines for i := 0; i < 10; i++ { id := i g.Go(func() error { return processItem(ctx, id) }) } // Wait blocks until all goroutines finish // Returns the first non-nil error if err := g.Wait(); err != nil { fmt.Println("pipeline error:", err) } } ``` ตั้งแต่ Go 1.24 (เวอร์ชัน stable ล่าสุด ณ ต้นปี 2026) pattern นี้ยังคงเป็นวิธีที่แนะนำ เมธอด `SetLimit` ถูกเพิ่มใน Go 1.20 และทำให้ไม่ต้องเขียนการจำกัด concurrency แบบ semaphore ด้วยตัวเองอีกต่อไป ## Race Condition และ sync Primitive **ถ: ตรวจจับและป้องกัน race condition ใน Go อย่างไร?** Go มี race detector ในตัวที่เปิดใช้งานด้วย flag `-race` เครื่องมือนี้ตรวจจับการเข้าถึง shared memory แบบ concurrent ที่ไม่ซิงโครไนซ์กันในขณะ runtime ```go // race_condition.go package main import ( "fmt" "sync" "sync/atomic" ) // BAD: race condition — do not use in production func unsafeCounter() int { counter := 0 var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ // DATA RACE: concurrent read/write }() } wg.Wait() return counter // result is non-deterministic } // GOOD: atomic operations for simple counters func atomicCounter() int64 { var counter atomic.Int64 var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Add(1) // thread-safe atomic increment }() } wg.Wait() return counter.Load() // always 1000 } // GOOD: mutex for complex shared state type SafeMap struct { mu sync.RWMutex data map[string]int } func (m *SafeMap) Set(key string, val int) { m.mu.Lock() // exclusive lock for writes defer m.mu.Unlock() m.data[key] = val } func (m *SafeMap) Get(key string) (int, bool) { m.mu.RLock() // shared lock for reads defer m.mu.RUnlock() v, ok := m.data[key] return v, ok } func main() { fmt.Println("unsafe:", unsafeCounter()) // unpredictable fmt.Println("atomic:", atomicCounter()) // always 1000 } ``` คำตอบสัมภาษณ์ควรครอบคลุมสามกลยุทธ์การซิงโครไนซ์: `sync.Mutex` / `sync.RWMutex` สำหรับ shared state ที่ซับซ้อน, `sync/atomic` สำหรับ counter และ flag จำนวนเล็ก, และ channel สำหรับการสื่อสารระหว่าง goroutine ("share memory by communicating, don’t communicate by sharing memory") การรัน `go test -race ./...` ควรเป็นส่วนหนึ่งของทุก CI pipeline ## Context และ Pattern การยกเลิก **ถ: อธิบายว่า context.Context ควบคุมวงจรชีวิต goroutine อย่างไร** Package `context` มีกลไกสำหรับส่งต่อสัญญาณการยกเลิก deadline และค่าต่างๆ ในระดับ request ข้ามขอบเขตของ goroutine ทุก goroutine ที่ทำงานยาวนานควรรับ `context.Context` เป็นพารามิเตอร์ตัวแรก ```go // context_cancellation.go package main import ( "context" "fmt" "time" ) // worker simulates a long-running task func worker(ctx context.Context, id int, results chan<- string) { select { case <-time.After(time.Duration(id*100) * time.Millisecond): results <- fmt.Sprintf("worker %d: done", id) case <-ctx.Done(): results <- fmt.Sprintf("worker %d: cancelled (%v)", id, ctx.Err()) } } func main() { // Parent context with 250ms deadline ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) defer cancel() results := make(chan string, 5) // Launch 5 workers with increasing durations for i := 1; i <= 5; i++ { go worker(ctx, i, results) } // Collect all results for i := 0; i < 5; i++ { fmt.Println(<-results) } } ``` Worker 1 และ 2 ทำงานเสร็จภายใน deadline 250ms Worker 3, 4 และ 5 ได้รับสัญญาณยกเลิกผ่าน `ctx.Done()` Pattern นี้เป็นพื้นฐานสำหรับการสร้าง HTTP server และ microservice ที่มีความทนทานใน Go — ทุก request handler จะรับ context ที่ส่งต่อสัญญาณยกเลิกเมื่อ client ตัดการเชื่อมต่อ > **กับดักที่พบบ่อยในสัมภาษณ์** > > ห้ามเก็บ `context.Context` ไว้ใน field ของ struct [เอกสารอ้างอิงของ Go](https://pkg.go.dev/context) ระบุชัดเจนว่า: "Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it." ผู้สัมภาษณ์ทดสอบเรื่องนี้เพื่อประเมินว่าผู้สมัครปฏิบัติตามแนวทางของ Go หรือไม่ ## การตรวจจับ Deadlock: คำถามสัมภาษณ์แบบหลอกล่อ **ถ: โค้ดนี้จะเกิด deadlock หรือไม่? เพราะอะไร?** คำถามเรื่อง deadlock เป็นที่นิยมเพราะทดสอบความสามารถในการวิเคราะห์การจัดตาราง goroutine และการดำเนินการ channel ```go // deadlock_example.go package main func main() { ch := make(chan int) ch <- 42 // DEADLOCK: unbuffered send with no receiver // The main goroutine blocks here forever // Go runtime detects this: "fatal error: all goroutines are asleep" } ``` วิธีแก้ไขตรงไปตรงมา: ทำ channel เป็นแบบ buffered (`make(chan int, 1)`) หรือสร้าง goroutine เพื่อรับก่อนส่ง Go runtime ตรวจจับ deadlock เมื่อทุก goroutine ถูกบล็อก — แต่เฉพาะเมื่อ *ทุกตัว* หยุดทำงาน หากมีแม้แต่ goroutine เดียวที่ยังทำงานอยู่ (เช่น HTTP server ที่ทำงานอยู่เบื้องหลัง) runtime จะไม่ตรวจจับ deadlock บางส่วน > **Deadlock บางส่วนมองไม่เห็น** > > Go runtime ตรวจจับ deadlock ได้เฉพาะเมื่อทุก goroutine ในโปรแกรมถูกบล็อก ในแอปพลิเคชันจริงที่มี HTTP server หรือ background worker goroutine ที่รั่วไหลและเกิด deadlock จะไม่กระตุ้นตัวตรวจจับของ runtime เครื่องมืออย่าง `pprof` และ goroutine dump (`runtime.Stack`) จำเป็นสำหรับวิเคราะห์ปัญหาเหล่านี้ในสภาพแวดล้อม production ## Pattern ขั้นสูง: การประมวลผลแบบ Concurrent พร้อม Rate Limiting **ถ: เขียน rate-limited concurrent API call อย่างไร?** คำถามนี้ทดสอบความสามารถในการรวม concurrency primitive หลายตัวเข้าเป็นโซลูชันที่สมบูรณ์ ```go // rate_limited.go package main import ( "context" "fmt" "sync" "time" ) // RateLimiter controls concurrent and temporal access type RateLimiter struct { semaphore chan struct{} // limits concurrency ticker *time.Ticker // limits rate } func NewRateLimiter(maxConcurrent int, interval time.Duration) *RateLimiter { return &RateLimiter{ semaphore: make(chan struct{}, maxConcurrent), ticker: time.NewTicker(interval), } } func (rl *RateLimiter) Execute(ctx context.Context, fn func() error) error { // Wait for rate limit tick select { case <-rl.ticker.C: case <-ctx.Done(): return ctx.Err() } // Acquire concurrency slot select { case rl.semaphore <- struct{}{}: case <-ctx.Done(): return ctx.Err() } defer func() { <-rl.semaphore }() // release slot return fn() } func main() { rl := NewRateLimiter(3, 100*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() err := rl.Execute(ctx, func() error { fmt.Printf("[%v] processing %d\n", time.Now().Format("04:05.000"), id) time.Sleep(150 * time.Millisecond) // simulate work return nil }) if err != nil { fmt.Printf("item %d: %v\n", id, err) } }(i) } wg.Wait() } ``` Pattern นี้รวม semaphore ที่ใช้ channel (จำกัด concurrency) กับ ticker (จำกัดอัตรา) Double `select` ที่ตรวจสอบ context รับประกันการ shutdown แบบ graceful คำตอบระดับ production เช่นนี้คือสิ่งที่แยกผู้สมัครระดับ senior ออกจากคนอื่น ## สรุป - Goroutine เป็น thread ระดับ user-space ที่ Go runtime จัดการด้วยการลำดับ M:N; ต้อง recover panic ใน goroutine ที่ถูก spawn เสมอ - Unbuffered channel ซิงโครไนซ์ผู้ส่งและผู้รับ; buffered channel แยกเวลา — เลือกตามว่าผู้ส่งต้องการการยืนยันหรือไม่ - คำสั่ง `select` ทำ multiplexing การดำเนินการ channel ด้วยการเลือกสุ่มเมื่อหลาย case พร้อม; ใช้ร่วมกับ `context.Context` สำหรับ timeout - Fan-out/fan-in และ worker pool (ผ่าน `errgroup.SetLimit`) เป็นสอง concurrency pattern ที่ถูกถามบ่อยที่สุด - ใช้ `sync.Mutex` สำหรับ shared state ซับซ้อน, `sync/atomic` สำหรับ counter ง่ายๆ, และ channel สำหรับการสื่อสารระหว่าง goroutine - รัน `go test -race` ใน CI เสมอเพื่อจับ data race; deadlock บางส่วนต้องใช้ `pprof` ในการวิเคราะห์ - ห้ามเก็บ `context.Context` ใน struct — ส่งเป็นพารามิเตอร์ตัวแรกของฟังก์ชัน - Rate limiting ใน Go รวม semaphore channel กับ ticker ห่อหุ้มใน select statement ที่รับรู้ context --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/go/go-technical-interview-goroutines-channels-concurrency