Go言語

【Go言語】select文を理解したいんじゃ!!

select文はチャネルの送受信操作を多重化できる。

select文の構文

select文の書き方は、switch文に似ている。

select {
case <-ch1:
	// ch1から受信したときに実行される処理
case v := <-c2:
	// ch2から受信したときに実行される処理
	// 変数に値を入れることもできる
case ch3 <- y:
	// ch3に送信したときに実行される処理
default:
	// どのcaseも準備できていないときに実行される処理(省略可能)
}

ポイントは、「select文は上から順番に評価されない」こと。

チャネルへの送受信は実行可能かを判断して、可能であれば実行される。

つまり、送信の場合は「キャパシティ(バッファ)がいっぱいになっていないか」、受信の場合は、「チャネルに値が送信されたか、チャネルが閉じられたか」を確認し、処理を進める準備が出来ていれば実行される。

もし、どのチャネルも準備が出来ていなければ、defaultが実行される。もしdefaultを省略していたらselect文全体がブロックされる。

複数のcaseが実行可能な状況だった場合、どれか1つのcaseがランダムに実行される。

default節を省略したサンプルコード

defaultを省略した状態でselect文がブロックされるのを確認する。

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()
	ch := make(chan struct{})
	go func() {
		time.Sleep(5 * time.Second)
		close(ch)
	}()

	fmt.Println("Selectブロック中")
	select {
	case <-ch:
		fmt.Printf("ch1 close. ブロック時間: %v\n", time.Since(start))
	}
}

実行結果は

$ go run main.go
Selectブロック中
ch1 close. ブロック時間: 5.005249353s

 

default節を用意した場合のサンプルコード

すべてのcase文が準備が整っていない場合、default節が実行される。

package main

import "fmt"

func main() {
	ch1 := make(chan struct{})
	ch2 := make(chan struct{})
	// ch1, ch2ともに宣言しただけで、何も送信されてこない(受信することはない)
	// そのため、default節が実行される
	select {
	case <-ch1:
		fmt.Println("ch1")
	case <-ch2:
		fmt.Println("ch2")
	default:
		fmt.Println("default")
	}
}

実行結果は

$ go run main.go
default

 

複数のcaseが実行可能な場合のサンプルコード

複数のcaseが実行可能な状態の挙動を確認する。

サンプルコードに出てくるチャネル(ch1とch2)は宣言後すぐに閉じているので、いつでも何度でも受信可能な状態。

package main

import "fmt"

func main() {
	ch1 := make(chan struct{})
	ch2 := make(chan struct{})
	var count1, count2 int
	close(ch1)
	close(ch2)
	for i := 0; i < 100; i++ {
		// ch1, ch2ともに閉じられているので、準備が整っている(すぐに受信できる)
		select {
		case <-ch1:
			count1++
		case <-ch2:
			count2++
		}
	}

	fmt.Printf("count1 = %d, count2 = %d\n", count1, count2)
}

実行結果は次の通り。

// 1回目
$ go run main.go
count1 = 45, count2 = 55
// 2回目
$ go run main.go
count1 = 53, count2 = 47
// 3回目
$ go run main.go
count1 = 48, count2 = 52

ほぼ半分ずつ実行されている。Goのランタイムはcase文全体に対して疑似乱数による一様選択をしているため、等しく選択される可能性がある。

 

select文とfor文を組み合わせる

並行処理を書くときに、select文とfor文を組み合わせることが良くある。

package main

import (
	"fmt"
	"time"
)

func main() {
	idCh := make(chan int)
	ids := []int{1, 2, 3, 4, 5}
	go func() {
		// 1秒ごとにチャネルにidを送信
		// すべて送信したら閉じる
		defer close(idCh)
		for _, id := range ids {
			time.Sleep(1 * time.Second)
			idCh <- id
		}
	}()

	for {
		select {
		case id, ok := <-idCh:
			// チャネルが閉じられたら終了
			if !ok {
				return
			}
			fmt.Println(id)
		default:
			// 何もしない。ブロックしないためにdefault節を用意しておく
		}
	}
}

実行結果は

$ go run main.go
1
2
3
4
5

for文の後にも処理を続けたい場合は、Labeled Breakを使う。(returnすると、そこで処理が終了していまうため)

package main

import (
	"fmt"
	"time"
)

func main() {
	idCh := make(chan int)
	ids := []int{1, 2, 3, 4, 5}
	go func() {
		// 1秒ごとにチャネルにidを送信
		// すべて送信したら閉じる
		defer close(idCh)
		for _, id := range ids {
			time.Sleep(1 * time.Second)
			idCh <- id
		}
	}()

loop:
	for {
		select {
		case id, ok := <-idCh:
			// チャネルが閉じられたら終了
			if !ok {
				break loop
			}
			fmt.Println(id)
		default:
			// 何もしない。ブロックしないためにdefault節を用意しておく
		}
	}

	fmt.Println("Do something...")
	fmt.Println("Done!!")
}

実行結果は

$ go run main.go
1
2
3
4
5
Do something...
Done!!

 

参考図書

Tour of Goを終えた人にオススメです!!

イチオシ記事

1

自己紹介 フリーランスエンジニアをしているヨノと申します。 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエンド中 ...

2

はじめまして、フリーランスエンジニアのヨノと申します。 自己紹介 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエ ...

3

ネット上で色々言われているフリーランスエンジニア....。「本当はどうなの?」と思っている人は多いでしょう。 そこで本記事ではフリーランスエンジニア5年生の私が、ネット上の意見も引用しながら実態を解説 ...

-Go言語