ข้ามไปยังเนื้อหา

Channels

Channel คือท่อส่งข้อมูลแบบ typed ที่ goroutines ใช้สื่อสาร การส่งค่าเข้า channel และรับจาก channel เป็นสองคำสั่งที่ถ่ายโอนทั้งข้อมูลและการ synchronize ในขั้นตอนเดียว

ch := make(chan int) // unbuffered channel of int
ch <- 42 // send (blocks until a receiver is ready)
v := <-ch // receive (blocks until a sender is ready)

Type system บังคับสิ่งที่ไหลผ่าน channel chan string ไม่ยอมรับ int

UnbufferedBuffered
สร้างด้วยmake(chan T)make(chan T, capacity)
Send บล็อก?ใช่ — จนกว่า receiver พร้อมไม่ — จนกว่า buffer เต็ม
Receive บล็อก?ใช่ — จนกว่า sender พร้อมไม่ — จนกว่า buffer ว่าง
Synchronize?ใช่ — sender และ receiver พบกันในเวลาเดียวกันไม่ — buffer แยกเวลาออกจากกัน

Unbuffered channels ให้ synchronization guarantee ที่แข็งแกร่งที่สุด: sender รู้ว่า receiver ได้รับค่าแล้วก่อนที่จะดำเนินต่อ

คุณสามารถจำกัด channel parameter ให้เป็น send-only หรือ receive-only เพื่อแสดง intent ใน type:

func producer(out chan<- int) { out <- 1 } // send-only
func consumer(in <-chan int) { v := <-in; _ = v } // receive-only

chan T ที่เป็นสองทิศทางสามารถ assign ให้กับ direction ไหนก็ได้ Compiler จะบังคับว่า receive-only channel จะถูกส่งเข้าไม่ได้ และในทางกลับกันก็เช่นกัน

Sender เรียก close(ch) เพื่อบอกว่าจะไม่มีค่าอีกแล้ว Receivers ใช้ for range loop เพื่อดึงค่าทั้งหมดจนกว่าจะ close:

close(ch)
for v := range ch { // exits when ch is closed and empty
fmt.Println(v)
}

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 value

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 +0x2c
exit status 2

Tag [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

สาม operation — send, receive, close — ทำงานต่างกันขึ้นอยู่กับว่า channel เป็น nil, open, หรือ closed:

Operationnil channelopen channelclosed 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 channelclose สำเร็จ — 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 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 พร้อมกัน

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 เขียน” ด้านล่างของบทนี้เป๊ะ

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
}
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
Unbuffered channel บังคับให้ sender กับ receiver ต้องพบกัน ส่วน buffered channel จะ block ก็ต่อเมื่อ buffer เต็มหรือว่างเท่านั้น
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
Channel เปลี่ยนสถานะ nil → open → closed ทางเดียว ไม่ย้อนกลับ แต่ละสถานะมีกฎ send/receive ของตัวเอง
สิ่งที่ได้ประโยชน์ต้นทุน
unbuffered channelsynchronization guarantee แข็งแกร่งG ส่ง block จนกว่า G รับพร้อม
buffered channeldecouple sender/receiver timingต้อง size buffer ให้เหมาะ, อาจ hide backpressure
channel direction (chan<-, <-chan)compile-time safetyverbose ใน function signature
close + rangenotify 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 สร้างมาสร้างหรือ 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 +0x98

go 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

เกิดอะไรขึ้นเมื่อคุณรับจาก unbuffered channel ขณะที่ไม่มี goroutine กำลังส่ง?
คุณเรียก close(ch) บน buffered channel ที่ยังมี 3 items อยู่ใน buffer เกิดอะไรขึ้นเมื่อ range?
return value ตัวที่สอง ok ใน v, ok := <-ch บอกอะไร?
annotation ทิศทาง channel ใดที่จำกัด parameter ให้รับเท่านั้น?
เกิดอะไรขึ้นเมื่อรัน?
func main() {
    var ch chan int
    ch <- 1
}
โค้ดนี้ print อะไร?
ch := make(chan int, 1)
close(ch)
v, ok := <-ch
fmt.Println(v, ok)