บทเรียน 22 · Day 6 · Concurrency

Channel: ท่อที่ทำให้ goroutine คุยกันได้

~17 นาที · ต้องมี: บทเรียน 21, บทเรียน 11 (comma-ok)

ถ้า goroutine คือนักวิ่งหลายคนที่วิ่งพร้อมกัน channel คือไม้ต่อในการวิ่งผลัด — ส่งของถูกจังหวะ ไม่มีใครวิ่งชนกัน
บทนี้มีกับดัก 4 อันที่ทำให้โปรแกรมค้างหรือ panic ทุกอันผมรันให้ดูแล้วว่าพังยังไง เพราะรู้จักหน้าตามันไว้ จะแก้ได้ในสามวินาทีแทนที่จะนั่งงงสามชั่วโมง

1 · ปรัชญาที่อยู่เบื้องหลัง

"Do not communicate by sharing memory; instead, share memory by communicating."
— คำขวัญของ Go เรื่อง concurrency

แปลเป็นภาษาคน: แทนที่จะให้ทุก goroutine แย่งกันเขียนตัวแปรก้อนเดียว (ซึ่งต้องใส่ lock — กุญแจที่กันไม่ให้คนอื่นแตะของชิ้นนั้นระหว่างที่เรากำลังแก้ — และพังง่ายมาก บทเรียน 24 จะพิสูจน์ให้ดู) ให้ส่งข้อมูลผ่านท่อแทน — ของอยู่กับใครก็คนนั้นจัดการ

❌ แชร์ตัวแปรกัน

var total int

go func() { total++ }()
go func() { total++ }()

// สองตัวแย่งเขียนที่เดียวกัน
// ผลลัพธ์เดาไม่ได้ (บทเรียน 24)

✅ ส่งผ่านท่อ

ch := make(chan int)

go func() { ch <- 1 }()
go func() { ch <- 1 }()

total := <-ch + <-ch
// คนรับคนเดียว ไม่มีใครแย่งกัน

2 · สร้าง channel — ต้องใช้ make

chInt := make(chan int)
chStr := make(chan string)

type Order struct {
    ID    int
    Total float64
}
orders := make(chan Order)   // ส่ง struct ก็ได้

⚠️ ประกาศเฉย ๆ ไม่พอ — var ch chan int ได้ nil channel และ ch == nil เป็น true
อ่านหรือเขียน nil channel = ค้างตลอดกาล ไม่ panic ไม่ error แค่หยุดนิ่ง
ในโปรแกรมเล็กที่ไม่มี goroutine อื่นเลย Go จับได้และบอกตรง ๆ ว่า goroutine 1 [chan receive (nil chan)] แต่ในโปรแกรมจริงที่ยังมีตัวอื่นทำงานอยู่ runtime จะไม่ฟ้องอะไรเลย — goroutine ตัวนั้นค้างหายไปเงียบ ๆ ตัวเดียว
ต้อง make เสมอ

3 · เครื่องหมาย <- — ลูกศรชี้ทางเดินของข้อมูล

ch <- 10          // ส่ง 10 เข้า channel   (ลูกศรชี้เข้า ch)

value := <-ch     // รับค่าออกมา          (ลูกศรชี้ออกจาก ch)

<-ch              // รับแล้วทิ้ง — ใช้เมื่อสนใจแค่ "จังหวะ" ไม่สนค่า
   ch <- 10          value := <-ch

   ┌────┐              ┌────┐
   │ 10 │──►[ ch ]     │ ch │──► value
   └────┘              └────┘
   ลูกศรอยู่ขวา ch      ลูกศรอยู่ซ้าย ch
   = ส่งเข้า            = ดึงออก

   จำง่าย ๆ: ลูกศรชี้ไปทางไหน ข้อมูลไหลไปทางนั้น

4 · Unbuffered channel — ต้องมีทั้งคนส่งและคนรับ

make(chan string) ไม่ระบุขนาด = ความจุ 0
แปลว่าไม่มีที่พักของ — คนส่งต้องยืนรอจนกว่าจะมีคนมารับถึงมือ และคนรับก็ต้องรอจนกว่าจะมีคนส่ง

