はじめに
今回はGoの基本文法である「フロー制御・並行処理」についてです。
目次
- For : ループ
- If : 条件分岐
- Switch : 条件分岐
- Defer : 遅延実行
- Goroutines : 並行処理
- Channel : チャネル
- Buffered Channel : バッファチャネル
- 参考文献
For : ループ
基本的には、for ループは;(セミコロン)
で2つのステートメントと1つの条件式で定義されます。
他の言語とは異なり、以下の3つの要素を括る()は不要です。
・初期化ステートメント : 最初のイテレーション(繰り返し)の前に初期化を実行
・条件式 : イテレーション毎に評価(評価が false の場合はイテレーションを停止)
・後処理ステートメント : イテレーション毎の最後に実行
種類としては、以下の通りです。
package main
import "fmt"
func main() {
// 古典的for
for i := 0; i < 100; i++ {
fmt.Println(i) // => 0 2 4 6 8...98
i++
}
// breakによるループ中断
j := 0
for {
fmt.Println(j) // => 0...99
j++
if j == 100 {
break
}
}
// 条件付きfor
k := 0
for k < 100 {
k++
}
// if の条件を満たせば、次のループへスキップ(continue)
for i := 0; i < 100; i++ {
if (i % 2 == 1) {
continue
}
fmt.Println(i)
i++
}
// 範囲節rangeによるfor
sports := [3]string{"baseball", "soccer", "tennis"}
for i, sport := range sports {
}
// 無限ループ
for {
fmt.Println("infinite roop")
}
}
初期化と後処理ステートメントの記述は省略可能です。
;
を省略することもできます。
// 初期化と後処理ステートメントを省略
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)
// 「;」を省略
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
If : 条件分岐
for ループと同様に、括弧 ( )は不要で、中括弧 { } は必要です。
package main
import (
"fmt"
)
func main() {
str1 := ""
int1 := 5
if int1 == 0 {
str1 = "Zero!"
} else if int1 > 0 {
str1 = "Positive!"
} else {
str1 = "Negative!"
}
fmt.Println(int1) // => 5
fmt.Println(str1) // => Positive!
}
for 同様、条件式の前にステートメントを記述することが可能
func pow(int, str, float float64) float64 {
if input := math.Pow(int, str); input < float {
return input
}
return float
}
func main() {
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
)
}
条件式で使われる比較演算子と論理演算子を載せておきます。
// 比較演算子
== 左辺と右辺が等しい
!= 左辺と右辺が等しくない
< 左辺が右辺より小さい
<= 左辺が右辺と等しいか小さい
> 左辺が右辺より大きい
>= 左辺が右辺と等しいか大きい
// 論理演算子
&& AND
|| OR
! NOT 否定
Switch : 条件分岐
switch は if と同様に条件によって処理を分けることができます。
switch case は上から下まで評価後、case の条件が一致すればそこで停止(break)します。
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X")
case "linux":
fmt.Println("Linux")
default:
fmt.Printf("%s.\n", os)
}
// => Go runs on Linux.
}
条件のないswitch は switch true と同じ意味となります。
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
Defer : 遅延実行
defer へ渡した関数の実行を、呼び出し元の関数の終わり(returnする)まで遅延させます。
defer へ渡した関数の引数は、すぐに評価されますが、その関数自体は呼び出し元の関数がreturnするまで実行されません。
package main
import "fmt"
func main() {
defer fmt.Println("hello") // 関数の最後に実行される
fmt.Println("world")
// => world
// => hello
}
defer へ渡した関数が複数ある場合、呼び出し元の関数がreturnするとき、defer へ渡した関数は LIFO(last-in-first-out)
の順で実行されます。
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i) // LIFOにより、「10 9 8 7・・・」(通常は「0 1 2 3・・・」)
}
fmt.Println("done")
Goroutines : 並行処理
goroutines(ゴルーチン) はGoのランタイム(プログラム実行時)に管理される軽量スレッドかつ並行処理です。
ゴルーチンは生成後、並行処理される新しい処理の流れとしてランタイムへ追加されます。
goroutine の生成方法は、並行処理させる関数名の前に「go」を付けるだけです。
go 関数名()
package main
import (
"fmt"
"time"
)
func my_goroutine(s string) {
for i := 0; i < 2; i++ {
time.Sleep(100 * time.Millisecond) // 0.1秒待つ
fmt.Println(s)
}
}
func hoge(s string) {
for i := 0; i < 2; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go my_goroutine("world")
hoge("hello")
}
実行結果
world
hello
hello
何回か再実行すると、結果が変わるはずです。
並列処理されているためですね。
実行結果
world
hello
world
ちなみにtimeをコメントアウトすると、helloだけが出力されるようになります。
これは、生成されたmy_goroutinesスレッドの実行前に main 関数の処理が終わってしまい、my_goroutines関数が未実行で終わっているためです。
package main
import (
"fmt"
)
func my_goroutine(s string) {
for i := 0; i < 2; i++ {
// time.Sleep(100 * time.Millisecond) // 0.1秒待つ
fmt.Println(s)
}
}
func hoge(s string) {
for i := 0; i < 2; i++ {
// time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go my_goroutine("world")
hoge("hello")
}
実行結果
hello
hello
それぞれの goroutine は独立しているため、 goroutine の処理が終わらなくてもプログラムは終了するというのも大きな特徴ですね。
Channel : チャネル
channel は、値をやり取りする通路とgoroutine の順序性を制御する役割で使われます。
channel は、make()で生成できます。
変数名 := make(chan 型)
値のやり取りは<-
で行われます。
ch <- send_data // ch に send_data 変数の値を送る(送信)
receive_data := <-ch // ch から値を抽出し、その値を receive_data 変数に代入(受信)
以下の場合、ch
は自身へ値が送信されるのを待つため、sum 関数のch <- sum
でgo sum(s1, ch)
が終了しない限り、以降の処理へは進みません。
ch
へ合計値であるsum
が送信されたら、結果を示すx, y
へsum
変数の値が代入(受信)されます。
package main
import "fmt"
func sum(s []int, ch chan int) {
sum := 0
for _, v := range s {
sum += v
}
ch <- sum // ch へ sum 変数の値を送る(送信)
}
func main() {
s1 := []int{1, 3, 5, 7, 9}
s2 := []int{2, 4, 6, 8, 10}
ch := make(chan int) // チャネル生成
go sum(s1, ch)
go sum(s2, ch)
x, y := <-ch, <-ch // ch から sum 変数の値を抽出し、x, y へ代入(受信)
fmt.Println(x, y, x+y)
close(ch) // チャネルを閉める(以降、chへ値の送信ができなくなる)
}
実行結果
30 25 55
channel は基本的に双方向ですが、単方向channelを生成することも可能です。
// 受信用channel
c1 := make(<-chan Type)
// 送信用channel
c2 := make(chan<- Type)
Buffered Channel : バッファチャネル
buffered channel はバッファ、つまりチャネルの容量が指定されたチャネルのことです。
buffered channel の生成方法は、型の後にバッファの数を指定するだけです。
チャネル名 := make(chan 型名, バッファ数)
package main
import "fmt"
func main() {
c := make(chan int, 2)
c <- 1
c <- 2
fmt.Println(<-c) // => 1 2
}
上記では、バッファ数と出力データ数が同じなためチャネル値の取り出し、出力が問題なく行えています。
ただ、ここでバッファ数と出力データ数を1つ増やした場合、どうなるか見てみましょう。
package main
import "fmt"
func main() {
c := make(chan int, 2)
c <- 1
c <- 2
c <- 3
fmt.Println(<-c)
fmt.Println(<-c)
fmt.Println(<-c)
}
実行結果
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/Users/(goの実行ファイルへパス)
exit status 2
バッファ数が2つにも関わらず、3つ目の値を入れようとしたためにエラーになりました。
参考文献
A Tour of Go
【Go言語入門】goroutineとは? 実際に手を動かしながら goroutineの基礎を理解しよう!
チャネル(channel)を通して、goroutine間でデータを送受信