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 ไม่ได้ทำหน้าที่ "เก็บของ" แต่ทำหน้าที่
"นัดเจอ" — ทั้งสองฝ่ายต้องมาถึงจุดนัดพร้อมกันถึงจะเดินต่อได้
เพราะแบบนี้มันจึงใช้เป็นสัญญาณจับเวลาได้ด้วย:
เมื่อ 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!")
}
คนส่งเป็นคนปิด ไม่ใช่คนรับ —
เพราะคนส่งเท่านั้นที่รู้ว่า "ส่งครบแล้ว"
มีคนส่งหลายคน? ห้ามให้ใครปิดเอง —
ให้ตัวที่รอทุกคนเสร็จเป็นคนปิด (จะเห็นสำนวนนี้ในบทเรียน 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 [...] ไว้ด้วย
- สร้าง
type Order struct { ID int; Menu string }และ channelorders := make(chan Order) - เขียน
kitchen(orders chan<- Order)ที่ส่งออเดอร์ 3 รายการ (Burger, Pizza, Bubble Tea) แล้วdefer close
ใช้ directional channel ให้ถูกทิศ - ใน
mainสั่งgo kitchen(orders)ก่อน — ต้องมีคำว่าgoไม่งั้นmainจะติดอยู่ในตัวkitchenและไปไม่ถึงบรรทัดถัดไป (รูปแบบเดียวกับgo producer(ch)ในหัวข้อ 6)
จากนั้นวนfor order := range ordersพิมพ์✅ Burger readyทีละอัน แล้วปิดท้ายด้วยAll orders completed 🎉
ถูกแล้วเมื่อได้ 3 บรรทัดเรียงตามลำดับที่ส่ง ตามด้วยบรรทัดปิดท้าย และโปรแกรมจบเองโดยไม่ค้าง - ทำให้พัง 1: ลบ
defer close(orders)ออก → เกิดอะไร? อยู่ที่บรรทัดไหน? - ทำให้พัง 2: ใส่
closeกลับ แล้วเพิ่มorders <- Order{...}อีกอันหลังclose→ ได้ error ว่าอะไร? - ทำให้พัง 3: ย้าย
kitchen(orders)ออกจากgo(เรียกตรง ๆ) → เกิดอะไร? ทำไม? - ทำให้พัง 4: ลองสั่ง
close(orders)ข้างในmainทั้งที่kitchenปิดไปแล้ว - เปลี่ยนเป็น 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 พร้อมกัน
และวิธีไม่ให้ต้องรอตลอดกาล
อ้างอิง
- A Tour of Go — Channels ถึงหน้า 6 · รันได้ในหน้าเว็บ
- Share Memory By Communicating — ที่มาของคำขวัญในหัวข้อ 1
- Go Spec — Channel types · Close
- Effective Go — Channels