คำว่า "รอ" ตรงนี้แปลตรงตัว: โปรแกรมหยุดค้างที่บรรทัดนั้นจริง ๆ ไปบรรทัดถัดไปไม่ได้ จนกว่าอีกฝ่ายจะมา

📦 เทียบให้เห็นภาพ — พัสดุที่ต้องรับถึงมือ

ลองนึกถึงพัสดุที่ต้องเซ็นรับเอง วางหน้าบ้านไม่ได้
· ไรเดอร์ถึงก่อน → ต้องยืนรอหน้าประตู จนกว่าเราจะออกไปรับ
· เราออกไปยืนรอก่อน → ก็ต้องยืนรอ จนกว่าไรเดอร์จะมา
ของเปลี่ยนมือได้ก็ต่อเมื่อทั้งสองฝ่ายมาถึงพร้อมกัน
unbuffered channel เป็นแบบนี้เป๊ะ · ch <- "Hello" คือ "ยืนถือของรอ" และ <-ch คือ "ยืนแบมือรอ"

ที่ไม่เหมือนกัน: ไรเดอร์รอสักพักแล้วเอาของกลับได้ แต่ Go รอตลอดกาล ไม่มีเวลาหมดอายุ ไม่มีการวางทิ้งไว้ (วิธีบอกว่า "รอเกินเท่านี้แล้วเลิกรอ" อยู่ในบทเรียน 23)

❌ ส่งคนเดียว ไม่มีใครรับ

ch := make(chan string)

ch <- "Hello"       // ค้างตรงนี้ตลอดกาล

fmt.Println(<-ch)   // ไปไม่ถึงบรรทัดนี้

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]

✅ แยกคนส่งไปอยู่คนละ goroutine

ch := make(chan string)

go func() {
    ch <- "Hello"
}()

fmt.Println(<-ch)   // Hello

main รอรับ · goroutine ส่ง — เจอกันพอดี

💡 มุมที่ทำให้เข้าใจ unbuffered channel จริง ๆ

unbuffered channel ไม่ได้ทำหน้าที่ "เก็บของ" แต่ทำหน้าที่ "นัดเจอ" — ทั้งสองฝ่ายต้องมาถึงจุดนัดพร้อมกันถึงจะเดินต่อได้
เพราะแบบนี้มันจึงใช้เป็นสัญญาณจับเวลาได้ด้วย: เมื่อ ch <- x ผ่านไปได้ คุณรู้แน่นอน ว่ามีคนรับไปแล้ว

🗣 ศัพท์ที่เพิ่งเรียนไป

ใช้คำให้ตรง แล้วคำตอบที่ได้จาก senior หรือจาก AI จะตรงตาม

unbuffered channel
channel ที่ไม่มีที่พักของ (ความจุ 0) ต้องส่งถึงมือกันเท่านั้น · เวลาถามให้บอกไปเลยว่า “channel ตัวนี้ unbuffered หรือ buffered” จะได้คำตอบตรงกว่าถามลอย ๆ ว่า “ทำไมโปรแกรมค้าง”
block / blocking
หยุดค้างที่บรรทัดนั้น รอจนเงื่อนไขครบถึงไปต่อ · พูดว่า “มัน block ที่บรรทัด ch <- x” จะสื่อกว่า “โปรแกรมมันค้าง ๆ”
deadlock
ฝ่ายที่รอกันเอง ไม่มีใครขยับไปต่อได้เลย เพราะต่างคนต่างรออีกฝ่ายก่อน · จุดที่มือใหม่สรุปผิดบ่อยมาก: ข้อความ fatal error: all goroutines are asleep ที่เห็นข้างบนนั้น Go ฟ้องให้เฉพาะตอน ทุก goroutine ในโปรแกรมรอพร้อมกันหมดเท่านั้น · ถ้ามีแค่บาง goroutine รอกันเองอยู่ แล้ว goroutine อื่นยังวิ่งได้ Go จะเงียบสนิท ไม่ฟ้องอะไรเลย โปรแกรมแค่ค้างไปเฉย ๆ — อันนั้นก็เป็น deadlock เต็มตัว แต่ต้องหาเอง · “ไม่ขึ้น fatal error” จึงไม่ได้แปลว่า “ไม่มี deadlock” · ใช้พูดว่า “เจอ deadlock ตอนส่งเข้า unbuffered channel จาก main โดยไม่มีคนรับ”
synchronous / hand-off
จังหวะที่คนส่งกับคนรับต้องมาเจอกันพอดีถึงจะส่งของได้ · ถ้า senior พูดว่า “unbuffered channel มันเป็น synchronous” เขากำลังพูดถึงสิ่งที่คุณเพิ่งอ่านจบไปทั้งหัวข้อนี้

