今回は、GoroutineとChannelを学びましょう。
Goroutineは、複数の異なるtaskを一度に実行することができる、いわゆる”並列処理“を可能にするものです。
Channelは、Goroutineによる並列処理において、データのやりとりを行う際に使用されます。
<目次>
学習記事まとめ
Goroutineによる並行処理
プログラムの実行速度を向上させたい場合の取りうる選択肢の一つは、「処理のマルチタスク化」です。
一度に一つのtaskをこなすより、複数のtaskを同時にこなした方が処理時間を短くすることが可能です。
例えば、次のプログラムを例にあげてみましょう。
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 30 31 |
package main import ( "fmt" "io/ioutil" "log" "net/http" "time" ) func main() { fmt.Println("START", time.Now()) getWebPageSize("https://example.com/") getWebPageSize("https://golang.org/") getWebPageSize("https://golang.org/doc") fmt.Println("END", time.Now()) } func getWebPageSize(url string) { fmt.Println("Getting", url) response, err := http.Get(url) if err != nil { log.Fatal(err) } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } fmt.Println(len(body)) } |
このプログラムは、main関数に設定されたURLへコネクションを貼り、そのページのサイズを取得します。
実行してみましょう。
1 2 3 4 5 6 7 8 9 10 |
$ go run main.go START 2019-11-11 21:08:48.527033 +0900 JST m=+0.000765124 Getting https://example.com/ 1256 Getting https://golang.org/ 11071 Getting https://golang.org/doc 13512 END 2019-11-11 21:08:51.635589 +0900 JST m=+3.109313643 |
上記の実行結果の通り、通常は読み込まれた関数順に逐次処理でプログラムが実行されます。
そのため、処理時間がそれなりにかかってしまいますね。
これをGoroutineを使って解決します。
Goroutineを使うには、関数やメソッドの前に”go“キーワードをつけるだけです。
1 2 3 4 |
// Goroutineの構文 go <function> or <method> go myFunc("test") |
簡単なサンプルを以下に記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package main import "fmt" func main() { one() two() fmt.Println("end main()") } func one() { for i := 0; i < 50; i++ { fmt.Print(1) } } func two() { for i := 0; i < 50; i++ { fmt.Print(2) } } |
このサンプルコードは、並列処理でないバージョンのものです。
このプログラムを実行すると以下の通りになります。
1 2 |
$ go run sample/main.go 1111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222end main() |
one関数から順次実行されているのがわかると思います。
それでは、goキーワードを付けたバージョンをみていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package main import "fmt" func main() { go one() go two() fmt.Println("end main()") } func one() { for i := 0; i < 50; i++ { fmt.Print(1) } } func two() { for i := 0; i < 50; i++ { fmt.Print(2) } } |
今度は、one関数およびtwo関数の呼出しにgoキーワードを付与しています。
プログラムを実行してみましょう。
1 2 |
$ go run sample/main.go clear end main() |
しかし、出力された結果を見ると、one関数や two関数の処理結果が表示されていません。
実は、Goroutineが未完了の状態でも、プログラムの最後の関数が実行された時にプログラムは終了してしまいます。
この事象の回避策の一つは、timeパッケージのSleep関数を使うことです。
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 |
package main import ( "fmt" "time" ) func main() { go one() go two() time.Sleep(time.Second) fmt.Println("end main()") } func one() { for i := 0; i < 50; i++ { fmt.Print(1) } } func two() { for i := 0; i < 50; i++ { fmt.Print(2) } } |
main関数に設定しました。
プログラムを実行してみましょう。
1 2 |
$ go run sample/main.go 2222222222222222222222222222222222222222111111111111111111111222222222211111111111111111111111111111end main() |
先ほどとは違って並列実行のため、処理が幾分早く終了します。
goキーワードを使って、先ほどのgetWebPageSize関数の呼び出しをGoroutine化してみましょう。
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 30 31 32 |
package main import ( "fmt" "io/ioutil" "log" "net/http" "time" ) func main() { fmt.Println("START", time.Now()) go getWebPageSize("https://example.com/") go getWebPageSize("https://golang.org/") go getWebPageSize("https://golang.org/doc") time.Sleep(5 * time.Second) fmt.Println("END", time.Now()) } func getWebPageSize(url string) { fmt.Println("Getting", url) response, err := http.Get(url) if err != nil { log.Fatal(err) } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } fmt.Println(len(body)) } |
1 2 3 4 5 6 7 8 9 10 |
$ go run main.go START 2019-11-11 22:06:02.428337 +0900 JST m=+0.000621707 Getting https://golang.org/doc Getting https://example.com/ Getting https://golang.org/ 11071 1256 13512 END 2019-11-11 22:06:07.428832 +0900 JST m=+5.001082681 |
終了時間的にはGoroutineの方が遅く感じられますが、それはtime.Sleep関数を実行しているからです。ページ容量については、このプログラムの方が早く表示されます(実際に実行するとわかります)。
goステートメントは戻り値を返せない件
大変便利そうなGoroutineですが、一つ問題があります。
それは、”戻り値を返せないこと“です。
先ほどのソースコードを書き換えて、確認してみましょう。
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 30 31 |
package main import ( "fmt" "io/ioutil" "log" "net/http" "time" ) func main() { var size int size = go getWebPageSize("https://example.com/") size = go getWebPageSize("https://golang.org/") size = go getWebPageSize("https://golang.org/doc") time.Sleep(5 * time.Second) } func getWebPageSize(url string) int { fmt.Println("Getting", url) response, err := http.Get(url) if err != nil { log.Fatal(err) } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } return len(body) } |
getWebPageSize関数をページサイズを返却できるように書き換えました。
このプログラムを実行してみます。
1 2 3 4 5 6 |
$ go run main.go # command-line-arguments ./main.go:13:9: syntax error: unexpected go, expecting expression ./main.go:14:9: syntax error: unexpected go, expecting expression ./main.go:15:9: syntax error: unexpected go, expecting expression |
ご覧の通り、エラーが発生しましたね。
Goroutineの実行はいつ終了するのか保証されていません。そのため、戻り値を取得して利用することはできないようです。
このような場合、Go言語では”channel“を使って、データのやりとりを行います。
channelを使うには、いくつかの決まりごとがあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
1) "chan"キーワードと共にchannelを作成する var myChannel chan float64 myChannel = make(chan float64) // makeでchannelを作成 myChannel2 := make(chan float64) 2) channelからデータを送信する myChannel <- 3.14 // <- オペレータでデータを送信 3) channelからデータを受診する <- myChannel 4) 関数のパラメータにchannelを設定する func myChannel(channel chan int) {...} |
具体例を以下に記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package main import "fmt" func hello(myChannel chan string) { myChannel <- "Hello" } func main() { myChannel := make(chan string) go hello(myChannel) fmt.Println(<-myChannel) } |
この例では、main関数でchannelを作成しています。その後、Goroutineにてhello関数を実行し、myChannelへ文字列”Hello”を送信しています。
そして、hello関数の呼び出し元でmyChannelから”Hello”を受け取り、terminalへ出力しています。
プログラムを実行してみましょう。
1 2 |
$ go run main.go Hello |
channelによるデータの送信は、受診側がそのデータの使用を試みる前に必ず送信されます。
channelは、gorutineの中でこのデータを”ブロッキング“しているのです。
これにより、データを取り出すタイミングが自由自在です。
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" func seo(channel chan string) { channel <- "s" channel <- "e" channel <- "o" } func eyu(channel chan string) { channel <- "e" channel <- "y" channel <- "u" } func main() { channel1 := make(chan string) channel2 := make(chan string) go seo(channel1) go eyu(channel2) fmt.Print(<-channel1) fmt.Print(<-channel2) fmt.Print(<-channel1) fmt.Print(<-channel2) fmt.Print(<-channel1) fmt.Print(<-channel2) } |
1 2 |
$ go run main.go seeyou |
やってみよう
getWebPageSize関数を実装したプログラムでは、戻り値の返却に失敗していました。
これを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 30 31 32 |
package main import ( "fmt" "io/ioutil" "log" "net/http" ) func main() { channel := make(chan int) go getWebPageSize("https://example.com/", channel) go getWebPageSize("https://golang.org/", channel) go getWebPageSize("https://golang.org/doc", channel) fmt.Println(<-channel) fmt.Println(<-channel) fmt.Println(<-channel) } func getWebPageSize(url string, channel chan int) { fmt.Println("Getting", url) response, err := http.Get(url) if err != nil { log.Fatal(err) } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } channel <- len(body) } |
1 2 3 4 5 6 7 |
$ go run main.go Getting https://golang.org/doc Getting https://golang.org/ Getting https://example.com/ 11071 1256 13512 |
問題なく処理できましたね。
しかし、出力結果には処理したURLとページサイズが出力されていますが、このままだとどれがどれのページサイズなのかわかりません。
URLとページサイズの結びつきがわかるように、コードをバージョンアップしましょう。
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 30 31 32 33 34 35 36 37 38 39 40 |
package main import ( "fmt" "io/ioutil" "log" "net/http" ) // URLとページサイズをまとめる type Page struct { URL string Size int } func getWebPageSize(url string, channel chan Page) { response, err := http.Get(url) if err != nil { log.Fatal(err) } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } channel <- Page{URL: url, Size: len(body)} } func main() { pages := make(chan Page) urls := []string{"https://example.com/", "https://golang.org/", "https://golang.org/doc"} for _, url := range urls { go getWebPageSize(url, pages) } for i := 0; i < len(urls); i++ { page := <-pages fmt.Printf("%s: %d\n", page.URL, page.Size) } } |
1 2 3 4 5 |
$ go run main.go https://golang.org/: 11071 https://example.com/: 1256 https://golang.org/doc: 13512 |
Structを使って、データをまとめました。だいぶ分かりやすくなったと思います。
まとめ

最後にこの章で学んだことをまとめます。
・ 処理のマルチタスク化をしたい場合、Goroutineを使用する
・ Goroutineの宣言には、goキーワードを関数またはメソッドの直前に使用する
・ Goroutineが未完了の場合でもプログラムが終了したら一緒に終了する
・ Goroutineの処理を待ちたい場合は、time.Sleep関数を使用する
・ goキーワードを使った関数では、戻り値を設定できない
・ Goroutine間のデータのやりとりには、channelを用いる
・ channelは、chanキーワードと共にmake関数で作成する
・ channelの送受信には、<-オペレーターを用いる
次回
次回は、Go言語でのテストコードの書き方を学びましょう。
コメントを残す
コメントを投稿するにはログインしてください。