defer で指定した関数をエラーハンドリングする
概要
golangci-lint run --fix
で go の lint をしている際にdefer
の箇所で警告が発生しました。
警告はError return value is not checked (errcheck)
です。
defer throwErr(flag)
通常では、err = tx.Rollback()
でエラーを受ければ解決します。
しかし、defer
を使うと変数に代入できません。では、以下をのように実装すればよいと考えました。
defer func() error {
err = throwErr(flag)
return err
}()
しかし、同様にError return value is not checked (errcheck)
の警告が発生してしまいます。
つまり、この問題は「defer
で発生したエラーをどのように返すか」と同じでした。
調査をしていると「無名関数」「defer
」「名前付き戻り値」の理解が必要になりました。
本記事では結論を最初に記述したあと、解説を記述します。
go のバージョンは 1.17.5 です。
自分は Go 初学者のため、間違ったことを記述していたら、コメントにてご指摘のほどお願いします。
ソースコードは以下になります。
結論
defer
で評価した式をエラーハンドリングするには後述するソースコードになります。エラーを発生させるためだけのソースコードです。
returnErr
を注目すると、以下の処理が実行されています。
- 名前付き戻り値で
(err Error)
が指定されている -
defer
で無名関数を指定しているため、最後に実行される - 代入された
err
は名前付き戻り値なので関数returnErr
の戻り値として返される
package main
import (
"fmt"
"log"
)
func throwErr(flag bool) error {
fmt.Println("throwErr")
if flag {
return fmt.Errorf("err is occured in throwErr")
} else {
return nil
}
}
func returnErr(flag bool) (err error) { // 1. 名前付き戻り値で err が指定されている
fmt.Println("returnErr")
defer func() { // 2. defer で無名関数を指定しているため、最後に実行される
err = throwErr(flag) // 3. 評価された err は名前付き戻り値で自動的に戻り値になる
}()
return nil
}
func main() {
fmt.Println("main function is start")
if err := returnErr(true); err != nil {
log.Fatal(err)
}
fmt.Println("main function is end")
}
実行結果は以下になります。
> go run main.go
main function is start
returnErr
throwErr
2022/02/05 15:02:52 err is occured in throwErr
exit status 1
解説
結論で記述したソースコードの解説をします。
「無名関数」「defer
」「名前付き戻り値」の順番に解説します。
無名関数
まず、無名関数について説明します。
無名関数はその名前の通り、名前を持たない関数です。
関数内で使い捨ての関数をつかうときに便利です。
たとえば、以下のようなパターンで実行できます。
即時実行するためには関数の最後に()
が必要になります(最初なんで必要なのか考えたのですが、普通の関数でも実行するのにFunc()
みたいに最後に()
をつけるのと同じですよね)。
- 無名関数を変数に渡すパターン
- 引数を渡して即時実行するパターン
- 引数を渡さずに即時実行するパターン
以下がサンプルコードです。
package main
import "fmt"
func main() {
f := func(str string) {
fmt.Println(str)
}
f("sample1")
func(str string) {
fmt.Println(str)
}("sample2")
func() {
fmt.Println("sample3")
}()
}
実行結果は以下になります。
> go run main.go
sample1
sample2
sample3
defer
続いて、defer
について説明します。
defer
は、A Tour of Go(引用元 英語、日本語)にもあるとおり、後に続く処理を呼び出した関数の最後に実行させます。
ややこしいですが、変数は呼び出した時点で評価されます。
また、複数記述された場合、後入先出法で実行されます。
A defer statement defers the execution of a function until the surrounding function returns.
The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.
defer ステートメントは、 defer へ渡した関数の実行を、呼び出し元の関数の終わり(return する)まで遅延させるものです。
defer へ渡した関数の引数は、すぐに評価されますが、その関数自体は呼び出し元の関数が return するまで実行されません。
以下のコードでは、最終的にword
が"sample1"
になりますが、最初のdefer
では"sample1"
として評価されます。
また、後入先出法のため 2 つ目のdefer
("sample2"
)の後に、1 つ目のdefer
("sample"
)が実行されます。
package main
import "fmt"
func main() {
word := "sample1"
defer fmt.Println(word)
word = "sample2"
defer fmt.Println(word)
}
> go run main.go
sample2
sample1
最初にも述べた通り、f := defer fmt.Println(word)
のような変数に代入できません。
そのため、以下の実装はできません。
err = defer throwErr(flag) // エラーが発生する
では、どうやってdefer
をエラーハンドリングすればよいのでしょうか。
その方法が、「名前付き戻り値」です。
名前付き戻り値
名前付き戻り値とは通常の関数とは違い、戻り値に名前をつけることです。
戻り値に指定した変数名を宣言して代入(var
、:=
)することなく関数内で代入=
でき、return
文に変数の記述を省略できます(明示的に書くこともできます)。
以下のサンプルコード「名前付き戻り値の関数」では名前付き戻り値c
を宣言しているためreturn
のみで省略できます。
これを使って、defer
の実行後に変数を戻すことができます。
func add(a, b int) int {
return a + b
}
func add(a, b int) (c int) {
c = a + b
return
}
結論(再掲)
今まで解説をまとめて、defer
で評価した式をエラーハンドリングするには以下のコードになりました。
これまで解説した要素を使用していることがわかります。
- 名前付き戻り値で
err
が指定されている -
defer
で無名関数を指定しているため、最後に実行される - 評価された
err
は名前付き戻り値なので関数returnErr
の戻り値として返される
package main
import (
"fmt"
"log"
)
func throwErr(flag bool) error {
fmt.Println("throwErr")
if flag {
return fmt.Errorf("err is occured in throwErr")
} else {
return nil
}
}
func returnErr(flag bool) (err error) { // 1. 名前付き戻り値で err が指定されている
fmt.Println("returnErr")
defer func() { // 2. defer で無名関数を指定しているため、最後に実行される
err = throwErr(flag) // 3. 評価された err は名前付き戻り値で自動的に戻り値になる
}()
return nil
}
func main() {
fmt.Println("main function is start")
if err := returnErr(true); err != nil {
log.Fatal(err)
}
fmt.Println("main function is end")
}
実行結果は以下になります。
> go run main.go
main function is start
returnErr
throwErr
2022/02/05 15:02:52 err is occured in throwErr
exit status 1
エラーの再現
では、この書き方ではないとき、どのようなエラーが発生するのでしょうか。
今まで解説してきた方法に沿わない実装方法で警告とエラーを発生させてみます。
returnErr
の名前付き戻り値を通常の戻り値にしてみます。
すると、err
でerr declared but not used
のエラーが発生します。
func returnErr(flag bool) error {
fmt.Println("returnErr")
defer func() {
err := throwErr(flag) // err declared but not used
}()
return nil
}
また、以下のように実装しても、最初に書いたように、Error return value is not checked (errcheck)
の警告が発生します。
func returnErr(flag bool) error {
fmt.Println("returnErr")
defer func() error {
err := throwErr(flag)
return err
}()
return nil
}
そのため、結論で書いた方法が最初にエラーが発生せずに、戻り値が返されるソースコードになります。
func returnErr(flag bool) (err error) { // 1. 名前付き戻り値で err が指定されている
fmt.Println("returnErr")
defer func() { // 2. defer で無名関数を指定しているため、最後に実行される
err = throwErr(flag) // 3. 評価された err は名前付き戻り値で自動的に戻り値になる
}()
return nil
}
まとめ
defer
を用いたときのエラーハンドリングの方法についてまとめました。
必要な要素は、「無名関数」「defer
」「名前付き戻り値」の3つでした。
手順を踏むと、何が必要になっていたのかわかりました。
自分の解説が間違っている可能性があるので、間違いありましたらコメントで指摘のほどお願いします。
参考
Discussion