5 · Buffered channel — มีที่พักของ

ch := make(chan string, 3)   // ความจุ 3

เลขที่ใส่เพิ่มเข้าไป = ขอที่พักของไว้กี่ชิ้น · คนส่งจึงไม่ต้องยืนรอคนรับอีกต่อไป ตราบใดที่ที่พักยังไม่เต็ม

🔐 เทียบให้เห็นภาพ — ตู้รับพัสดุหน้าคอนโด

คอนโดหลายที่มีตู้รับพัสดุอัตโนมัติ สมมติมี 3 ช่อง
· ช่องยังว่าง → ไรเดอร์หย่อนใส่แล้วขับต่อได้เลย ไม่ต้องรอเจอเรา
· ช่องเต็มทั้ง 3 → ไรเดอร์คนที่ 4 ต้องยืนรอ จนมีคนมาเปิดเอาของออกสักช่อง
· เราลงมาเปิดตู้ตอนไม่มีของ → ก็ต้องรอจนมีคนมาหย่อน
นี่คือเหตุผลที่ make(chan string, 2) ส่ง "A" กับ "B" ได้สบาย ๆ โดยไม่ต้องมี goroutine อื่น แต่พอส่ง "C" เป็นตัวที่ 3 ปุ๊บ ค้างทันที

ที่ไม่เหมือนกัน: ตู้พัสดุจริงเราเลือกเปิดช่องไหนก่อนก็ได้ แต่ channel หยิบออกตามลำดับที่ใส่เข้าไปเท่านั้น — ใส่ก่อนออกก่อน เลือกไม่ได้
และที่พักไม่ได้แก้ปัญหา แค่เลื่อนมันออกไป: ถ้าคนรับช้ากว่าคนส่งเรื่อย ๆ เดี๋ยวก็เต็มแล้วกลับมาค้างอยู่ดี

   ┌─────┬─────┬─────┐
   │  A  │  B  │  C  │   ← เต็มแล้ว คนส่งคนที่ 4 ต้องรอ
   └─────┴─────┴─────┘

   ส่ง: buffer ยังว่าง → ผ่านเลย ไม่ต้องรอคนรับ
        buffer เต็ม  → block จนกว่าจะมีคนดึงออก
   รับ: มีของ       → ได้เลย
        ว่างเปล่า    → block จนกว่าจะมีคนส่ง (ยกเว้น channel ถูกปิด)

✅ ส่งไม่เกินความจุ

ch := make(chan string, 2)

ch <- "A"
ch <- "B"

fmt.Println(<-ch)  // A
fmt.Println(<-ch)  // B

ไม่ต้องมี goroutine อื่นก็ได้

❌ ส่งเกินความจุ

ch := make(chan string, 2)

ch <- "A"
ch <- "B"
ch <- "C"      // ค้างตรงนี้

deadlock! goroutine 1 [chan send]

