Go/条件分岐と繰り返し

出典: フリー教科書『ウィキブックス(Wikibooks)』
< 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を使う方法があります。

コード例
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	n := 0x5a5a5a5a5a5a5a5a
	size := int(unsafe.Sizeof(n)) * 8

	for i := 0; i < size; i++ {
		if (1 << (size - i - 1) & n) == 0 {
			fmt.Print("●")
		} else {
			fmt.Print("○")
		}
	}
	fmt.Println()

	m := map[bool]string{false: "●", true: "○"}
	for i := 0; i < size; i++ {
		fmt.Print(m[(1<<(size-i-1)&n) != 0])
	}
	fmt.Println()
}
実行結果
●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●
●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●●○●○○●○●


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() で初期化しています(スコープはif文が終わるまでです)。
Cファミリーの言語を知っている人ならば、break がないのが気になると思いますが、Goのswitch文のcase節ではbreakが標準動作で、フォールスルーさせるためには、fallthrough 文を使います[1]
Goのcase文の式には、定数以外も書くこともでき上の例では(動的な)比較演算が使われています。
この例では(暗黙の)true と比較しているので、最初にtrueになったcaseに対応する文が実行されるのですが、numはNaNでどの条件にも当てはまらないので default 節まで落ちてきます。

selecth文[編集]

selecth文は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

脚註[編集]

  1. ^ Javaの -> と同じふるまいをします。ただしGoのswitch文はJavaの様に文の値を持ちません。