goのchanの理解が追いついてないので改めて入門する
はじめに
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されていなければok
はtrue
、closeされていればok
はfalse
となります。
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