🗣 ศัพท์ที่เพิ่งเรียนไป
buffered channel
channel ที่มีที่พักของ — คือตัวที่ใส่เลขตอน make · ประโยคที่ถามแล้วได้คำตอบตรง: “ตรงนี้ควรใช้ buffered channel ขนาดเท่าไหร่ดี”
capacity และ length
คนละเรื่องกัน · capacity = มีกี่ช่องทั้งหมด (เลขที่ใส่ตอน make) · length = ตอนนี้มีของค้างอยู่กี่ชิ้น · อ่านค่าได้ด้วย cap(ch) กับ len(ch)
backpressure
แรงดันกลับ — สัญญาณที่ปลายทางดันกลับมาบอกต้นทางว่า “ตามไม่ทันแล้ว ช้าลงหน่อย” · ในที่นี้คือที่พักเต็มจนคนส่งถูกบังคับให้รอ · มีไว้ดีกว่าไม่มี (ถ้าไม่มี ของจะกองสะสมจนหน่วยความจำบวม) แต่ไม่ได้แปลว่าไม่ต้องแก้อะไร — ถ้าเจอบ่อย ๆ แปลว่าฝั่งบริโภคช้าเกินไป ต้องไปแก้ตรงนั้น ไม่ใช่ขยาย buffer หนี · ใช้พูดว่า “channel เต็ม เกิด backpressure กลับไปที่ฝั่งส่ง”

6 · close และ for range

ch := make(chan string, 3)
ch <- "A"
ch <- "B"
ch <- "C"

for val := range ch {
    fmt.Println(val)
}

ของอยู่ครบ 3 ตัว วนอ่านออกมาหมดแล้วจะจบสวย ๆ ใช่ไหม?

ผมรันแล้วได้:

A
B
C
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:

อ่านครบแล้วยังค้าง — เพราะ for range ch ไม่ได้แปลว่า "วนตามจำนวนของที่มี" แต่แปลว่า "วนไปเรื่อย ๆ จนกว่า channel จะถูกปิด"

พออ่านครบ 3 ตัว มันไปรออันที่ 4 ต่อ ซึ่งไม่มีวันมา
วิธีแก้: ใส่ close(ch) ก่อนเข้า loop

ch <- "C"
close(ch)          // ← บรรทัดนี้

for val := range ch {   // A B C แล้วจบเรียบร้อย
    fmt.Println(val)
}
🍣 เทียบให้เห็นภาพ — สายพานร้านซูชิกับป้ายปิดครัว

เรานั่งริมสายพาน หยิบจานที่ไหลมาเรื่อย ๆ · อยู่ ๆ สายพานว่างลง — แล้วเราลุกกลับบ้านเลยไหม? ไม่ เพราะสายพานว่างไม่ได้แปลว่าร้านปิด อาจแค่ครัวทำไม่ทัน · เราจึงนั่งรอจานถัดไปต่อ จนกว่าครัวจะประกาศว่าปิดครัว
for val := range ch คือคนที่นั่งรอริมสายพาน · close(ch) คือประกาศปิดครัว
ใส่ของ 3 ชิ้นแล้วไม่ประกาศ = อ่านครบ 3 แล้วนั่งรอชิ้นที่ 4 ที่ไม่มีวันมา = ค้าง

ที่ไม่เหมือนกัน: ประกาศปิดครัวแล้ว ของที่ยังอยู่บนสายพานยังหยิบได้จนหมด ไม่ได้หายวับไปทันที · และร้านจริงไม่มีกฎว่า ประกาศซ้ำรอบสองคือ panic — ข้อนี้ภาษาบังคับ พังจริง · ส่วน “คนกินไม่ประกาศปิดครัวแทนครัว” เป็นกฎที่เราตั้งกันเอง ไม่ใช่ข้อห้ามของภาษา: ถ้าฝั่งรับถือตัวแปรแบบสองทาง (chan T) มันเรียก close(ch) ได้จริง คอมไพล์ผ่าน รันผ่าน ไม่ panic · ที่ปิดไม่ได้จริง ๆ คือตอนถือแบบรับอย่างเดียว (<-chan T) — อันนั้นคอมไพล์ไม่ผ่าน (cannot close receive-only channel) · เหตุผลที่ห้ามฝั่งรับปิดคือ ถ้าฝั่งส่งยังส่งอยู่ มันจะ panic: send on closed channel ทันที

ในโค้ดจริง คนส่งเป็นคนปิด และมักปิดด้วย defer:

