Goのチャネルを理解する
[到達したい状態]
- 受信用チャネルと送信用チャネルの違いを理解している
- チャネルを利用したコードを書ける
- 受信用チャネルがある場合のコードを書く
- selectを理解する
[やること]
- チャネルのコードを書く。
チャネルの目的
チャネルは、複数のゴルーチン間でデータを受けた渡すために存在している。
チャンネルの型
- 「chan 〇〇」型で、〇〇型のチャネルを表す型になる。つまり、チャネルにはサブタイプを指定する必要がある。
- この型は送受信に制限を加えていない。つまり、「送信専用のチャネル型」にも「受信専用のチャネル型」にもなることができる。
- 「<-chan 〇〇」型で、〇〇型の受信専用のチャネルを表す。
- 受信専用のチャネルは受信したデータを別の変数に送ることしかできない。「受信したデータを別の変数に送れる型」と認識しておけば良い
- 「chan<- 〇〇」型で、〇〇型の送信専用のチャネルを表す。
- 送信専用のチャネルは、データを送信してもらうしかできない。「データを送信してもらう型」と覚えておけば良い。
変数自体がその型と普通に考えると、矢印の意味がスッと理解できる
func main() {
// makeでも作れる。makeで作る場合、バッファサイズ(要はキューの大きさ)を指定できる
var ch chan int
// int型の受信専用のチャネル
var ch1 <-chan int
// 受信チャネルから整数値を取得
i := <-ch1
// int型の送信専用のチャネル
var ch2 chan<- int
// 送信チャネルにデータを送る
ch2 <- 5
}
送信や受信を決めていないチャネルは、どちらにもなれるので、送信用のチャネルや受信用のチャネルに代入できる。しかし、送信用のチャネルや受信用のチャネルを、送信や受信を決めていないチャネルに代入することはできない。なぜなら、送信用のチャネルは受信用のチャネルになることはできないから。
ch1 = ch // できる
ch2 = ch // できる
// 以降はできない
ch = ch1
ch = ch2
ch1 = ch2
ch2 = ch1
チャネルはキューのデータ構造。つまり、First In First Out(最初に入ったものが最初に出てくる)。
チャネルは、複数のゴルーチンが存在する時に使う。
チャネルは、そもそも複数のゴルーチン間でデータを受け渡すために存在します。そのため、ゴルーチンが複数存在しない状況でチャネルを使うと、エラーになります。
チャネルがデータを受信して、そのデータを出力する場合
チャネルからデータを受信するという処理は、裏を返せば、「他のゴルーチンがチャネルへデータを送信するのを待つ」ということである。つまり、Goでは、チャネルがデータを受信するまでは、そこで処理が止まることを意味する。
以下のコードでは、main関数を処理するための1つのゴルーチンしか存在しない。そのため、データを送信してくれる別のゴルーチンが存在しないので、エラーが起きる。
func main() {
var ch chan int
fmt.Println(<-ch) // => fatal error: all goroutines are asleep - deadlock!
}
ゴルーチン
ゴルーチンとはGoのランタイムによって管理される軽量な並行処理スレッドのこと。
main()関数も、1つのゴルーチンの中で実行されている。
go文を用いて、任意の関数を別のゴルーチンとして起動することで、処理を並行して走らせることができる
チャネルを使ったコード
func receiver(ch <-chan int) {
for {
i := <-ch
fmt.Println(i)
}
}
func main() {
ch := make(chan int)
go receiver(ch)
for i := 0; i < 10; i++ {
ch <- i
}
}
出力結果
0
1
2
3
4
5
6
7
8
9
これって、for文の処理はチャネルがデータを受信するまでは止まっていたってことか。なるほど。
closeを利用して書き換える
送信処理が完了したチャネルをclose関数で明示的に閉じることもできます。
クローズしたチャネルにはデータを送信することはできません。送信するとランタイムパニックが発生する。
func receiver(ch <-chan int) {
for {
i, isThere := <-ch
// チャネルのバッファ内が空でかつクローズされた状態の場合、falseになる
if !isThere {
break
}
fmt.Println(i)
}
}
func main() {
ch := make(chan int)
go receiver(ch)
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
select
- 一つのゴルーチンで複数のチャネルを利用している場合、あるチャネルが受信待ちだと、たとえ後続のチャネルがデータを受信していても、そこで処理がストップしてしまう。
- selectを使うことで、複数のチャネルをコントロールすることができる。その結果、ゴルーチンの処理を停止させずに済む
- 以下のreceiver関数では、forループがないと、どれか一つの条件節の処理が実行されて終了する。receiver関数のゴルーチンが終了するので、main関数でのチャンネルへの送信処理とかが全部失敗する。そのため、エラーが出る。
func receiver(done <-chan bool, ch2 <-chan int, ch3 <-chan int) {
// select文のcase節の条件式は、全てチャネルへの処理を伴っている必要がある
// case節はランダムに実行される
// forループがないと、どれか一つの条件説の処理が実行されて終了する
for {
select {
// doneチャネルが受信したら、このcaseの処理が実行される
// doneチャネルの値を別の変数に入れる必要がないなら、入れなくても良い
case <-done:
return
// ch2チャネルが受信したら、このcaseの処理が実行される
// 変数を宣言している理由は、このcase節内の処理で利用するからである。
case i, isThere := <-ch2:
if !isThere {
// doneが送られる前にゴルーチンが終了しないようにするために、continueを実行している
continue
}
fmt.Printf("偶数を受信しました, %d\n", i)
case j, isThere := <-ch3:
if !isThere {
continue
}
fmt.Printf("奇数を受信しました, %d\n", j)
}
}
}
func main() {
ch2 := make(chan int)
ch3 := make(chan int)
done := make(chan bool)
go receiver(done, ch2, ch3)
for i := 0; i < 10; i++ {
if i%2 == 0 {
ch2 <- i
} else {
ch3 <- i
}
}
close(ch2)
close(ch3)
done <- true
fmt.Println("終了")
}
処理結果
偶数を受信しました, 0
奇数を受信しました, 1
偶数を受信しました, 2
奇数を受信しました, 3
偶数を受信しました, 4
奇数を受信しました, 5
偶数を受信しました, 6
奇数を受信しました, 7
偶数を受信しました, 8
奇数を受信しました, 9
終了
クローズされたチャネルは、バッファが空になっても、チャネルが内包する型の初期値を受信し続けてしまう。しかも、クローズされたチャネルからデータを受信してもランタイムパニックは発生しない(クローズしたチャネルにデータを送信しようとするとランタイムパニックは出る)。
チャネルがクローズされているかどうかを検知するためには、チャネルからデータを取り出す際に第二変数で確認すれば良い。