Channels
Channel คืออะไร?
หัวข้อที่มีชื่อว่า “Channel คืออะไร?”Channel คือท่อส่งข้อมูลแบบ typed ที่ goroutines ใช้สื่อสาร การส่งค่าเข้า channel และรับจาก channel เป็นสองคำสั่งที่ถ่ายโอนทั้งข้อมูลและการ synchronize ในขั้นตอนเดียว
ch := make(chan int) // unbuffered channel of intch <- 42 // send (blocks until a receiver is ready)v := <-ch // receive (blocks until a sender is ready)Type system บังคับสิ่งที่ไหลผ่าน channel chan string ไม่ยอมรับ int
Unbuffered vs Buffered
หัวข้อที่มีชื่อว่า “Unbuffered vs Buffered”| Unbuffered | Buffered | |
|---|---|---|
| สร้างด้วย | make(chan T) | make(chan T, capacity) |
| Send บล็อก? | ใช่ — จนกว่า receiver พร้อม | ไม่ — จนกว่า buffer เต็ม |
| Receive บล็อก? | ใช่ — จนกว่า sender พร้อม | ไม่ — จนกว่า buffer ว่าง |
| Synchronize? | ใช่ — sender และ receiver พบกันในเวลาเดียวกัน | ไม่ — buffer แยกเวลาออกจากกัน |
Unbuffered channels ให้ synchronization guarantee ที่แข็งแกร่งที่สุด: sender รู้ว่า receiver ได้รับค่าแล้วก่อนที่จะดำเนินต่อ
ทิศทางของ Channel
หัวข้อที่มีชื่อว่า “ทิศทางของ Channel”คุณสามารถจำกัด channel parameter ให้เป็น send-only หรือ receive-only เพื่อแสดง intent ใน type:
func producer(out chan<- int) { out <- 1 } // send-onlyfunc consumer(in <-chan int) { v := <-in; _ = v } // receive-onlychan T ที่เป็นสองทิศทางสามารถ assign ให้กับ direction ไหนก็ได้ Compiler จะบังคับว่า receive-only channel จะถูกส่งเข้าไม่ได้ และในทางกลับกันก็เช่นกัน
การปิด Channel และ Range
หัวข้อที่มีชื่อว่า “การปิด Channel และ Range”Sender เรียก close(ch) เพื่อบอกว่าจะไม่มีค่าอีกแล้ว Receivers ใช้ for range loop เพื่อดึงค่าทั้งหมดจนกว่าจะ close:
close(ch)for v := range ch { // exits when ch is closed and empty fmt.Println(v)}Comma-ok Receive Idiom
หัวข้อที่มีชื่อว่า “Comma-ok Receive Idiom”Receive expression สามารถ return boolean ตัวที่สองที่บอกว่า channel ยังเปิดอยู่หรือไม่:
v, ok := <-ch// ok == true → v is a real value// ok == false → channel is closed and drained; v is the zero valueNil Channel
หัวข้อที่มีชื่อว่า “Nil Channel”Channel variable ที่แค่ประกาศ แต่ไม่เคย make คือ nil:
var ch chan int // nil — not the same as make(chan int)ทุก operation บน nil channel จะ block ตลอดไป — ไม่ panic, block คือ send และ receive บน nil channel ทั้งคู่ทำให้ goroutine หยุดรอโดยไม่มีอะไรมาปลุกมันได้เลย:
var ch chan int<-ch // blocks foreverถ้ารันเป็น goroutine เดียวใน main นี่คือ deadlock จริง ๆ: สถานะที่ชุด goroutine ทั้งหมด block รออยู่ โดยแต่ละตัวรอสิ่งที่มีแต่ goroutine อื่นที่ block อยู่ในชุดเดียวกันเท่านั้นที่ให้ได้ ทำให้ไม่มีตัวไหนไปต่อได้เลย Go runtime ตรวจจับกรณีเฉพาะที่ ทุก goroutine ใน program block พร้อมกัน แล้วจบด้วย fatal error ไม่ใช่ panic — recover() เรียกไม่ได้:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]:main.main() /tmp/nilchandeadlock/main.go:5 +0x2cexit status 2Tag [chan receive (nil chan)] ใน goroutine trace คือตัวบอกใบ้ — มันระบุ nil channel เป็นสาเหตุตรง ๆ ไม่ใช่แค่ “block เฉย ๆ” nil channel send ก็ deadlock เหมือนกัน รายงานเป็น [chan send (nil chan)] นี่คือเหตุผลที่ struct field แบบ chan ที่เป็น zero value ที่ลืม initialize เป็นบัค AI-generated คลาสสิก: มัน compile ผ่าน แล้ว hang ทันทีที่มีอะไรไปแตะมันครั้งแรก
Nil channel ไม่ได้เป็นแค่บัคเสมอไป — case ที่เป็น nil ใน select จะถูก ปิดใช้งาน แทนที่จะ block ซึ่งเป็นเทคนิคที่ตั้งใจใช้ อธิบายในบท Select
ตาราง Channel Operation ฉบับสมบูรณ์
หัวข้อที่มีชื่อว่า “ตาราง Channel Operation ฉบับสมบูรณ์”สาม operation — send, receive, close — ทำงานต่างกันขึ้นอยู่กับว่า channel เป็น nil, open, หรือ closed:
| Operation | nil channel | open channel | closed channel |
|---|---|---|---|
ch <- v (send) | block ตลอดไป | block จนกว่า receiver พร้อม (unbuffered) หรือ buffer มีที่ว่าง | panic: send on closed channel |
<-ch (receive) | block ตลอดไป | block จนกว่ามีค่า | return ทันที: zero value, ok == false |
close(ch) | panic: close of nil channel | close สำเร็จ — receiver ที่ block อยู่ unblock ทันที | panic: close of closed channel |
len(ch) | 0 | จำนวน element ใน buffer ที่ยังไม่ถูกรับ | จำนวน element ใน buffer ที่ยังเหลือ |
cap(ch) | 0 | ความจุของ buffer | ความจุของ buffer (ไม่เปลี่ยนหลัง close) |
panic message ทั้งสาม และ deadlock message ด้านบน copy มาจาก output จริงของ go run บน Go 1.26.5 คำเดียวที่ต้องจำ: nil block, closed panic ตอน send และไม่ block เลยตอน receive
package main
import "fmt"
func producer(ch chan<- int, n int) { for i := 1; i <= n; i++ { ch <- i } close(ch)}
func main() { // Unbuffered channel: producer runs in a goroutine ch := make(chan int) go producer(ch, 5) for v := range ch { fmt.Println(v) }
// Buffered channel: all sends fit in the buffer buf := make(chan string, 3) buf <- "a" buf <- "b" buf <- "c" close(buf) for s := range buf { fmt.Println(s) }
// Comma-ok: distinguish real value from closed zero value done := make(chan bool, 1) done <- true close(done) v1, ok1 := <-done fmt.Println(v1, ok1) // true true v2, ok2 := <-done fmt.Println(v2, ok2) // false false} close คือสัญญาณ Broadcast
หัวข้อที่มีชื่อว่า “close คือสัญญาณ Broadcast”การ close channel จะปลุก ทุก goroutine ที่ block รออยู่บนมัน ไม่ใช่แค่ตัวเดียว — ต่างจาก receive ปกติที่ส่งค่าให้ goroutine ที่รออยู่แค่ตัวเดียวเท่านั้น เมื่อรวมกับ chan struct{} นี่คือวิธี idiomatic ในการ broadcast “stop” หรือ “ready” ไปยัง goroutine จำนวนไม่จำกัดพร้อมกัน
struct{} คือ empty struct — ไม่มี field เลยและกินพื้นที่ 0 byte chan struct{} ถูกใช้เป็นสัญญาณล้วน ๆ ไม่มีใครสนใจค่าที่รับได้ สนใจแค่ว่า receive มัน unblock:
done := make(chan struct{})
go func() { <-done // unblocks the instant done is closed fmt.Println("goroutine 1 saw the signal")}()go func() { <-done // unblocks at the same instant, independently fmt.Println("goroutine 2 saw the signal")}()
close(done) // broadcasts to both goroutines simultaneouslyถ้าส่งค่าเดียวเข้า done จะปลุกแค่หนึ่งใน goroutine ทั้งสอง — อีกตัวจะ block ตลอดไป close เป็น channel operation เดียวที่ไปถึงทุก waiter พร้อมกัน
Happens-before และ Channel Ownership
หัวข้อที่มีชื่อว่า “Happens-before และ Channel Ownership”Channel ไม่ใช่แค่ความสะดวกด้าน scheduling — มันเป็นส่วนหนึ่งของการรับประกันใน Go memory model เรื่องว่าเมื่อไหร่ write ที่ทำใน goroutine หนึ่งจะมองเห็นได้ใน goroutine อีกตัว กฎคือ: send บน channel เกิด happens-before receive ที่จับคู่กันเสร็จสมบูรณ์ และสำหรับ channel ที่มี capacity C การ send ครั้งที่ k จะ happens-before การ receive ครั้งที่ (k+C) เสร็จสมบูรณ์ ในทางปฏิบัติหมายความว่า variable ใด ๆ ที่ goroutine เขียน ก่อน มัน send บน channel จะมองเห็นได้แน่นอนโดย goroutine ที่อ่านมัน หลังจาก receive จาก channel นั้น — ไม่ต้องใช้ mutex หรือ atomic แยกสำหรับการส่งต่อนี้
การรับประกันนี้ยังเป็นเหตุผลที่ Go channel มี กฎ ownership ชัดเจน: goroutine ที่สร้าง channel คือตัวที่ send และ close มัน ส่วน goroutine อื่นทุกตัวถือแค่ <-chan T แบบ receive-only เท่านั้น ไม่เคย close หรือ send เลย function ที่ return channel ควร return เป็น directional type เพื่อให้ compiler บังคับได้ ไม่ใช่แค่ตาม convention:
// producer owns out: it is the only goroutine that sends to it or closes it.func producer(n int) <-chan int { out := make(chan int, n) go func() { defer close(out) // the owner closes, always for i := 1; i <= n; i++ { out <- i } }() return out // callers only ever see <-chan int}การละเมิดกฎนี้ — เช่น goroutine สองตัวคิดว่าตัวเองเป็นเจ้าของ channel เดียวกันและเรียก close ทั้งคู่ — คือบัคที่อยู่ใน section “อ่านโค้ดที่ AI เขียน” ด้านล่างของบทนี้เป๊ะ
Buffered Channel เป็น Semaphore
หัวข้อที่มีชื่อว่า “Buffered Channel เป็น Semaphore”Capacity ของ buffered channel ใช้เพื่อการนับล้วน ๆ ได้ ไม่ต้องส่งข้อมูลจริง — นี่คือ implementation แบบคลาสสิกของ counting semaphore ใน Go acquire permit ด้วยการ send เข้า channel (block เมื่อ buffer เต็ม); release ด้วยการ receive:
sem := make(chan struct{}, 2) // at most 2 permits
sem <- struct{}{} // acquire — blocks if 2 permits are already out// ... critical section, at most 2 goroutines here at once ...<-sem // releaseนี่คือรูปแบบง่ายที่สุดของ bounded-parallelism pattern ที่อธิบายเต็ม ๆ ในบท Patterns — ควรรู้จักไว้ตรงนี้เพราะเป็นการใช้ capacity ของ buffered channel แบบที่สอง ต่างจากแค่ “ช่วยปรับ timing ให้ราบรื่น”
snippet ด้านล่างรันทั้งสามไอเดีย: close เป็น broadcast ไปยัง 3 goroutine ที่รออยู่, chan struct{} ที่ buffer เป็น 2 ใช้เป็น semaphore, และ producer ที่เป็นเจ้าของ channel ของตัวเองตั้งแต่ต้นจนจบ
package main
import ( "fmt" "sort" "sync")
func main() { // close as broadcast: every receiver unblocks the instant the channel // closes, no matter how many goroutines are waiting. done := make(chan struct{}) var wg sync.WaitGroup results := make([]string, 3) for i := 0; i < 3; i++ { wg.Add(1) go func(id int) { defer wg.Done() <-done // unblocks for every goroutine at once when done is closed results[id] = fmt.Sprintf("worker %d saw the broadcast", id) }(i) } close(done) // broadcasts to all 3 waiters simultaneously wg.Wait() for _, r := range results { fmt.Println(r) }
// buffered channel as a counting semaphore: only 2 permits, so at most 2 // goroutines run their critical section concurrently. sem := make(chan struct{}, 2) var mu sync.Mutex inFlight, maxInFlight := 0, 0 var wg2 sync.WaitGroup for i := 0; i < 5; i++ { wg2.Add(1) go func() { defer wg2.Done() sem <- struct{}{} // acquire mu.Lock() inFlight++ if inFlight > maxInFlight { maxInFlight = inFlight } mu.Unlock()
mu.Lock() inFlight-- mu.Unlock() <-sem // release }() } wg2.Wait() fmt.Println("never exceeded 2 concurrent holders:", maxInFlight <= 2)
// ownership: the sender both sends and closes; the consumer only ever // holds a <-chan so it physically cannot close or send. ch := ownedProducer(3) var got []int for v := range ch { got = append(got, v) } sort.Ints(got) fmt.Println(got)}
// ownedProducer owns out: it is the only goroutine that sends to it or// closes it. Callers only ever see the receive-only <-chan int.func ownedProducer(n int) <-chan int { out := make(chan int, n) go func() { defer close(out) for i := 1; i <= n; i++ { out <- i } }() return out} Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart TD
subgraph Unbuf["Unbuffered channel -- rendez-vous point"]
G1u["G1"] -->|"send (blocks)"| CHu["channel"]
G2u["G2"] -->|"recv (blocks)"| CHu
CHu --> Meet["G1 and G2 must meet"]
end
subgraph Buf["Buffered channel (cap=3)"]
G1b["G1"] -->|"send"| Slots["[_][_][_]"]
Slots -->|"recv"| G2b["G2"]
Slots -.->|"blocks when full"| G1b
Slots -.->|"blocks when empty"| G2b
Slots --> Indep["G1/G2 run more independently"]
end flowchart LR
subgraph Lifecycle["chan T lifecycle"]
Nil["nil
(var ch chan T)"]
Open["open
(make(chan T, n))"]
Closed["closed
(after close(ch))"]
end
Nil -->|make chan T| Open
Open -->|close ch| Closed
Nil -.send / receive: blocks forever.-> Nil
Closed -.send: panic!.-> Closed
Closed -.receive: zero value, ok=false, never blocks.-> Closed ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| unbuffered channel | synchronization guarantee แข็งแกร่ง | G ส่ง block จนกว่า G รับพร้อม |
| buffered channel | decouple sender/receiver timing | ต้อง size buffer ให้เหมาะ, อาจ hide backpressure |
channel direction (chan<-, <-chan) | compile-time safety | verbose ใน function signature |
| close + range | notify completion ง่าย | ส่งหลัง close → panic |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- channel เสมอเร็วกว่า mutex — channel มี overhead มากกว่า mutex สำหรับ simple counter
- buffered channel ช่วยป้องกัน deadlock — ไม่เสมอ — deadlock ยังเกิดได้ถ้า buffer เต็มและไม่มี receiver
- close(ch) ลบข้อมูลใน buffer — ไม่ — receiver ยังรับข้อมูลที่เหลือใน buffer ได้หลัง close
- สามารถ close channel จาก receiver ได้ — sender เท่านั้นที่ควร close channel
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อโค้ดที่ AI สร้างมาสร้างหรือ close channel ให้เช็กสิ่งเหล่านี้ก่อนเชื่อมัน:
- มี goroutine เดียวเท่านั้นที่เรียก
closeบน channel นี้จริงไหม หรือมีโอกาสที่ goroutine สองตัวจะเรียกcloseบนตัวเดียวกัน? - ทุก goroutine ที่ send บน channel นี้มี receiver ที่การันตีว่ายังอ่านอยู่จริงไหม หรือ receiver อาจ return ไปแล้ว ทำให้ sender block ตลอดไป?
- มี channel variable ที่ถูกใช้ก่อน
makeหรือเปล่า (field แบบ channel ที่เป็น zero value ของ struct หรือvar ch chan Tที่ไม่เคยถูก assign)? ทุก operation บนมันจะ block ตลอดไปแทนที่จะ panic - function ที่ send อย่างเดียวประกาศ channel parameter เป็นสองทิศทาง (
chan T) แทนที่จะเป็นchan<- Tหรือเปล่า? ไม่ใช่บัค แต่พลาดโอกาสให้ compiler ช่วยป้องกัน receive โดยไม่ตั้งใจ - ทุก path — รวมถึง error return — ทำให้ send-after-close เป็นไปไม่ได้จริงไหม หรือมี branch ที่ early-return ข้ามจุดที่ channel ถูก close ไปแล้ว?
🐛 หา bug ในโค้ดนี้
func merge(chs ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup for _, c := range chs { wg.Add(1) go func(c <-chan int) { defer wg.Done() for v := range c { out <- v } close(out) // called once per input channel }(c) } return out}เฉลย
goroutine ทั้ง len(chs) ตัวเรียก close(out) เมื่อ input channel ของตัวเองหมด — แต่ out ใช้ร่วมกัน goroutine ตัวแรกที่เสร็จจะ close มัน; goroutine ตัวอื่นที่เสร็จทีหลังจะ panic บนการเรียก close(out) ของตัวเอง และ goroutine ตัวไหนที่ยังพยายาม send อยู่ก็ panic ด้วย send on closed channel output จริงบน Go 1.26.5 (ลำดับต่างกันไปตามการรัน):
panic: send on closed channel
goroutine 19 [running]:main.merge.func1(0xc0001c8000) /tmp/mergebug/main.go:16 +0x98go run -race ยังรายงาน WARNING: DATA RACE ระหว่าง send ที่แข่งกันกับ receive ที่ trigger close เพิ่มด้วย — นี่คือคำถาม “ใคร close” เรื่อง ownership จากก่อนหน้านี้ในบทนี้เป๊ะ วิธีแก้: ไม่มีใครใน goroutine ต่อ channel เรียก close(out) เลย goroutine เฉพาะทางตัวเดียวเรียก wg.Wait() แล้วค่อย close(out) แค่ครั้งเดียว หลังจาก input goroutine ทุกตัวเสร็จแล้ว — ดู fan-in merge เวอร์ชันที่แก้แล้วในบท Patterns