func producer(ch chan int) {
    defer close(ch)      // ส่งครบแล้วปิดเสมอ
    for i := 1; i <= 5; i++ {
        ch <- i
    }
}

func main() {
    ch := make(chan int)
    go producer(ch)

    for val := range ch {   // 1 2 3 4 5 แล้วจบ
        fmt.Println(val)
    }
    fmt.Println("Done!")
}
💡 กฎการปิด channel ที่ตอบได้ทุกกรณี

คนส่งเป็นคนปิด ไม่ใช่คนรับ — เพราะคนส่งเท่านั้นที่รู้ว่า "ส่งครบแล้ว"
มีคนส่งหลายคน? ห้ามให้ใครปิดเอง — ให้ตัวที่รอทุกคนเสร็จเป็นคนปิด (จะเห็นสำนวนนี้ในบทเรียน 25)
ไม่ปิดเลยก็ได้ ถ้าไม่มีใคร for range — channel ที่ไม่มีใครใช้แล้วจะถูก garbage collect ไปเอง (garbage collector = ตัวเก็บกวาดหน่วยความจำที่ไม่มีใครใช้แล้ว Go จัดการให้อัตโนมัติ) การไม่ปิดไม่ใช่ memory leak

🗣 ศัพท์ที่เพิ่งเรียนไป
close
ไม่ใช่ "ลบทิ้ง" และไม่ใช่ "คืนหน่วยความจำ" แต่แปลว่า “ประกาศว่าจะไม่มีของส่งเข้ามาอีกแล้ว” · คำถามแรกที่ senior จะถามเสมอคือ “แล้วใครเป็นคนปิด channel ตัวนี้”
producer / consumer
ฝั่งผลิต (คนส่งเข้า channel) กับ ฝั่งบริโภค (คนวนอ่านออกมา) · โครงแบบนี้เรียกว่า producer–consumer — พูดชื่อนี้ออกไปคนเห็นภาพโค้ดทันทีโดยไม่ต้องเล่ายาว
goroutine leak
goroutine ที่ค้างรอ channel ตลอดกาลจนไม่จบสักที โดยไม่มี error ฟ้องสักตัว · ตัวมันเองไม่ได้โตขึ้นเรื่อย ๆ — กินคงที่ (stack ของมัน บวกของที่มันยังถืออยู่ ซึ่งตัวเก็บกวาดหน่วยความจำเก็บไม่ได้เพราะยังมีคนถือ) · ที่โตขึ้นเรื่อย ๆ คือจำนวนของมัน — ถ้ารั่วซ้ำทุก request มันจะสะสมจนหน่วยความจำบวม · ใช้ถามว่า “ตรงนี้จะเกิด goroutine leak ไหม ถ้าไม่มีใครอ่าน channel ต่อ”

7 · ปิดแล้วเกิดอะไรขึ้น

ก่อนอ่านตาราง ต้องรู้จักคำว่า zero value ก่อน — คือค่าเริ่มต้นประจำ type ที่ Go แจกให้เมื่อไม่มีค่าจริงจะให้ · int ได้ 0 · string ได้ "" · bool ได้ false · pointer กับ interface ได้ nil · slice, map, chan, func ก็ได้ nil เหมือนกัน · ส่วน struct ได้ struct ที่ทุกช่องข้างในเป็นค่าเริ่มต้นของช่องนั้นเอง

ทำอะไรกับ channel ที่ปิดแล้วผล
รับ (<-ch) ตอนยังมีของค้างได้ของนั้นตามปกติ
รับ ตอนไม่มีของแล้วได้ zero value ทันที ไม่ block
for rangeวนจนของหมดแล้วออกจาก loop
ส่ง (ch <- x)💥 panic: send on closed channel
close ซ้ำอีกรอบ💥 panic: close of closed channel
ch := make(chan int, 1)
ch <- 0              // ส่งเลขศูนย์เข้าไปจริง ๆ
close(ch)

v, ok := <-ch
fmt.Println(v, ok)

v, ok = <-ch
fmt.Println(v, ok)

