こんにちは。KOUKIです。
とあるWeb系企業でシステム開発をしています。Go言語を使ったプロジェクトに参加することが多いので割とGo言語に明るくなってきました。
最近では、goroutineを使った並行処理の実装も増えて来ており、日々スキルアップを実感します。
そんな中、Go言語の同期処理でよく使うChannelが結構便利だと感じたので、その使い方をまとめておこうと思います。
<目次>
Channelとは
Channelは、メモリアクセス同期に使える、Go言語の同期処理のプリミティブの一つです。
メモリに対するアクセスに使える一方で、ゴルーチン間の通信に使うのが最適です。
例をあげてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import "fmt" func main() { // channelを宣言する ch := make(chan int) // 無名関数をgoroutineで実行 go func() { ch <- 1 }() // 値を取り出す result := <-ch fmt.Println(result) } |
Channelを使用するには、最初にmake(chan datatype)で宣言する必要があります。以下の方法でもOKです。
1 2 |
var ch chan int chan = make(chan int) |
Channelへの値の格納/取り出しには「<-」アロー演算子を使えばOKです。
格納するときは「ch<-1」、取り出すときは「<-ch」です。
このチャネルを読み込もうとすると必ず同じメモリを参照するので、プログラムのどこか他の場所でその値を読み込ませることが可能です。
そして何より面白いのは、Channelはgoroutineをブロックします!
理由を説明する前に、このプログラムを一度実行します。
1 2 |
$ go run main.go 1 |
無事に値を取り出せました。
goroutineが起動するとGo言語のランタイムに登録されますが、それがいつ実行されるかわかりません。つまり、無名関数で実行したゴルーチンより先にメインの処理が終了してしまう可能性があります(無名関数は永久に実行されない)。
しかし、Channelはゴルーチンをブロックするので、確実に処理が実行されます。
1 2 |
// channelから値を読み込むまで、メインゴルーチンをストップする result := <-ch |
一方向Channel
Channelは、「<-」演算子を使うことで、Channelの向きを表現し、一方向だけにデータが流れるように宣言することが可能です。
つまり、送信だけ、受信だけのChannelを作れるということです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package main import "fmt" func main() { // 受信専用のChannle var recvChan chan<- interface{} // 送信専用のChannel var sendChan <-chan interface{} // chnnaleを初期化 ch := make(chan interface{}) // 暗黙的に型変換 recvChan = ch sendChan = ch go func() { // データを送信 recvChan <- "hello" }() // データを受信 fmt.Println(<-sendChan) } |
1 2 |
$ go run main.go hello |
ちなみに向きに反した実装を行うと怒られます。

Channelのブロック
もう一度以下の例をみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package main import "fmt" func main() { var writeCh chan<- interface{} var readCh <-chan interface{} ch := make(chan interface{}) writeCh = ch readCh = ch go func() { writeCh <- "Writing..." }() fmt.Println(<-readCh) } |
この例では、メインゴルーチンの中で無名関数のgoroutineを作成しています。
プログラムを実行すると次の出力が確認できます。
1 2 |
$ go run main.go Writing... |
「Writing」が出力される前に処理が終了しない理由は、前述の通り、Channelがメインゴルーチンをブロックしているからです。
つまり、以下のことが言えます。
- キャパシティが一杯のチャネルに書き込もうとするゴルーチンは、チャネルの空きが出るまで待機する
- 空のチャネルから読み込もうとするゴルーチンは、チャネルに要素が入ってくるまで待機する
では、チャネルへの書き込みを一旦、コメントアウトしてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package main import "fmt" func main() { // var writeCh chan<- interface{} var readCh <-chan interface{} ch := make(chan interface{}) // writeCh = ch readCh = ch go func() { // writeCh <- "Writing..." }() fmt.Println(<-readCh) } |
このコードを実行すると以下のdeadlockが発生します。
1 2 |
$ go run main.go fatal error: all goroutines are asleep - deadlock! |
これは、「fmt.Println(<-readCh)」にて、チャネルから値を読み込もうと永遠に待ち続けるため、発生する例外です。

チャネルは簡単に実装できるけど、制御はとても難しい。
Channelの戻り値
Channelは、オプションとして2つの値を返すことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package main import "fmt" func main() { ch := make(chan int) go func() { ch <- 1000 }() // チャネル空の値の取り出しと取り出し結果 num, ok := <-ch fmt.Printf("%v(%v)\n", num, ok) } |
1 2 |
$ go run main.go 1000(true) |
2つ目の値としてTrueが帰ってきましたね。
これは、以下の値を示します。
- プロセス内のどこかで書き込みがあったことで、読み込み演算子が読み込みができたかどうか
- 閉じたチャネルから生成されたデフォルトの値
閉じたチャネルのデフォルトの値は、チャネルを閉じた時に取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package main import "fmt" func main() { ch := make(chan int) go func() { ch <- 1000 }() num, ok := <-ch fmt.Printf("%v(%v)\n", num, ok) close(ch) // 閉じる num, ok = <-ch fmt.Printf("%v(%v)\n", num, ok) } |
1 2 3 |
$ go run main.go 1000(true) 0(false) <<< チャネルを閉じた時のデフォルトの値 |
0はint型デフォルト値ですね。String型はスペース、bool型はfalseが入ります。
面白いのが、閉じたチャネルの読み込みが行えることです。
これであるチャネルに関して、上流の書き込みに対し、複数の下流で読み込みが行えますし、条件判定も行えます。
Channelのループ処理
Channelは、ループ処理が可能です。for … rangeを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package main import "fmt" func main() { // Channleを初期化 ch := make(chan int) loop := func() { // Channelを閉じる defer close(ch) for i := 1; i <= 5; i++ { ch <- i } } // goroutineを起動 go loop() // Channelが閉じられるまでロープする for c := range ch { fmt.Println(c) } } |
プログラムを実行してみます。
1 2 3 4 5 6 |
$ go run main.go 1 2 3 4 5 |
1~5までの数値が出力されましたね。for … rangeにてChannelを繰り返し読み込んでいます。そして、Channelがクローズされた時、このループが終了します。
では、「defer close(ch)」をコメントアウトしたらどうなるでしょうか?
答えは、デッドロックが発生します。
1 2 3 4 5 6 7 8 9 |
$ go run main.go 1 2 3 4 5 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: |
これはChannelが閉じられないため、ループが完了せずに、永久にChannelを読み込み続けるからです。
Channelとシグナル
Channelを閉じると複数のゴルーチンに同時にシグナルを送ることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup ch := make(chan int) loop := func(i int) { defer wg.Done() // Channelが読み込まれるまで皇族処理をブロック <-ch fmt.Printf("Goroutin - %d\n", i) } // goroutineを複数起動 for i := 0; i < 5; i++ { wg.Add(1) go loop(i) } fmt.Println("ゴルーチンを解放します...") close(ch) wg.Wait() } |
上記のコードでは、loop関数を5つ、ゴルーチン化しました。そして、「<-ch」にてgoroutineをブロックしています。
このブロックは、「close(ch)」を実行した時に解放されます。
1 2 3 4 5 6 7 |
$ go run main.go ゴルーチンを解放します... Goroutin - 0 Goroutin - 1 Goroutin - 4 Goroutin - 3 Goroutin - 2 |
バッファつきChannel
Channelには、バッファをつけることができます。
1 2 3 |
// 2個のバッファ // 2回まで書き込み可能 ch := make(chan int, 2) |
このバッファが一杯になった場合、このチャネルへの書き込みはブロックされます。
この性質を利用すれば、Goroutineの最大起動数を制御することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package main import ( "fmt" "time" ) func main() { // goroutineの最大起動数を4にする ch := make(chan int, 4) for i := 1; i < 10; i++ { // チャネルに書き込みをする // バッファが満タンの時は、空きが出るまでブロック ch <- i fmt.Printf("The Number of goroutine: %d\n", len(ch)) // gorutineを起動 go func() { time.Sleep(100 * time.Millisecond) // バッファ解放 <-ch }() } } |
上記のコードでは、バッファサイズ4のChannelを用意しました。つまり、goroutineの最大起動数は、4つになるようにしています。
「fmt.Printf(“The Number of goroutine: %d\n”, len(ch))」にて、Channel数を出力できるようにしているので、この数が5以上にならなかったら成功です。
1 2 3 4 5 6 7 8 9 10 |
$ go run main.go The Number of goroutine: 1 The Number of goroutine: 2 The Number of goroutine: 3 The Number of goroutine: 4 The Number of goroutine: 4 The Number of goroutine: 2 The Number of goroutine: 3 The Number of goroutine: 4 The Number of goroutine: 4 |
OKですね。
Channelの初期化の重要性
Channelは、makeを使って初期化をしますが、初期化をしなかった場合はどうなるでしょうか。
読み込みの場合
最初に読み込みの場合をみてみましょう。
1 2 3 4 5 6 7 8 |
package main func main() { // Channelを宣言 var ch chan interface{} // 初期化なしで読み込み <-ch } |
1 2 |
$ go run main.go fatal error: all goroutines are asleep - deadlock! |
初期化していない場合は、デッドロックが走りますね。
書き込みの場合
次は、書き込みの場合です。
1 2 3 4 5 6 7 8 |
package main func main() { // Channelを宣言 var ch chan interface{} // 初期化なしで書き込み ch <- "Writing without initializing" } |
1 2 |
$ go run main.go fatal error: all goroutines are asleep - deadlock! |
書き込みの場合でも、デッドロックが走りました。
closeした場合
最後にcloseした場合です。
1 2 3 4 5 6 7 8 |
package main func main() { // Channelを宣言 var ch chan interface{} // 初期化なしでclose close(ch) } |
1 2 |
$ go run main.go panic: close of nil channel |
初期化なしでcloseした場合は、デッドロックではなく、panicが発生するようですね。
この結果から、Channelを扱う前には、確実に初期化をする必要があります。
おわりに
Channelはうっかりすると、プログラムそのものを永久にブロックしてしまう可能性があるなど、取扱注意な機能です。
しかし、goroutineをまとめる糊のような物でもあるので、使いこなしていきたいですね。
それでは、また!
最近のコメント