今川館

都内勤務の地味OLです

Goは関数の戻り値に名前を付けられる / deferの驚き

Goは関数を宣言するとき、戻り値に名前をつけることができる。*1

例えば、任意個数の整数を引数にとって合計値を返却するSumという関数を作るとして

func Sum(xs ...int) int {
	acc := 0
	for i := 0; i < len(xs); i++ {
		acc += xs[i]
	}
	return acc
}

今までわたしはこう書いていた。

戻り値に名前をつける

ところが、同じSumを、戻り値に名前をつけて定義してみると、実は色々不思議なルールがあることを知った。

戻り値に名前をつけられることを知って、わたしは最初はこのように書いていた。

func Sum(xs ...int) (result int) {
	acc := 0
	for i := 0; i < len(xs); i++ {
		acc += xs[i]
	}
	return acc
}

要するに、関数のシグニチャに宣言したresultという名前は単なるお約束で、触っちゃいけないと思っていたのだ。

名前をつけた戻り値は変数として利用して構わない

だが、どうやら名前をつけて定義した戻り値はゼロ値で初期化された変数として与えられ、関数本体から使って良いらしい。

つまり、こういうことをしても全く構わない。

func Sum(xs ...int) (result int) {
	for i := 0; i < len(xs); i++ {
		result += xs[i]
	}
	return result
}

戻り値に名前をつけて定義した関数はreturnだけで終了しても構わない

更に驚いたのが、戻り値に名前をつけておくと、returnだけで処理を終えても構わないのである。

以下のコードも当然コンパイルが通って動く。

func Sum(xs ...int) (result int) {
	for i := 0; i < len(xs); i++ {
		result += xs[i]
	}
	return
}

多値を返す場合も同様で、こんなことをしても(意味のない処理ではあるが)コンパイルは通り動いてくれる。

func Fn() (x int, y string, z [2]int) {
	return
}

Fn() // => 0 "" [0, 0]

deferと組み合わせる

deferキーワードを付けた文は、関数の処理修了後まで保留される。よく、コネクションやファイルのClose()に使えと言われる、アレである。

例えば上記のSum関数は、こう書いても一応正しく動く。

func Sum(xs ...int) (result int) {
	for _, x := range xs[:len(xs)-1] {
		result += x
	}
	return result + xs[len(xs)-1]
}

deferを使って戻り値をプリントしてみる

これをdeferで戻り値をプリントするよう変えて動かすと、

func Sum(xs ...int) (result int) {
	defer fmt.Printf("Sum> result = %s\n", result)
	for _, x := range xs[:len(xs)-1] {
		result += x
	}
	return result + xs[len(xs)-1]
}

Sum(1, 2, 3, 4) // => 10

[出力内容]

Sum> result = %!s(int=0)

この通り、よく考えれば当然なのだが、deferを行った時点のresultを束縛して後回しにされるので、初期値0が表示されてしまう。

無名関数を使う

無名関数で更に遅延すると期待通りの内容が表示される。

func Sum(xs ...int) (result int) {
	defer fmt.Printf("Sum> result = %s\n", result)
	defer func(){ fmt.Printf("Sum> result = %s\n", result) }()
	for _, x := range xs[:len(xs)-1] {
		result += x
	}
	return result + xs[len(xs)-1]
}

Sum(1, 2, 3, 4) // => 10

[出力内容]

Sum> result = %!s(int=10)
Sum> result = %!s(int=0)

ここで無名関数から出力した結果は6ではなく10になっている点に注意。
つまり、returnの後にresultの評価にワンチャンあるのだ。

また、deferは複数スタックされた場合は後ろから順に実行されていくので、無名関数を使って10が出力される処理の方が先に表示される。

deferの値を更に書き換えることも可能

上記コードでは無名関数の中から変数resultを参照できるので、内容を書き換えることもできる。
よって、以下のことも可能である。

func Sum(xs ...int) (result int) {
	defer func(){
		result -= 500
		fmt.Printf("Sum> result = %s\n", result)
	}()
	for _, x := range xs[:len(xs)-1] {
		result += x
	}
	return result + xs[len(xs)-1]
}
Sum(1, 2, 3, 4) // => -490

[出力内容]

Sum> result = %!s(int=-490)

deferはよく知らずに使うとろくでもないことをやらかしそうな気がした。

*1:これを「名前付き結果」とか「名前付き結果パラメータ」と呼ぶらしい。