ブログに戻る

Go の chan キーワード:channel の方向とデッドロック

Go の chan キーワードの仕組みを解説します。バッファありとバッファなしの channel、送信専用と受信専用の型、クローズ、デッドロック、goroutine リーク、select、ワーカープールを扱います。

Go の chan キーワード:channel の方向とデッドロック

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.gogo
package 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.texttext
true 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.gogo
func 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.texttext
scanner: 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.gogo
type 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.texttext
job 101: 200
job 102: 200
job 103: 200

シグネチャを見れば、どちらの端を誰が持っているかがわかります。deliver を読めば、in に書き込むことも、in をクローズすることもないとわかります。これはコンパイラが強制します。deliver が入力に送信したり、入力をクローズしたりしようとすると次のようになります。

example.gogo
package 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.gogo
func 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.texttext
panic: 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.gogo
func main() {
	uploads := make(chan string)
	uploads <- "invoice-1001.pdf"
	fmt.Println("queued")
}
example.texttext
fatal 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.gogo
func 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.gogo
pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
example.texttext
goroutineleak 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.gogo
ch := 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.gogo
func 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.texttext
0 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.gogo
type 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.texttext
queued: 3 dropped: 2

Record は多数のリクエストハンドラーで同時に実行されるので、破棄のカウンターは通常の int ではなく atomic.Int64 にしています。破棄した数は記録しておいてください。何も記録しない default では、コンシューマーが追いつかなくなった瞬間が見えなくなります。

select で channel に nil を代入するのはなぜか?

case を無効にするためです。nil の channel からの受信は永遠にブロックするので、nil の channel に対する select の case は決して選ばれません。2 つのストリームをマージするときは、それぞれがクローズされたら nil を代入し、両方が nil になったら終了します。

example.gogo
func 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.gogo
func 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.texttext
delivered: 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.gogo
sem := 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.gogo
func 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.texttext
flushes 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.gogo
type 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 を呼ぶ必要があります。

出典

シニアエンジニアのように Go を書く

ブラウザで学べるインタラクティブなレッスン。最初のレッスンは無料です。

無料レッスンを試すまたは無料アカウントを作成