Goの学習を行った記録としてこの記事に残す。
この記事では Goでの関数についてまとめていく。
関数の宣言と呼び出し
関数の宣言は、キーワードfunc、関数名、引数、戻り値(返り値)の型の四つの部分から構成されている。
Goは静的型付け言語なので、引数の型の指定は必須。
関数が値を返すものであれば、returnがなくてはならない。
同じ型の引数を受け取るときは、型の指定をまとめることが出来る。
func test(num1, num2 int) int {
return num1 + num2
}
-
複数の戻り値
関数が複数の戻り値を返す場合、戻り値の型を(int, int, error)のように示す。
関数内でreturnを使って複数の戻り値を返す時には、全てをカンマで区切って返す。
問題なく処理を完了したときは、エラー値としてnilを返す。errorは戻り値の最後に(あるいは唯一の戻り値として)返す習慣になっている。 -
名前付き戻り値
Goでは関数から複数の値を返せるだけでなく、戻り値に名前を指定できる。
戻り値に名前をつける時に、関数の戻り値を指定する部分にカンマで区切って名前と型を列挙し()で囲む。
名前つき戻り値はゼロ値で初期化される。なので使わなくても、値を代入しなくても返すことができる。
名前つき戻り値はシャドーイングや戻り値を無視することが出来ることによる、混乱が生じることがある。
以上の点から名前付き戻り値は限定的である、しかしdeferを使うときは必要になる。 -
無名関数
関数内で別の関数を定義し、それを変数に代入することができる。
関数内部の関数として名前を持たない「無名関数」が使われる。
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
func(j int) {
fmt.Println("無名関数の中で", j, "を出力")
}(i)
}
}
宣言の方法は、funcに続けて引数、戻り値、{ を書く。
関数名を書くとコンパイル時にエラーになる。
無名関数も()で引数を指定して呼び出す。上の例だと、無名関数の外側のforのiを渡し、Iは無名関数の引数jに代入される。
無名関数は普通はあまり使わないが、deferを使う時とゴルーチンの起動をするときに役立つ。
-
defer
プログラムでは、ファイルやネットワーク接続といった一時的なリソースを作成することがよくあるが、そういったリソースは後始末が必要。この後始末は必ず行わなければならない。
Go言語ではこの後始末のコードはdeferを使って関数に付与する。
deferがあるとこの文を含む関数の終了時まで実行が延期される。
また関数内で複数のクロージャをdeferできる。
defer指定されたクロージャ内のコードはreturn文のあとで実行される。
defer文の関数には引数を指定できるので、渡された変数はクロージャが実行されるまで評価されない。
deferに戻り値を持つ関数を書くことはできるが、その戻り値を知る術はない。
deferと名前つき戻り値を使ったデータベースのトランザクションの後始末の方法は下記。
func DoSomeInsert(ctx context.Context, db *sql.DB, value1, value2 string)(err error){
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func{
if err == nil {
err = tx.Commit() //エラーがなければコミット
}
if err != nil {
tx.Rollback() //ロールバック
}
}()
_, err := tx.ExecContext(ctx, "INSERT INTO FOO (val) values $1", value1)
if err != nil {
retun nil
}
//処理
return nil
}
上の例では書き込みが一つでも失敗すればロールバックを実行し、全て成功ならコミットする。
またGoでよくあるのが、関数がリソースを割り当てるだけでなく、その後始末をするクロージャを返すというパターン。
以下が例
package main
import (
"io"
"log"
"os"
)
func getFile(name string) (*os.File, func(), error) { //liststart1
file, err := os.Open(name)
if err != nil {
return nil, nil, err
}
return file, func() {
file.Close()
}, err
} //listend1
func main() {
if len(os.Args) < 2 {
log.Fatal("ファイルが指定されていません")
}
f, closer, err := getFile(os.Args[1]) //liststart2
if err != nil {
log.Fatal(err)
}
defer closer() //listend2
data := make([]byte, 2048)
for {
count, err := f.Read(data)
os.Stdout.Write(data[:count])
if err != nil {
if err != io.EOF {
log.Fatal(err)
}
break
}
}
}
Goでは変数はかならず使わなければならないので、getFileから返されるcloserを呼び出さないとプログラムをコンパイルできないので、このような形にするとdeferの指定を忘れることはなくなる。また、deferでcloserを指定するときは()をつけ忘れないようにする必要がある。
-
Goは値渡し
値渡しというのは、関数に引数を渡した時にGoは必ず引数のコピーを作る。
実例は以下。
type person struct { //liststart1
age int
name string
} //listend1
func modifyFails(i int, s string, p person) { //liststart2
i *= 2
s = "さようなら"
p.name = "Bob"
} //listend2
func main() { //liststart3
p := person{}
i := 2
s := "こんにちは"
fmt.Println(i, s, p) // 2 こんにちは {0 }
modifyFails(i, s, p)
fmt.Println(i, s, p) // 2 こんにちは {0 }
} //listend3
このコードを実行すると、関数は渡された引数の値を変えられないということがわかると思う。
マップとスライスの場合では挙動が少し違うがここでは割愛する。
Goでは定数のサポートに制限があってもさほど問題がないのは値渡しであることがひとつの理由。
変数は値として渡されるので、関数を呼び出しても値を渡すのに使った変数が(マップ・スライスでない限り)変更されない。
関数に変更可能なものを渡す場合はポインタが必要になる。