chan キーワードは channel 型を宣言します。channel は goroutine どうしが値を受け渡すための型付きのパイプで、送信と受信のたびに、関わる 2 つの goroutine の同期も行われます。chan Job は Job の値を双方向に運び、chan<- Job は送信だけ、<-chan Job は受信だけができます。channel 型のゼロ値は nil なので、使う前に make で作成する必要があります(Go spec)。
要約
make(chan Job)はバッファなしの channel を作成します。送信は別の goroutine が受信するまでブロックするので、送信のたびに値が直接手渡されます。make(chan Job, 100)はバッファ付きの channel を作成します。送信がブロックするのはバッファが満杯のときだけで、受信がブロックするのはバッファが空のときだけです。- 関数のシグネチャに
chan<- Jobや<-chan Jobと書くと、その関数は送信か受信のどちらかしかできなくなります。それ以外の操作はコンパイラが拒否します。 - channel をクローズするのは送信側で、受信側ではありません。
for job := range jobsはクローズ後に終了し、v, ok := <-chは channel がクローズされて空になるとok == falseを返します。 - クローズ済みの channel に送信すると panic し、channel を 2 回クローズしても panic します。
nilの channel に対する操作は、closeを除いてすべて永遠にブロックします。 fatal error: all goroutines are asleep - deadlock!は、すべての goroutine がブロックしていることを意味します。一部の goroutine だけが止まっているのがリークで、こちらは表に出にくい問題です。Go 1.27 ではgoroutineleakプロファイルでそれを見つけられます。selectは複数の channel 操作を同時に待ちます。タイムアウト、キャンセル、ブロックしない送信を実現できます。
Go で channel を宣言して作成するには?
chan の後に要素の型を書きます。そう宣言した変数は、make で作った channel を代入するまで nil です。
example.gogopackage main import "fmt" type Job struct { ID int URL string } func main() { var queue chan Job fmt.Println(queue == nil, len(queue), cap(queue)) queue = make(chan Job, 100) queue <- Job{ID: 1, URL: "https://api.example.com/webhooks/stripe"} queue <- Job{ID: 2, URL: "https://api.example.com/webhooks/github"} fmt.Println(len(queue), cap(queue)) job := <-queue fmt.Println(job.ID, job.URL) }
example.texttexttrue 0 0 2 100 1 https://api.example.com/webhooks/stripe
ch <- v は送信、<-ch は受信です。矢印は常にデータが移動する方向を向いています。cap は make に渡したバッファサイズを返し、len はいまバッファに入っている値の数を返します。値は入れた順に取り出されます。
channel は map と同じく、ランタイムの構造体への参照です。関数に渡したり struct に格納したりすると参照がコピーされるので、どのコピーも同じ channel を操作します。Go の var キーワードと var と make の違いでは、通常 make で作成する 3 つの型として channel、map、slice を解説しています。
バッファありとバッファなしの channel の違いは?
バッファなしの channel には値を保持する場所がありません。送信は受信側が値を受け取るまで待ち、受信は送信側が値を差し出すまで待ちます。両者は同じ瞬間に出会います。Go spec はこれを「送信側と受信側の両方の準備ができたときにだけ通信が成功する」と表現しています。
次の例では、アップロードのハンドラーが、起動に 100ms かかるウイルススキャナーにファイル名を渡します。
example.gogofunc main() { uploads := make(chan string) go func() { time.Sleep(100 * time.Millisecond) fmt.Println("scanner: ready") for name := range uploads { _ = name // virus-scan the file } }() start := time.Now() uploads <- "invoice-1001.pdf" fmt.Println("handler: send returned after", time.Since(start).Round(10*time.Millisecond)) }
example.texttextscanner: ready handler: send returned after 100ms
スキャナーが受信できる状態になるまで、送信は完了できませんでした。送信から戻った時点で、ハンドラーはスキャナーが値を受け取ったことを確実に知っています。この保証はそれだけで役に立ちます。Go のメモリモデルはこれを正式に定めており、バッファなしの channel からの受信は、対応する送信の完了より前に同期されます。
バッファ付きの channel は最大 cap 個の値を保持します。make(chan string, 10) であれば、ハンドラーの送信はすぐに戻り、スキャナーは 100ms 後にファイルを受け取っていたはずです。送信側がブロックするのは、10 個の枠がすべて埋まったときだけです。
Go の channel のバッファサイズはどう決めるべきか?
まずは 0 か 1 から始め、それより大きな数は用途を説明できるときにだけ選びます。バッファで遅いコンシューマーが速くなることはありません。プロデューサーが常にコンシューマーより速いなら、1,000 のバッファはやがて満杯になり、その後はバッファなしの channel と同じように振る舞います。しかも手前に 1,000 個分の余計な遅延が加わります。
バッファが役に立つのは次の 3 つの場面です。
- 結果を 1 つだけ返す goroutine。
make(chan Result, 1)なら、誰も受信しなくても goroutine は送信して終了できます。後述のリークはこれで直せます。 - バースト。一度に 50 個のサンプルを受け取り、1 秒に 1 回まとめて送るメトリクスエクスポーターなら、通常のバーストの大きさのバッファを使えます。
- 並行数の制限。容量 N のバッファ付き channel は、N 個の goroutine を同時に実行させるセマフォとして使えます。例はパターンのセクションにあります。
Go の chan<- と <-chan の意味は?
どちらも方向付きの channel 型です。chan<- Job は送信専用、<-chan Job は受信専用です。双方向の chan Job はどちらにも自動的に変換されるので、channel は 1 回だけ作成し、各関数には必要な方向だけを渡します。
プロデューサーと配信ステージを持つ webhook のパイプラインは次のようになります。
example.gogotype Result struct { JobID int Status int } func produce(ids []int, out chan<- Job) { for _, id := range ids { out <- Job{ID: id, URL: fmt.Sprintf("https://hooks.example.com/%d", id)} } close(out) } func deliver(in <-chan Job, results chan<- Result) { for job := range in { results <- Result{JobID: job.ID, Status: 200} } close(results) } func main() { jobs := make(chan Job) results := make(chan Result) go produce([]int{101, 102, 103}, jobs) go deliver(jobs, results) for r := range results { fmt.Printf("job %d: %d\n", r.JobID, r.Status) } }
example.texttextjob 101: 200 job 102: 200 job 103: 200
シグネチャを見れば、どちらの端を誰が持っているかがわかります。deliver を読めば、in に書き込むことも、in をクローズすることもないとわかります。これはコンパイラが強制します。deliver が入力に送信したり、入力をクローズしたりしようとすると次のようになります。
example.gogopackage main type Job struct{ ID int } func deliver(in <-chan Job) { in <- Job{ID: 1} close(in) }
example.texttext./main.go:6:2: invalid operation: cannot send to receive-only channel <-chan Job in (variable of type <-chan Job) ./main.go:7:8: invalid operation: cannot close receive-only channel in (variable of type <-chan Job)
クローズは送信側の操作とみなされるので、クローズできるのは chan<- か双方向の channel だけです。そのため、「送信側がクローズする」という一般的な所有権のルールを、型システムが確認してくれます。
Go で channel をクローズするには?
close(ch) を呼びます。クローズは、これ以上値が来ないことを受信側に伝えます。すでにバッファにある値は捨てられません。受信側はまずそれらを受け取り、その後の受信はすべてゼロ値ですぐに戻ります。
example.gogofunc main() { events := make(chan string, 2) events <- "user.created" events <- "user.deleted" close(events) for i := 0; i < 3; i++ { ev, ok := <-events fmt.Printf("%q %v\n", ev, ok) } }
example.texttext"user.created" true "user.deleted" true "" false
2 つ目の値 ok は、実際の値なら true、channel がクローズされて空になった後は false になります。単純な ev := <-events では、送信された空文字列とクローズ済みの channel のゼロ値を区別できません。channel がクローズされる可能性があるときは、常に 2 値の形を使ってください。
for v := range ch は同じ確認を代わりに行います。channel がクローズされて空になるまで受信し、その後ループを抜けます。上の deliver はこの仕組みで止まるタイミングを判断しています。produce が ID を使い切ったときに jobs をクローズしなければならないのも同じ理由です。クローズしなければ、deliver は 4 つ目のジョブを永遠に待ち続けます。
Go の channel は誰がクローズすべきか?
送信する goroutine です。受信側には、次の値が向かっているかどうかがわかりません。受信側からクローズすると、すぐに後述の panic につながります。複数の goroutine が 1 つの channel に送信する場合は、どれも単独ではクローズできません。別の goroutine が sync.WaitGroup ですべての送信側を待ち、その後で channel をクローズします。この形は後述のワーカープールで紹介します。
次の 3 つの間違いは実行時に panic します。
example.texttextpanic: send on closed channel panic: close of closed channel panic: close of nil channel
すべての channel をクローズする必要はありません。誰も参照していない channel は、クローズしたかどうかにかかわらずガベージコレクションで回収されます。range ループやシャットダウンのシグナルのように、ストリームが終わったことを受信側が知る必要があるときに channel をクローズします。
nil の channel やクローズ済みの channel に送信するとどうなるか?
すべての組み合わせを 1 つの表にまとめます。
| 操作 | nil の channel | オープンな channel | クローズ済みの channel |
|---|---|---|---|
送信 ch <- v | 永遠にブロックする | 受信側が受け取るか、バッファに空きができるまでブロックする | panic する |
受信 <-ch | 永遠にブロックする | バッファの値を返すか、値が届くまでブロックする | バッファの値を返し、その後は ok == false とともにゼロ値を返す |
close(ch) | panic する | 成功する | panic する |
nil の channel に対する select の case は決して選ばれません。後述の merge の例はこれを利用しています。クローズ済みの channel はすべての受信側にすぐ戻るので、close はブロードキャストとして機能します。その channel から受信しているすべての goroutine が同時に起こされます。
Go が「all goroutines are asleep - deadlock!」と表示するのはなぜか?
プログラム内のすべての goroutine がブロックしていて、どれも二度と起きられないことをランタイムが検出したからです。最小の例は、受信側のいないバッファなしの channel への送信です。
example.gogofunc main() { uploads := make(chan string) uploads <- "invoice-1001.pdf" fmt.Println("queued") }
example.texttextfatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /app/main.go:7 +0x34
goroutine は main だけで、何も受信しない送信を待っています。トレースには、ブロックしている操作(chan send)と行番号が表示されます。直し方は、別の goroutine で受信するか、値を 1 つ置いておくだけでよいなら channel にバッファを持たせることです。Go でよくある間違いでは、これを 10 番目の間違いとして挙げています。
この検出が働くのは、すべての goroutine が止まっているときだけです。Web サーバーには、次の接続を待ってネットワークポーラーで待機している goroutine が常にあり、検出器はそれを止まっているとはみなしません。そのため、channel でブロックしたハンドラーがあっても検出されません。ハンドラーはただ止まり、その goroutine はプロセスが終了するまでメモリに残ります。これを goroutine リークと呼びます。
Go で goroutine リークを見つけるには?
よくあるリークは次のような形です。チェックアウトページが 3 社の配送業者に見積もりを依頼し、最も速い回答を採用します。
example.gogofunc fastestQuote(ctx context.Context) (Quote, error) { ch := make(chan Quote) go func() { ch <- fetchQuote("ups", 50*time.Millisecond) }() go func() { ch <- fetchQuote("dhl", 80*time.Millisecond) }() go func() { ch <- fetchQuote("fedex", 300*time.Millisecond) }() select { case q := <-ch: return q, nil case <-ctx.Done(): return Quote{}, ctx.Err() } }
この関数は 1 回受信して戻ります。遅い 2 つの goroutine は、その後誰も読まないバッファなしの channel に送信しようとします。これらはプロセスが動いている間ずっとブロックしたままです。チェックアウトが 100 回あれば、200 個の goroutine が止まったままになります。
Go 1.27 では、Go 1.26 で 1 リリースの間実験的機能だった goroutineleak プロファイルが正式に利用可能になりました(Go 1.27 リリースノート)。ガベージコレクターは、実行可能などの goroutine からも到達できなくなった channel やロックでブロックしている goroutine を探し、スタックごとに報告します。
example.gogopprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
example.texttextgoroutineleak profile: total 200 100 @ 0x1044f99a8 0x104492854 0x104492468 0x104550560 0x1044ffc74 # 0x10455055f main.fastestQuote.func3+0x4f /app/main.go:27 100 @ 0x1044f99a8 0x104492854 0x104492468 0x1045505d0 0x1044ffc74 # 0x1045505cf main.fastestQuote.func2+0x4f /app/main.go:26
DHL と FedEx の goroutine が行番号付きで示されています。net/http/pprof をインポートしたサーバーは、同じデータを /debug/pprof/goroutineleak で公開します。このプロファイルについては Go 1.27 の新機能で詳しく解説しています。
すべての送信側の分だけ枠を持つバッファにすれば直ります。
example.gogoch := make(chan Quote, 3)
これで、誰かが値を読むかどうかにかかわらず、どの goroutine も送信して終了できます。この変更を加えると、同じプログラムは goroutineleak profile: total 0 と報告します。より一般的に言えば、起動する goroutine にはどれも、終了する手段が必要です。たいていは、必ず読む受信側、すべての送信を収められるバッファ、ctx.Done() を含む select のどれかです。
Go の select は channel とどう組み合わせて動くのか?
select は複数の channel 操作を待ち、最初に進めるようになったものを実行します。同時に複数の準備ができている場合はランダムに 1 つを選ぶので、ある case がほかの case を飢餓状態にすることはありません(Go spec)。
channel の受信にタイムアウトを付けるには?
受信とタイムアウト用の channel を同じ select に入れます。context.Context を使う場合、タイムアウト用の channel は ctx.Done() です。
example.gogofunc lookupPrice(ctx context.Context, sku string) (int, error) { result := make(chan int, 1) go func() { result <- slowPriceService(sku) }() select { case cents := <-result: return cents, nil case <-ctx.Done(): return 0, fmt.Errorf("price for %s: %w", sku, ctx.Err()) } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() fmt.Println(lookupPrice(ctx, "SKU-1001")) fmt.Println(lookupPrice(context.Background(), "SKU-1001")) }
example.texttext0 price for SKU-1001: context deadline exceeded 4999 <nil>
slowPriceService は 300ms かかるので、1 回目の呼び出しは 100ms で諦め、2 回目は回答を待ちます。result のバッファ 1 は、前のセクションで紹介したリーク対策です。タイムアウトの後でも、goroutine は遅れた回答を送信して終了できます。
context がない場合は case <-time.After(2 * time.Second): でも動きます。Go 1.23 からは、誰も参照していないタイマーは発火前でもガベージコレクションで回収できるので、ループ内で time.After を使ってもタイマーが溜まらなくなりました(Go 1.23 リリースノート)。Go 1.27 では、以前の動作に戻す GODEBUG 設定 asynctimerchan が削除されました。リクエストを処理するコードでは context を優先してください。クライアントからのキャンセルも伝えてくれるからです。
ブロックしない送信や受信をするには?
default の case を追加します。ほかの case の準備ができていないとき、select は default を実行するので、操作が待たされることはありません。リクエストの処理経路を決して遅くしてはならないメトリクスエクスポーターは、この方法でキューが満杯のときにサンプルを破棄します。
example.gogotype Exporter struct { queue chan Metric dropped atomic.Int64 } func (e *Exporter) Record(m Metric) { select { case e.queue <- m: default: e.dropped.Add(1) } } func main() { e := &Exporter{queue: make(chan Metric, 3)} for i := range 5 { e.Record(Metric{Name: "http_requests_total", Value: float64(i)}) } fmt.Println("queued:", len(e.queue), "dropped:", e.dropped.Load()) }
example.texttextqueued: 3 dropped: 2
Record は多数のリクエストハンドラーで同時に実行されるので、破棄のカウンターは通常の int ではなく atomic.Int64 にしています。破棄した数は記録しておいてください。何も記録しない default では、コンシューマーが追いつかなくなった瞬間が見えなくなります。
select で channel に nil を代入するのはなぜか?
case を無効にするためです。nil の channel からの受信は永遠にブロックするので、nil の channel に対する select の case は決して選ばれません。2 つのストリームをマージするときは、それぞれがクローズされたら nil を代入し、両方が nil になったら終了します。
example.gogofunc merge(a, b <-chan string) []string { var out []string for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil continue } out = append(out, v) case v, ok := <-b: if !ok { b = nil continue } out = append(out, v) } } return out }
nil を代入しないと、クローズ済みの channel は常に準備ができた状態になり、ループは CPU を使い切ってゼロ値を受け取り続けます。
Go でよく使われる channel のパターンは?
本番環境のサービスで使われる channel のコードは、ほとんどが次の 4 つのパターンで説明できます。
ワーカープール
決まった数の goroutine が、1 つのジョブ channel から読み取ります。次の例は 3 つのワーカーで webhook を配信します。
example.gogofunc main() { hooks := make(chan Webhook) deliveries := make(chan Delivery) var wg sync.WaitGroup for worker := range 3 { wg.Go(func() { for h := range hooks { deliveries <- Delivery{WebhookID: h.ID, Worker: worker, Status: send(h)} } }) } go func() { for id := 1; id <= 9; id++ { hooks <- Webhook{ID: id, URL: fmt.Sprintf("https://partner.example.com/hooks/%d", id)} } close(hooks) }() go func() { wg.Wait() close(deliveries) }() ok := 0 for d := range deliveries { if d.Status == 200 { ok++ } } fmt.Println("delivered:", ok) }
example.texttextdelivered: 9
このコードが正しく動く理由は 4 つあります。プロデューサーが hooks をクローズするので、各ワーカーの range ループが終了します。Go 1.25 で追加された wg.Go は、1 回の呼び出しでワーカーを起動し、追跡も行います。どのワーカーも、ほかのワーカーがいつ終わるかを知らないので、別の goroutine がすべてのワーカーを待ってから deliveries をクローズします。Go 1.22 からはループの反復ごとに別の変数が作られるので、クロージャーは worker を安全にキャプチャできます(クロージャーの仕組みを参照)。
ファンアウトとファンイン
ワーカープールはファンアウトです。1 つの channel が多数の goroutine に値を供給します。deliveries channel はファンインです。多数の goroutine が 1 つの channel に書き込み、それを 1 つのループが読み取ります。前述の merge 関数は、入力 channel の数が決まっている場合のファンインです。Go blog の パイプラインの記事では、同じ 2 つの部品を組み合わせてより長いチェーンを作っています。
セマフォ
空の struct を要素とするバッファ付き channel を使うと、同時に処理を行う goroutine の数を制限できます。次の例は、同時に 3 枚を超える画像をリサイズしてはならない画像サービスです。
example.gogosem := make(chan struct{}, 3) var wg sync.WaitGroup for _, img := range images { wg.Go(func() { sem <- struct{}{} defer func() { <-sem }() resizeImage(img) }) } wg.Wait()
4 つ目の goroutine は、最初の 3 つのどれかが枠を解放するまで sem <- struct{}{} でブロックします。容量 C の channel について、メモリモデルはこれを保証しています。k 番目の受信は、(k+C) 番目の送信の完了より前に起こります(Go のメモリモデル)。goroutine がエラーも返す場合は、golang.org/x/sync の errgroup.Group と SetLimit で同じことができ、最初のエラーも集められます。
chan struct{} による完了シグナル
送信には使わず、クローズだけする channel は、すべての受信側にシグナルをブロードキャストします。要素の型に struct{} を使うのは、0 バイトで済み、データが流れないことが明確になるからです(Go の struct キーワードを参照)。
example.gogofunc flushLoop(done <-chan struct{}, flushed chan<- int) { ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() n := 0 for { select { case <-ticker.C: n++ case <-done: flushed <- n return } } } func main() { done := make(chan struct{}) flushed := make(chan int) go flushLoop(done, flushed) time.Sleep(175 * time.Millisecond) close(done) fmt.Println("flushes before shutdown:", <-flushed) }
example.texttextflushes before shutdown: 3
context.Context も同じ考え方で作られています。ctx.Done() はキャンセル時にクローズされる <-chan struct{} を返すので、上の done と同じように、どの select でも使えます。
Go では channel と mutex のどちらを使うべきか?
データの所有権を渡すときや goroutine を協調させるときは channel を使います。複数の goroutine が 1 つの状態をその場で更新するときは sync.Mutex を使います。Go の格言「メモリを共有することで通信するのではなく、通信することでメモリを共有せよ」(Go Proverbs)が表しているのは前者です。Go wiki のページ Use a sync.Mutex or a channel? は、この格言が mutex を否定するものではないとはっきり述べています。
ルートごとのリクエスト数のカウンターは、値の流れではなく状態です。
example.gogotype Stats struct { mu sync.Mutex counts map[string]int } func (s *Stats) Inc(route string) { s.mu.Lock() defer s.mu.Unlock() s.counts[route]++ }
channel で書くと、map を所有する goroutine、リクエスト用の channel、読み取り用の返信 channel が必要になります。コードが増えるうえに、カウントを 1 つ増やすたびに別の goroutine の処理を待たなければなりません。ジョブキューの場合は逆です。mutex で保護した slice では、ワーカーに仕事を待たせるために sync.Cond かポーリングが必要ですが、channel ならブロックと起床が自然に手に入ります。
| channel を使う場面 | mutex を使う場面 |
|---|---|
| ジョブや結果を別の goroutine に渡す | カウンター、キャッシュ、map をその場で更新する |
| シャットダウン、キャンセル、完了を通知する | struct 内のいくつかのフィールドを保護する |
| 並行数を制限する、パイプラインを組む | ホットパス上の短いクリティカルセクション |
どちらの場合も go test -race でテストしてください。ロックなしで共有された map を検出するだけでなく、struct へのポインタを送信した後も送信側がその struct を変更し続け、受信側がそれを読んでいるケースも検出します。
LevelUpGo で学ぶ
LevelUpGo では、ブラウザ上で本物の Go コードを実行する演習を通じて Go を学べます。Go Concurrency Fundamentals では、goroutine、バッファなしとバッファ付きの channel、channel の方向、select、context、レース検出器を扱い、修正するまでデッドロックする演習に取り組みます。Go Design Patterns では、この記事で紹介したワーカープール、ファンアウトとファンイン、グレースフルシャットダウンのパターンを、完全な演習として組み立てます。Training Ground には、デッドロックする、リークする、処理を取りこぼすといったコードを扱う、並行処理の短い単独の演習があります。残り 24 個の予約語については、Go のキーワード一覧:全 25 個の予約語を解説をご覧ください。
よくある質問
Go で chan はキーワードですか?
はい。chan は Go の 25 個の予約語の 1 つなので、変数名や関数名には使えません。chan Job、chan<- Job、<-chan Job のような channel 型に登場します。演算子の <- はキーワードではありません。make も事前宣言された関数なので、キーワードではありません。
Go の channel は必ずクローズしなければなりませんか?
いいえ。channel をクローズするのは、for range ループを終わらせたいときやシャットダウンをブロードキャストしたいときなど、これ以上値が来ないことを受信側が知る必要がある場合だけです。クローズしていない channel も、何からも参照されていなければ、ほかの値と同じようにガベージコレクションで回収されます。
バッファありの channel とバッファなしの channel の違いは何ですか?
バッファなしの channel(make(chan T))では、受信側が値を受け取るまで送信側が待つので、2 つの goroutine が出会います。バッファ付きの channel(make(chan T, n))では、最大 n 個の値を channel 内で待たせておけるので、送信側がブロックするのはバッファが満杯のときだけです。
Go でクローズ済みの channel から読み取るとどうなりますか?
まず、バッファに残っている値が返されます。その後の受信はすべて、要素の型のゼロ値ですぐに戻ります。2 値の形 v, ok := <-ch では、そうしたゼロ値に対して ok が false になるので、実際の値と区別できます。
Go で channel がクローズされているか確認するには?
v, ok := <-ch で受信し、ok を確認します。受信せずに channel がクローズされているかを報告する関数はなく、len(ch) が数えるのはバッファ内の値だけです。そうした確認が必要なコードは、たいてい複数の goroutine が channel をクローズしており、直すべきなのはその設計です。
Go で channel を range で回せますか?
はい。for v := range ch は、channel がクローズされて空になるまで値を受信し、その後ループを抜けます。誰も channel をクローズしなければループは永遠に待ち続けるので、送信側は終わったときに close を呼ぶ必要があります。
出典
- The Go Programming Language Specification, Channel types: https://go.dev/ref/spec#Channel_types
- The Go Programming Language Specification, Send statements: https://go.dev/ref/spec#Send_statements
- The Go Programming Language Specification, Receive operator: https://go.dev/ref/spec#Receive_operator
- The Go Programming Language Specification, Close: https://go.dev/ref/spec#Close
- The Go Programming Language Specification, Select statements: https://go.dev/ref/spec#Select_statements
- The Go Memory Model, Channel communication: https://go.dev/ref/mem#chan
- Effective Go, Concurrency: https://go.dev/doc/effective_go#concurrency
- The Go Blog, Go Concurrency Patterns: Pipelines and cancellation: https://go.dev/blog/pipelines
- Go Wiki, Use a sync.Mutex or a channel?: https://go.dev/wiki/MutexOrChannel
- Go 1.23 Release Notes, Timer changes: https://go.dev/doc/go1.23#timer-changes
- Go 1.27 Release Notes: https://go.dev/doc/go1.27
- Go Proverbs: https://go-proverbs.github.io/