สองบรรทัดนี้พิมพ์อะไร — และต่างกันตรงไหน?

0 true
0 false

v เป็น 0 เหมือนกันทั้งสองครั้ง — ครั้งแรกคือ "ศูนย์ที่ถูกส่งมาจริง" ครั้งที่สองคือ "ศูนย์เพราะไม่มีอะไรแล้ว"
ดูจากค่าอย่างเดียวแยกไม่ออก

ตัวที่แยกให้คือ ok — นี่คือเหตุผลที่ต้องมี comma-ok รูปแบบเดียวกับ type assertion (บทเรียน 11) และ map lookup
ถ้า channel ส่งของที่ zero value เป็นค่าที่ถูกต้องได้ (0, "", false) — ต้องใช้ comma-ok เสมอ

8 · Directional channel — จำกัดทิศทางที่ parameter

// ส่งได้อย่างเดียว — ลูกศรชี้เข้า chan
func producer(ch chan<- int) {
    defer close(ch)
    for i := 1; i <= 5; i++ {
        ch <- i
    }
}

// รับได้อย่างเดียว — ลูกศรชี้ออกจาก chan
func consumer(ch <-chan int) {
    for val := range ch {
        fmt.Println(val)
    }
}

// ตอนเรียก ส่ง channel ปกติเข้าไปได้เลย Go แปลงให้เอง
ch := make(chan int)
go producer(ch)
consumer(ch)
   chan<- int     ลูกศรอยู่หลัง chan  →  ส่งเข้าอย่างเดียว
   <-chan int     ลูกศรอยู่หน้า chan  →  รับออกอย่างเดียว

   วิธีจำ: อ่านจากซ้ายไปขวาตามลำดับที่เขียน
   chan <- int  =  "channel รับ int เข้าไป"
   <- chan int  =  "ดึงออกจาก channel ได้ int"

ประโยชน์คือ compiler บังคับสัญญาให้ — consumer จะเผลอส่งของเข้าหรือปิด channel ไม่ได้เลย:

cannot close receive-only channel ch (variable of type <-chan int)

เขียน production code ให้ใช้ directional channel ที่ parameter เสมอ — เป็นการเขียน "ใครทำอะไรได้" ลงไปในลายเซ็นฟังก์ชัน คนอ่านไม่ต้องไล่โค้ดข้างใน และ compiler ช่วยกันพลาดให้
ตรงกันกับหลัก "accept interfaces, return structs" ที่คุณเจอใน บทเรียน 10: รับให้แคบที่สุดเท่าที่ต้องใช้

ทดสอบความเข้าใจ

1. ch := make(chan string) แล้ว ch <- "Hello" ใน main เกิดอะไร?

ไม่ระบุขนาด = ความจุ 0 = ไม่มีที่พักของ คนส่งต้องรอจนมีคนมารับถึงมือ แต่ main ค้างอยู่ตรงนั้นแล้ว ไม่มีใครไปอ่านได้
fatal error: all goroutines are asleep - deadlock! · goroutine 1 [chan send]
แก้โดยย้ายฝั่งส่ง (หรือฝั่งรับ) ไปอยู่คนละ goroutine

2. for val := range ch จะออกจาก loop เมื่อไหร่?

range บน channel ไม่รู้จัก "จำนวน" — มันวนไปเรื่อย ๆ รอ close อย่างเดียว
ใส่ของ 3 ตัวแล้วไม่ปิด → อ่านครบ 3 แล้วไปรอตัวที่ 4 → deadlock
ในโค้ดจริงนิยมเขียน defer close(ch) ไว้ในฟังก์ชันฝั่งส่ง

3. รับจาก channel ที่ปิดแล้วและไม่มีของเหลือ ได้อะไร?

รับจาก channel ที่ปิดแล้วไม่ block — ได้ zero value กลับมาทันที (0 · "" · nil แล้วแต่ type)
ที่ panic คือการส่งเข้า channel ที่ปิด: panic: send on closed channel
เพราะค่าที่ได้แยกไม่ออกจาก "ศูนย์จริง ๆ" จึงต้องใช้ v, ok := <-ch

