Go/条件分岐と繰り返し
制御構造 編集
Goの構文がC言語と同様だとか、似ているとか言われるのを聞いたことがあると思います。
間違いです。
構文的にも、意味論的にもC言語とGoでは大きな隔たりがあり、「同様」や「似ている」の言葉の幅を考えても明確に間違いです。
条件分岐 編集
Goでは、条件分岐は、if文などで行えます。C言語と同様では有りません。。 C言語にある if や else や switch などの機能は、Goにもあります。しかし、たとえばスコープルール1つをとってもC言語にない要素をGoはもっています。
if文 編集
- C言語のif文
if ( 条件式 ) 文
- に対して
- Goのif文
if 条件式 { 文 }
- if文は条件式を囲む丸括弧があっても(たまたま)許容されますが、for文などの他の構文でカッコがあると構文エラーになります。 逆に、Goでは波括弧は必須です。
- また、Goでは条件式は真偽値である必要があり、i != 0 を単に i と書くことはできません。
- 整数値と真偽値の間には自動変換がなく、キャストもできません。そのため、比較演算子や論理演算子を使って条件式を書く必要があります。
- コード例
package main import ( "fmt" "math" ) func main() { if num := math.NaN(); num < 0.0 { fmt.Println("負") } else if num > 0.0 { fmt.Println("正") } else if num == 0.0 { fmt.Println("零") } else { fmt.Println("NaN") } }
- 実行結果
NaN
- if の条件式の前に「単純な文」を書くことができます(必須ではありません)。
- 上の例では簡素な変数宣言で、変数 num を宣言して math.NaN() で初期化しています(スコープはif文が終わるまでです)。
- math.NaN() は NaN(非数)を返す関数です。
- NaN はfloat64型で、全ての浮動所数点数と比較したとき必ず false になる変わった値です。
- NaN == NaN も false です。
- このため浮動小数点数がNaNであるかは math.IsNaN() を使います。
- if文は条件がfalseだったとき実行する else 節を持つことができます(必須ではありません)。
- else 節は、さらに別の条件のif節を持つことができます(必須ではありません)。
Goには条件演算子はありません |
Goには条件演算子(式 ? 値 : 値)はありません。 このため、条件演算子を書きたくなるケースでは、if文を使うことになります。 別解として、mapを使う方法があります。
|
switch文 編集
switch文をつかうと複雑になったif文を簡素に書換えることができます。
- コード例
package main import ( "fmt" "math" ) func main() { switch num := math.NaN(); { case num < 0.0: fmt.Println("負") case num > 0.0: fmt.Println("正") case num == 0.0: fmt.Println("零") default : fmt.Println("NaN") } }
- 実行結果
NaN
- if文のコード例をswitch文に置換えてみました。
- switch文の冒頭
switch num := math.NaN(); {
- は
switch num := math.NaN(); true {
- の省略形で、if文と同じく「単純な文」を書くことができます(必須ではありません)。
- 上の例では簡素な変数宣言で、変数 num を宣言して math.NaN() で初期化しています(スコープはswitch文が終わるまでです)。
- Cファミリーの言語を知っている人ならば、break がないのが気になると思いますが、Goのswitch文のcase節ではbreakが標準動作で、フォールスルーさせるためには、fallthrough 文を使います[1]。
- Goのcase文の式には、定数以外も書くこともでき上の例では(動的な)比較演算が使われています。
- この例では(暗黙の)true と比較しているので、最初にtrueになったcaseに対応する文が実行されるのですが、numはNaNでどの条件にも当てはまらないので default 節まで落ちてきます。
select文 編集
select文はswitch文と似ていますが、通信チャンネルの並行待ちを行います。
- Select文を使ったタイムアウト
package main import ( "fmt" "time" ) func main() { done := make(chan bool) go func(s int) { fmt.Printf("#%d..do\n", s) time.Sleep(time.Duration(s) * time.Second) fmt.Printf("#%d..done\n", s) done <- true }(2) select { case <-done: fmt.Println("Done!") case <-time.After(1 * time.Second): fmt.Println("Timeout!") } }
- 実行結果
#2..do Timeout!
イテレート - for文 編集
Goでは、For文がイテレート(繰返し;反復)する唯一の構文です(Goには、doやwhileはありません)。 For文には素朴なFor文と他の言語で言うForeach文の2種類があります。
- 素朴なFor文
for i := 0; i < n; i++ { /* 繰返す処理 */ }
- C言語のFor文に相当する機能です
- Foreach文的なFor文
for i, e := range collection { /* 繰返す処理 */ }
- C++の範囲For文やPythonのenumerate関数を伴ったFor文のようにコレクションに作用します
- スライスや配列、mapなどのコレクションから一つずつ要素を取り出していく
素朴なFor文 編集
まず、素朴のFor文の使い方を説明します。
for 初期化; 条件式; ポスト文 { /* 繰返す処理 */ }
これをIf文とGoto文を使って書き直すと。
{ 初期化 top: if !(条件式) { goto bottom } /* 繰返す処理 */ ポスト文 goto top bottom: }
- (上記のコードはFor文の働きをうまく書き表せていますが、Break文とContinue文があった場合の挙動はFor文とは異なります)
- 初期化およびポスト文は単純な文(簡単な変数宣言、インクリメント文、代入、関数呼び出し)でなくてはなりません。
- また、条件式は論理式 (trueかfalseを返す式)でなくてはならず、省略されると true が仮定されます。
- while はこの形式の最初と最後の文を省略した
- と表現します。
for 条件式 { /* 繰返す処理 */ }
- コード例
package main import "fmt" func main() { for i := 1; i < 6; i++ { fmt.Printf("%d * 2 = %d\n", i, i*2) } }
- 実行結果
1 * 2 = 2 2 * 2 = 4 3 * 2 = 6 4 * 2 = 8 5 * 2 = 10
For文の三項はそれぞれ省略できます。
for ; cond ; { S() }
⇒for cond { S() }
for true { S() }
⇒for { S() }
range 節を伴った For文 編集
GoにもForeach文的なFor文があり、For文のrange節を伴った構文を使用します。 スライス・配列・文字列・マップ・チェンネルなどのコレクションの要素を1つづつ(breakやreturnで中途退出しない限り)すべてイテレートします。
range 節がスライスの場合 編集
- スライス
package main import "fmt" func main() { s := make([]int, 6) for i, _ := range s { s[i] = 10 * i } fmt.Printf("%#v\n", s) for i, e := range s { fmt.Println(i, e) } }
- 実行結果
[]int{0, 10, 20, 30, 40, 50} 0 0 1 10 2 20 3 30 4 40 5 50
- この例ではintのスライス s を宣言し、最初のForで各要素に添え字の10倍の値を代入しています。
- range 節は多値代入の右辺となり、左辺の最初に添え字が次に要素の値が入ります。
- 不要な項は、8行目のようにブランク識別子( _ )を起きます。
range 節が配列の場合 編集
- 配列
package main import "fmt" func main() { s := [6]int{} for i, _ := range s { s[i] = 10 * i } fmt.Printf("%#v\n", s) for i, e := range s { fmt.Println(i, e) } }
- 実行結果
[]int{0, 10, 20, 30, 40, 50} 0 0 1 10 2 20 3 30 4 40 5 50
- 宣言部分以外は、スライスと同じです。実行結果も全く同じです。
- このようにスライスと配列は区別しにくいですが、配列は append で要素数を増したり、部分要素を返したりできません(部分要素を得ようとするとスライスが返る)。
- 性能的には、スライスは要素のアクセスは線形時間 O(n)、配列の要素のアクセスは定数時間 O(1)であることが大きな違いです。
- 配列 s をスライスに変換するには、
s[:]
とします。 - スライスを配列に変換する方法は、現時点(go-1.17.1)ではありません(配列の要素数はコンパイル時に確定することが要求されている)。
range 節が文字列の場合 編集
- 文字列
package main import "fmt" func main() { s := "abcd" fmt.Printf("%#v\n", s) for i, e := range s { fmt.Println(i, e) } for _, e := range s { fmt.Print(string(e)) } }
- 実行結果
"abcd" 0 97 1 98 2 99 3 100 abcd
- 文字列にFor・rangeを適用するとインデックスと1文字をイテレートします。
- 返される文字は Rune型 です(Goには char型 はありません)
- Rune型は整数型なので単純に文字列化すると数字になります。
- Rune型を文字コード(厳密にはコードポイント)として文字化するには、string() で型変換します。
range 節がマップの場合 編集
- マップ
package main import "fmt" import "sort" func main() { s := map[int]string{} for i := 0; i < 6; i++ { s[i] = fmt.Sprintf("str%v", 10*i) } fmt.Printf("%#v\n", s) for i, e := range s { fmt.Println(i, e) } keys := []int{} for i := range s { keys = append(keys, i) } fmt.Printf("%#v\n", keys) sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) fmt.Printf("%#v\n", keys) for i := 0; i < len(keys); i++ { fmt.Println(i, s[i]) } }
- 実行結果
map[int]string{0:"str0", 1:"str10", 2:"str20", 3:"str30", 4:"str40", 5:"str50"} 2 str20 3 str30 4 str40 5 str50 0 str0 1 str10 []int{4, 5, 0, 1, 2, 3} []int{0, 1, 2, 3, 4, 5} 0 str0 1 str10 2 str20 3 str30 4 str40 5 str50
- ここでは、キーを整数値を文字列とするマップ s を宣言しました
- マップはキーとマップに様々な型を使えますが、1つのマップではキー・値それぞれの型はすべて一致している必要があります。
- マップの要素へのアクセスは定数時間 O(1)です。
- マップにFor文を適用した場合、すべてのキーと値のペアを順にイテレートできますが、順序は不定です。
- キーの値順に要素を取り出したい場合は、一旦すべてのキーをスライスに取り出し、そのスライスをソートした上で、スライスをループさせることで実現します。
range 節がチャンネルの場合 編集
- チェンネル
package main import "fmt" func main() { c := make(chan int) go func() { defer close(c) for i := 0; i < 5; i++ { c <- i } }() for v := range c { fmt.Println(v) } }
- 実行結果
0 1 2 3 4
- For・rangeにチェンネルを適用するとクローズされるまで受信した値をイテレートします。
For文からの脱出 編集
また、C言語などと同様にGoでも、Break文やReturn文などがforブロック中({ } の内部)にあれば、すべての要素をイテレートする前にFor文から脱出します。
- Continue文 Break文 Return文
package main import "fmt" func main() { for i := 0; i < 5; i++ { if i == 2 { continue } fmt.Print(i) } fmt.Println() for i := 0; i < 5; i++ { if i == 2 { break } fmt.Print(i) } fmt.Println() for i := 0; i < 5; i++ { if i == 2 { return } fmt.Print(i) } fmt.Println("Can't reach it.") }
- 実行結果
0134 01 01
脚註 編集
- ^ Javaの -> と同じふるまいをします。ただしGoのswitch文はJavaの様に文の値を持ちません。