2.3 フローと関数
この節ではGoの中のフロー制御と関数操作についてご紹介します。
フロー制御
フロー制御はプログラム言語の中の最も偉大な発明です。なぜならこれがあるだけで、あなたはとても簡単なフローの記述でとても複雑なロジックを表現できるからです。Goではフロー制御は3つの部分から成ります:条件判断、ループ制御及び無条件ジャンプです。
if
if
はあらゆるプログラミング言語の中で最もよく見かけるものかもしれません。この文法は大雑把に言えば:もし条件を満足しなければ何々を行い、そうでなければまたもう一つ別のことをやるということです。
Goの中ではif
分岐の文法の中は括弧で括る必要はありません。以下のコードをご覧ください。
if x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
Goのif
はすごいことに、条件分岐の中で変数を宣言できます。この変数のスコープはこの条件ロジックブロック内のみ存在し、他の場所では作用しません。以下に示します
// 取得値xを計算し、xの大きさを返します。10以上かどうかを判断します。
if x := computedValue(); x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
//ここではもしこのようにコールしてしまうとコンパイルエラーとなります。xは条件の中の変数だからです。
fmt.Println(x)
この条件の時は以下のようになります:
if integer == 3 {
fmt.Println("The integer is equal to 3")
} else if integer < 3 {
fmt.Println("The integer is less than 3")
} else {
fmt.Println("The integer is greater than 3")
}
goto
Goにはgoto
句があります- - ぜひ賢く使ってください。goto
は必ず事前に関数内で定義したタグにジャンプします。例えばこのようなループがあったと仮定します:
func myFunc() {
i := 0
Here: //この行の最初の単語はコロンを最後に持ってくることでタグとなります。
println(i)
i++
goto Here //Hereにジャンプします。
}
タグの名前は大文字小文字を区別します。
for
Goにで最も強力なロジックコントロールといえば、for
です。これはループでデータを読むのに使えます。while
でロジックをコントロールしても構いません。イテレーション操作も行えます。文法は以下の通りです:
for expression1; expression2; expression3 {
//...
}
expression1
、expression2
とexpression3
はどれも式です。この中でexpression1
とexpression3
は変数宣言または関数のコールの戻り値のようなものです。expression2
は条件判断に用いられます。expression1
はループの開始前にコールされます。expression3
は毎回ループする際の終了時にコールされます。
だらだら喋るよりも例を見たほうが早いでしょう。以下に例を示します:
package main
import "fmt"
func main(){
sum := 0;
for index:=0; index < 10 ; index++ {
sum += index
}
fmt.Println("sum is equal to ", sum)
}
// 出力:sum is equal to 45
時々複数の代入操作を行いたい時があります。Goのなかには,
という演算子はないので、平行して代入することができます。i, j = i+1, j-1
時々expression1
とexpression3
を省略します:
sum := 1
for ; sum < 1000; {
sum += sum
}
この中で;
は省略することができます。ですので下のようなコードになります。どこかで見た覚えはありませんか?そう、これはwhile
の機能です。
sum := 1
for sum < 1000 {
sum += sum
}
ループの中ではbreak
とcontinue
という2つのキーとなる操作があります。break
操作は現在のループから抜け出します。continue
は次のループに飛び越えます。ネストが深い場合、break
はタグと組み合わせて使用することができます。つまり、タグが指定する位置までジャンプすることになります。詳細は以下の例をご覧ください。
for index := 10; index>0; index-- {
if index == 5{
break // またはcontinue
}
fmt.Println(index)
}
// breakであれば10、9、8、7、6が出力されます。
// continueの場合は10、9、8、7、6、4、3、2、1が出力されます。
break
とcontinue
はタグを添えることができます。複数ネストしたループで外側のループからジャンプする際に使用されます。
for
はrange
と組み合わせてarray
、slice
、map
、string
のデータを読み込むことができます:
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}
Goは"複数の戻り値"をサポートしていますが、"宣言して使用されていない"変数に対してコンパイラはエラーを出力します。このような状況では_
を使って必要のない戻り値を捨てる事ができます。
例えば
for _, v := range map{
fmt.Println("map's val:", v)
}
switch
時々たくさんのif-else
を書くことでロジック処理を行いたくなるかもしれません。コードは非常に醜く冗長になります。またメンテナンスも容易ではなくなるので、switch
を使って解決することができます。この文法は以下のようなものです
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}
sExpr
とexpr1
、expr2
、expr3
の型は一致させる必要があります。Goのswitch
は非常に使い勝手がよく、式は必ずしも定数や整数である必要はありません。実行のプロセスは上から下まで、マッチする項目が見つかるまで行われます。もしswitch
に式がなければ、true
とマッチします。
i := 10
switch i {
case 1:
fmt.Println("i is equal to 1")
case 2, 3, 4:
fmt.Println("i is equal to 2, 3 or 4")
case 10:
fmt.Println("i is equal to 10")
default:
fmt.Println("All I know is that i is an integer")
}
5行目で、いくつもの値をcase
の中に集めています。また同時に、Goのswitch
はデフォルトでcase
の最後にbreak
があることになっているので、マッチに成功した後は他のcaseが実行されることはなく、switch
全体から抜け出します。ただし、fallthrough
を使用することであとに続くcaseコードを強制的に実行させることができます。
integer := 6
switch integer {
case 4:
fmt.Println("The integer was <= 4")
fallthrough
case 5:
fmt.Println("The integer was <= 5")
fallthrough
case 6:
fmt.Println("The integer was <= 6")
fallthrough
case 7:
fmt.Println("The integer was <= 7")
fallthrough
case 8:
fmt.Println("The integer was <= 8")
fallthrough
default:
fmt.Println("default case")
}
上のプログラムは以下のように出力します
The integer was <= 6
The integer was <= 7
The integer was <= 8
default case
関数
関数はGoの中心的な設計です。キーワードfunc
によって宣言します。形式は以下の通り:
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//ここはロジック処理のコードです。
//複数の値を戻り値とします。
return value1, value2
}
上のコードから次のようなことが分かります
- キーワード
func
でfuncName
という名前の関数を宣言します。 - 関数はひとつまたは複数の引数をとることができ、各引数の後には型が続きます。
,
をデリミタとします。 - 関数は複数の戻り値を持ってかまいません。
- 上の戻り値は2つの変数
output1
とoutput2
であると宣言されています。もしあなたが宣言したくないというのであればそれでもかみません。直接2つの型です。 - もしひとつの戻り値しか存在せず、また戻り値の変数が宣言されていなかった場合、戻り値の括弧を省略することができます。
- もし戻り値が無ければ、最後の戻り値の情報も省略することができます。
- もし戻り値があれば、関数の中でreturn文を追加する必要があります。
以下では実際に関数の例を応用しています(Maxの値を計算します)
package main
import "fmt"
// a、bの中から最大値を返します。
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
x := 3
y := 4
z := 5
max_xy := max(x, y) //関数max(x, y)をコール
max_xz := max(x, z) //関数max(x, z)をコール
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 直接コールしてもかまいません。
}
上ではmax
関数に2つの引数があることがわかります。この型はどれもint
です。第一引数の型は省略することができます(つまり、a,b int,でありa int, b intではありません)、デフォルトは直近の型です。2つ以上の同じ型の変数または戻り値も同じです。同時に戻り値がひとつであることに注意してください。これは省略記法です。
複数の戻り値
Go言語はCに比べ先進的な特徴を持っています。関数が複数の戻り値を持てるのもその一つです。
コードの例を見てみましょう
package main
import "fmt"
//A+B と A*B を返します
func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}
func main() {
x := 3
y := 4
xPLUSy, xTIMESy := SumAndProduct(x, y)
fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}
上の例では直接2つの引数を返しました。当然引数を返す変数に命名してもかまいません。この例では2つの型のみ使っていますが、下のように定義することもできます。値が返る際は変数名を付けなくてかまいません。なぜなら関数の中で直接初期化されているからです。しかしもしあなたの関数がエクスポートされるのであれば(大文字からはじまります)オフィシャルではなるべく戻り値に名前をつけるようお勧めしています。なぜなら名前のわからない戻り値はコードをより簡潔なものにしますが、生成されるドキュメントの可読性がひどくなるからです。
func SumAndProduct(A, B int) (add int, Multiplied int) {
add = A+B
Multiplied = A*B
return
}
可変長引数
Goの関数は可変長引数をサポートしています。可変長引数を受け付ける関数は不特定多数の引数があります。これを実現するために、関数が可変長引数を受け取れるよう定義する必要があります:
func myfunc(arg ...int) {}
arg ...int
はGoにこの関数が不特定多数の引数を受け付けることを伝えます。ご注意ください。この引数の型はすべてint
です。関数ブロックの中で変数arg
はint
のslice
となります。
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
値渡しと参照渡し
引数をコールされる関数の中に渡すとき、実際にはこの値のコピーが渡されます。コールされる関数の中で引数に修正をくわえても、関数をコールした実引き数には何の変化もありません。数値の変化はコピーの上で行われるだけだからです。
この内容を検証するために、ひとつ例を見てみましょう
package main
import "fmt"
//引数+1を行う、簡単な関数
func add1(a int) int {
a = a+1 // aの値を変更します。
return a //新しい値を返します。
}
func main() {
x := 3
fmt.Println("x = ", x) // "x = 3"と出力するはずです。
x1 := add1(x) //add1(x) をコールします。
fmt.Println("x+1 = ", x1) // "x+1 = 4" と出力するはずです。
fmt.Println("x = ", x) // "x = 3" と出力するはずです。
}
どうです?add1
関数をコールし、add1
のなかでa = a+1
の操作を実行したとしても、上述のx
変数には何の変化も発生しません。
理由はとても簡単です:add1
がコールされた際、add1
が受け取る引数はx
そのものではなく、x
のコピーだからです。
もし本当にこのx
そのものを渡したくなったらどうするの?と疑問に思うかもしれません。
この場合いわゆるポインタにまで話がつながります。我々は変数がメモリの中のある特定の位置に存在していることを知っています。変数を修正するということはとどのつまり変数のアドレスにあるメモリを修正していることになります。add1
関数がx
変数のアドレスを知ってさえいれば、x
変数の値を変更することが可能です。そのため、我々はx
の存在するアドレスである&x
を関数に渡し、関数の変数の型をint
からポインタ変数である*int
に変更します。これで関数の中でx
の値を変更することができるようになりました。この時関数は依然としてコピーにより引数を受け渡しますが、コピーしているのはポインタになります。以下の例をご覧ください。
package main
import "fmt"
//引数に+1を行う簡単な関数
func add1(a *int) int { // ご注意ください。
*a = *a+1 // aの値を修正しています。
return *a // 新しい値を返します。
}
func main() {
x := 3
fmt.Println("x = ", x) // "x = 3"と出力するはずです。
x1 := add1(&x) // add1(&x) をコールしてxのアドレスを渡します。
fmt.Println("x+1 = ", x1) // "x+1 = 4"を出力するはずです。
fmt.Println("x = ", x) // "x = 4"を出力するはずです。
}
このようにx
を修正するという目的に到達しました。では、ポインタを渡す長所はなんなのでしょうか?
- ポインタを渡すことで複数の関数が同じオブジェクトに対して操作を行うことができます。
- ポインタ渡しは比較的軽いです(8バイト)、ただのメモリのアドレスです。ポインタを使って大きな構造体を渡すことができます。もし値渡しを行なっていたら、相対的にもっと多くのシステムリソース(メモリと時間)を毎回のコピーで消費することになります。そのため大きな構造体を渡す際は、ポインタを使うのが賢い選択というものです。
- Go言語の
channel
、slice
、map
の3つの型はメカニズムを実現するポインタのようなものです。ですので、直接渡すことができますので、アドレスを取得してポインタを渡す必要はありません。(注:もし関数がslice
の長さを変更する場合はアドレスを取得し、ポインタを渡す必要があります。)
defer
Go言語のすばらしいデザインの中に、遅延(defer)文法があります。関数の中でdefer文を複数追加することができます。関数が最後まで実行された時、このdefer文が逆順に実行されます。最後にこの関数が返ります。特に、リソースをオープンする操作を行なっているようなとき、エラーの発生に対してロールバックし、必要なリソースをクローズする必要があるかと思います。さもなければとても簡単にリソースのリークといった問題を引き起こすことになります。我々はリソースを開く際は一般的に以下のようにします:
func ReadWrite() bool {
file.Open("file")
// 何かを行う
if failureX {
file.Close()
return false
}
if failureY {
file.Close()
return false
}
file.Close()
return true
}
上のコードはとても多くの重複がみられます。Goのdefer
はこの問題を解決します。これを使用した後、コードは減るばかりでなく、プログラムもよりエレガントになります。defer
の後に指定された関数が関数を抜ける前にコールされます。
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}
もしdefer
を多用する場合は、defer
はLIFOモードが採用されます。そのため、以下のコードは4 3 2 1 0
を出力します。
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
値、型としての関数
Goでは関数も変数の一種です。type
を通して定義します。これは全て同じ引数と同じ戻り値を持つ一つの型です。
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
関数を型として扱うことにメリットはあるのでしょうか?ではこの型の関数を値として渡してみましょう。以下の例をご覧ください。
package main
import "fmt"
type testInt func(int) bool // 関数の型を宣言します。
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// ここでは宣言する関数の型を引数のひとつとみなします。
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 関数の値渡し
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 関数の値渡し
fmt.Println("Even elements of slice are: ", even)
}
共有のインターフェースを書くときに関数を値と型にみなすのは非常に便利です。上の例でtestInt
という型は関数の型の一つでした。ふたつのfilter
関数の引数と戻り値はtestInt
の型と同じですが、より多くのロジックを実現することができます。このように我々のプログラムをより優れたものにすることができます。
PanicとRecover
GoにはJavaのような例外処理はありません。例外を投げないのです。その代わり、panic
とrecover
を使用します。ぜひ覚えておいてください、これは最後の手段として使うことを。つまり、あなたのコードにあってはなりません。もしくはpanic
を極力減らしてください。これは非常に強力なツールです。賢く使ってください。では、どのように使うのでしょうか?
Panic
ビルトイン関数です。オリジナルの処理フローを中断させることができます。パニックが発生するフローの中に入って関数
F
がpanic
をコールします。このプロセスは継続して実行されます。一旦panic
のgoroutine
が発生すると、コールされた関数がすべて返ります。この時プログラムを抜けます。パニックは直接panic
をコールします。実行時にエラーを発生させてもかまいません。例えば配列の境界を超えてアクセスする、などです。
Recover
ビルトイン関数です。パニックを発生させるフローの
goroutine
を復元することができます。recover
は遅延関数の中でのみ有効です。通常の実行中、recover
をコールするとnil
が返ります。他には何の効果もありません。もし現在のgoroutine
がパニックに陥ったらrecover
をコールして、panic
の入力値を補足し、正常な実行に復元することができます。
下の関数のフローの中でどのようにpanic
を使うかご覧ください
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
この関数は引数となっている関数が実行時にpanic
を発生するか検査します:
func throwsPanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() //関数fを実行します。もしfの中でpanicが出現したら、復元を行うことができます。
return
}
main
関数とinit
関数
Goでは2つの関数が予約されています:init
関数(すべてのpackage
で使用できます)とmain
関数(package main
でしか使用できません)です。この2つの関数は定義される際いかなる引数と戻り値も持ちません。package
のなかで複数のinit
関数を書いたとしても、もちろん可読性か後々のメンテナンス性に対してですが、package
の中では各ファイルに一つだけのinit
関数を書くよう強くおすすめします。
Goのプログラムは自動でinit()
とmain()
をコールしますので、どこかでこの2つの関数をコールする必要はありません。各package
のinit
関数はオプションです。しかしpackage main
は必ず一つmain
関数を含まなければなりません。
プログラムの初期化と実行はすべてmain
パッケージから始まります。もしmain
パッケージが他のパッケージをインポートしていたら、コンパイル時にその依存パッケージがインポートされます。あるパッケージが複数のパッケージに同時にインポートされている場合は、先にその他のパッケージがインポートされ、その後このパッケージの中にあるパッケージクラス定数と変数が初期化されます。次にinit関数が(もしあれば)実行され、最後にmain
関数が実行されます。以下の図で実行過程を詳しくご説明しています。
図2.6 main関数によるパッケージのインポートと初期化過程の図
import
Goのコードを書いている時は、importコマンドによってパッケージファイルをインポートすることがよくあります。私達が通常使う方法は以下を参考にしてください:
import(
"fmt"
)
その後コードの中では以下のような方法でコールすることができます。
fmt.Println("hello world")
上のfmtはGo言語の標準ライブラリです。実はGOROOT
環境変数で指定されたディレクトリの下にこのモジュールが加えられています。当然Goのインポートは以下のような2つの方法で自分の書いたモジュールを追加することができます:
相対パス
import "./model" //カレントファイルと同じディレクトリにあるmodelディレクトリ、ただし、この方法によるimportはおすすめしません。
絶対パス
import "shorturl/model" //gopath/src/shorturl/modelモジュールを追加します。
ここではimportの通常のいくつかの方法をご説明しました。ただ他にも特殊なimportがあります。新人を悩ませる方法ですが、ここでは一つ一つ一体何がどうなっているのかご説明しましょう
ドット操作
時々、以下のようなパッケージのインポート方法を見ることがあります
import( . "fmt" )
このドット操作の意味はこのパッケージがインポートされた後このパッケージの関数をコールする際、パッケージ名を省略することができます。つまり、前であなたがコールしたようなfmt.Println("hello world")はPrintln("hello world")というように省略することができます。
エイリアス操作
エイリアス操作はその名の通りパッケージ名に他の覚えやすい名前をつけることができます。
import( f "fmt" )
エイリアス操作の場合パッケージ関数をコールする際プレフィックスが自分たちのものになります。すなわち、f.Println("hello world")
_操作
この操作は通常とても理解しづらい方法です。以下のimportをご覧ください。
import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" )
_操作はこのパッケージをインポートするだけでパッケージの中の関数を直接使うわけではなく、このパッケージの中にあるinit関数をコールします。