4. func consumer(ch <-chan int) ทำอะไรกับ ch ไม่ได้?

<-chan int = รับได้อย่างเดียว → cannot close receive-only channel และส่งเข้าก็ไม่ได้
อ่าน · range · ส่งต่อให้ฟังก์ชันที่รับ <-chan — ทำได้หมด
เป็นการเขียนสัญญาไว้ในลายเซ็น ให้ compiler บังคับแทนคน

🛠️ ภารกิจ — สายพานร้านอาหาร

ทำในเครื่อง ทุกข้อที่ให้ "ทำให้พัง" ต้องจดคำในวงเล็บเหลี่ยม ของบรรทัด goroutine 1 [...] ไว้ด้วย

  1. สร้าง type Order struct { ID int; Menu string } และ channel orders := make(chan Order)
  2. เขียน kitchen(orders chan<- Order) ที่ส่งออเดอร์ 3 รายการ (Burger, Pizza, Bubble Tea) แล้ว defer close
    ใช้ directional channel ให้ถูกทิศ
  3. ใน main สั่ง go kitchen(orders) ก่อน — ต้องมีคำว่า go ไม่งั้น main จะติดอยู่ในตัว kitchen และไปไม่ถึงบรรทัดถัดไป (รูปแบบเดียวกับ go producer(ch) ในหัวข้อ 6)
    จากนั้นวน for order := range orders พิมพ์ ✅ Burger ready ทีละอัน แล้วปิดท้ายด้วย All orders completed 🎉
    ถูกแล้วเมื่อได้ 3 บรรทัดเรียงตามลำดับที่ส่ง ตามด้วยบรรทัดปิดท้าย และโปรแกรมจบเองโดยไม่ค้าง
  4. ทำให้พัง 1: ลบ defer close(orders) ออก → เกิดอะไร? อยู่ที่บรรทัดไหน?
  5. ทำให้พัง 2: ใส่ close กลับ แล้วเพิ่ม orders <- Order{...} อีกอันหลัง close → ได้ error ว่าอะไร?
  6. ทำให้พัง 3: ย้าย kitchen(orders) ออกจาก go (เรียกตรง ๆ) → เกิดอะไร? ทำไม?
  7. ทำให้พัง 4: ลองสั่ง close(orders) ข้างใน main ทั้งที่ kitchen ปิดไปแล้ว
  8. เปลี่ยนเป็น buffered: ทำ make(chan Order, 3) แล้วลองเรียก kitchen(orders) แบบไม่มี go อีกครั้ง — คราวนี้ผ่านไหม? ทำไมถึงต่างจากข้อ 6?

ข้อ 6 (unbuffered, ไม่มี go): ค้างที่ orders <- ... ตัวแรกเลย — deadlock! goroutine 1 [chan send]
main ยังไม่ได้ไปถึงบรรทัด for range เพราะติดอยู่ในตัว kitchen → ไม่มีใครรับ → ค้าง

ข้อ 8 (buffered 3, ไม่มี go): ผ่าน! เพราะ buffer รับ 3 ตัวไว้ได้โดยไม่ต้องมีคนรับ kitchen จึงส่งครบแล้ว close แล้ว return ออกมาได้ จากนั้น main ค่อยวนอ่าน
แต่ถ้าเพิ่มเป็น 4 ออเดอร์บน buffer 3 → กลับมาค้างที่ตัวที่ 4 ลองดูด้วย จะเห็นว่า buffer แค่เลื่อนปัญหา ไม่ได้แก้

💬 ข้อ 8 คือหัวใจของบทนี้ — คนมักคิดว่า "ใส่ buffer เยอะ ๆ ไว้ก่อน เดี๋ยวก็ไม่ค้าง" ซึ่งทำให้บั๊กไปโผล่ตอน load สูงแทน buffer มีไว้ลดการรอ ไม่ได้มีไว้แก้ deadlock
บทถัดไป: select — รอหลาย channel พร้อมกัน และวิธีไม่ให้ต้องรอตลอดกาล

อ้างอิง