🐀

defer で指定した関数をエラーハンドリングする

2022/02/10に公開

概要

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 初学者のため、間違ったことを記述していたら、コメントにてご指摘のほどお願いします。

ソースコードは以下になります。

https://github.com/Msksgm/err-handing-of-defer

結論

deferで評価した式をエラーハンドリングするには後述するソースコードになります。エラーを発生させるためだけのソースコードです。
returnErrを注目すると、以下の処理が実行されています。

  1. 名前付き戻り値で (err Error) が指定されている
  2. defer で無名関数を指定しているため、最後に実行される
  3. 代入された err は名前付き戻り値なので関数 returnErr の戻り値として返される
main.go
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()みたいに最後に()をつけるのと同じですよね)。

  • 無名関数を変数に渡すパターン
  • 引数を渡して即時実行するパターン
  • 引数を渡さずに即時実行するパターン

以下がサンプルコードです。

main.go 無名関数
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")が実行されます。

main.go defer
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で評価した式をエラーハンドリングするには以下のコードになりました。
これまで解説した要素を使用していることがわかります。

  1. 名前付き戻り値で err が指定されている
  2. defer で無名関数を指定しているため、最後に実行される
  3. 評価された err は名前付き戻り値なので関数 returnErr の戻り値として返される
main.go
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の名前付き戻り値を通常の戻り値にしてみます。
すると、errerr 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つでした。
手順を踏むと、何が必要になっていたのかわかりました。
自分の解説が間違っている可能性があるので、間違いありましたらコメントで指摘のほどお願いします。

参考

https://qiita.com/vengavengavnega/items/fd0782c30574a983b8a5

https://qiita.com/Ishidall/items/8dd663de5755a15e84f2

https://www.geeksforgeeks.org/anonymous-function-in-go-language/

Discussion