Closed11

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
ハガユウキハガユウキ

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
終了
ハガユウキハガユウキ

クローズされたチャネルは、バッファが空になっても、チャネルが内包する型の初期値を受信し続けてしまう。しかも、クローズされたチャネルからデータを受信してもランタイムパニックは発生しない(クローズしたチャネルにデータを送信しようとするとランタイムパニックは出る)。

チャネルがクローズされているかどうかを検知するためには、チャネルからデータを取り出す際に第二変数で確認すれば良い。

このスクラップは2023/06/25にクローズされました