🧑‍🎓

goのchanの理解が追いついてないので改めて入門する

2024/05/06に公開

はじめに

chanとかgoroutineってとても便利だと思うんですが、Webシステムのプロダクションコード書いてても意外と利用することが少なくて、いざ必要になったときに毎回1から調べてなんとなく書いてしまっています。

なので今回はchanについて、どういう挙動をするのか、どう書くとdeadlockしてしまうのかなど改めてまとめていこうと思います。

chanの基本的な使い方

以下のように、chanの初期化、送信、受信をすることができます。

func main() {
	// chanの初期化
	ch := make(chan int)

	// 送信
	go func() { ch <- 1 }()

	// 受信
	fmt.Println(<-ch) // 1
}

送受信は基本的に同時に行われる必要があり、片方が準備できるまでブロックされます。

deadlockが発生するコード

最初のコードのように別スレッドで送受信を行うと正常に動きますが、以下のコードのようにメインスレッド上で送受信を行おうとするとdeadlockが発生します。

func main() {
	// chanの初期化
	ch := make(chan int)

	// 送信 ← ここでブロックされる
	ch <- 1

	// 受信
	fmt.Println(<-ch)
}

また、送信数以上に受信しようとした場合も同様でに、受信を待つためdeaadlockが発生します。

func main() {
	// chanの初期化
	ch := make(chan int)

	// 送信
	go func() { ch <- 1 }()

	// 受信
	fmt.Println(<-ch) // 1
	fmt.Println(<-ch) // <- 受信待ちでdeadlockが発生する
}

バッファを使う

送受信は同時に行われると書きましたが、バッファを使うとバッファが詰まるまではブロックされません。

先ほどdeadlockが起きたコードですが、capを指定するとdeadlockは発生しません。

func main() {
	// chanの初期化
	ch := make(chan int, 1) // ← capを指定する

	// 送信
	ch <- 1

	// 受信
	fmt.Println(<-ch) // 1
}

deadlockが発生するコード

capを超えて送信しようとすると、バッファが詰まってdeadlockが発生します。

func main() {
	// chanの初期化
	ch := make(chan int, 1)

	// 送信
	ch <- 1
	ch <- 2 // <- capが1なので、バッファが詰まってdeadlockが発生する

	// 受信
	fmt.Println(<-ch) // 1
}

chanのclose

closeせずに受信しようとするとdeadlockが発生してしまいますが、close後に受信しようとするとブロックされずに0値が返ってきます。

func main() {
	// chanの初期化
	ch := make(chan int)

	// 送信
	go func() {
		ch <- 1
		close(ch)
	}()

	// 受信
	fmt.Println(<-ch) // 1
	fmt.Println(<-ch) // 0 <- ブロックされない
}

受診時に2つ目のパラメータを見ることで、chanがcloseされているかどうかを判断することができます。

	v, ok := <-ch

closeされていなければoktrue、closeされていればokfalseとなります。

panicが発生するコード

closeされたchanに送信しようとするとpanicが発生します。

func main() {
	// chanの初期化
	ch := make(chan int, 1)

	close(ch)
	// 送信
	ch <- 1 // <- panic: send on closed channel

	// 受信
	fmt.Println(<-ch) // 1
}

rangeで受信する

rangeは、chanがcloseするまで値を受信し続けます。

func main() {
	ch := make(chan int)

	go func() {
		ch <- 1
		ch <- 2
		ch <- 3
		close(ch)
	}()

	for v := range ch {
		fmt.Println(v)
	}
}

deadlockが発生するコード

closeするまで受信し続けるので、以下のようにcloseしなければdeadlockが発生します。

func main() {
	ch := make(chan int)

	go func() {
		ch <- 1
		ch <- 2
		ch <- 3
	}()

	for v := range ch { // <- closeされていないのでブロックし続ける
		fmt.Println(v)
	}
}

selectで受信する

rangeと異なる特徴は以下かなと思います。

  • 複数のchanの送受信を扱うことができる
  • どのcaseも準備できていない場合の処理を書くことができる

以下のように書くことができます。

func receive() {
	for {
		select {
		case v := <-ch1:
			// ch1受信時の処理
		case v := <-ch2:
			// ch2受信時の処理
		default:
			// どのcaseも準備できていない場合の処理
		}
	}
}

また、rangeと違いcloseされた後もforで回している場合は受信処理し続け、0値を受け取り続けます。chanがcloseした後、caseを無視したい場合、chanにnilを代入することでスキップすることができます。

func receive() {
	for {
		select {
		case v, ok := <-ch1:
			if !ok {
				ch1 = nil // <- nilを代入すると次回からスキップされる
				break
			}
			// ch1受信時の処理
		case v := <-ch2:
			// ch2受信時の処理
		default:
			// どのcaseも準備できていない場合の処理
		}
	}
}

まとめ

ざっくりと、以下の挙動を覚えておけば良さそうかなと思います。

  • chanは送受信でブロックする
  • バッファを使っていればバッファが詰まるまではブロックされない
  • closeされると受信でブロックしない
  • closeされたchanには送信できない
  • rangeはcloseされるまで受信し続ける
  • 複数のchanを扱うときはselectを使う
  • chanがnilだとselectのcaseからは無視される

Discussion