初めに
自分で簡単なWebサービスを作っていくにあたり、どこでどういうエラーが起こったのかわかりにくく、エラー解消に多くの時間を割くということがおきた。そのため、エラーハンドリングを学び、この問題を解消したいと考えた。
#version
go version go1.13.3
#対象読者
- エラーハンドリングって何?という人
- エラーハンドリングをやっていきたいが、何が正解かわからない人
- 一緒にエラーハンドリングについて考えてくれる方
躓いたところ
GolangのVersionが上がりライブラリerrorsに変更が加えられた。そのため、今までのエラーハンドリングと違うやり方になったらしい。それでは、どういう場合にどういうエラーハンドリングを採用すればいいかというところで沼にハマってしまった。
Go1.13 ライブラリerrors
エラーハンドリングをする意義
初めにで書いたように、エラーハンドリングは「どこに」「何をきっかけに」「どういう」エラーが起こったのかをサーバー側で判断できるようにする役割がある。また、もしクライアント側の操作にエラーがあった場合、どのようなエラーが起こりエラーが起こったのかを示すことで、クライアントに操作が間違っていることを示すことができる。
#Goにおけるエラーとは
Goにおけるエラーは以下のエラーインターフェースを実装するに過ぎない。
type error interface {
Error() string
}
そのため、自分でエラーを作るときは
type MyError struct {
Err error
// other fields
}
func (e *MyError) Error() string { return ... }
とポインタレシーバを使って自分が作ったエラー用の構造体でError()を実装することで、エラーインターフェースを満たす。
#エラーハンドリングの方法?
方法としては
- errorを単純に返す
- 型アサーションを使って場合分けをしてerror出力
- errorsを使ってerrorをWrapする
の3つではないかと。
errorを単純に返す
これは、今ままでに何回書いたかわからないが、
val, err := myFunction( args... );
if err != nil {
return err
}
// success
というerrorの基本的な返し方です。errの有無を err!=nil
で確認することができ、errorがあった場合errorを返す。
この返し方の問題点としては、特定のエラーが発生したかどうかを確認することができないことにある。
#型アサーションを使って場合分けをしてerror出力
ここで型アサーションについて
型アサーション は、インターフェースの値の基になる具体的な値を利用する手段を提供します。
インターフェースの値が特定の型を保持しているかどうかを テスト するために、型アサーションは2つの値(基になる値とアサーションが成功したかどうかを報告するブール値)を返すことができます。
t, ok := i.(T)
i が T を保持していれば、 t は基になる値になり、 ok は真(true)になります。
例えば、以下の例のように型アサーションを用いてエラーハンドリングを行う。(Go1.12以前)
package main
import (
"errors"
"fmt"
)
type MyError1 struct {
status int
err error
}
func (me *MyError1) Error() string { return "get error1" }
type MyError2 struct {
status int
Err error
}
func (me *MyError2) Error() string { return "get error2" }
func main(){
if err := MyFunction(); err != nil {
switch e := err.(type) {
case *MyError1:
fmt.Printf("%v", e)
case *MyError2:
fmt.Printf("%v", e)
}
}
}
上記のプログラムにおいてswitch文の際に使われているerr.(type)が型アサーションの部分である。
しかし、Go1.13で変わった。
package main
import (
"errors"
"fmt"
)
type MyError1 struct {
status int
err error
}
func (me *MyError1) Error() string { return "get error1" }
type MyError2 struct {
status int
Err error
}
func (me *MyError2) Error() string { return "get error2" }
func main() {
var e1 *MyError1
var e2 *MyError2
if err := MyFunction(); err != nil {
if errors.Is(err, e1) {
fmt.Println(err)
}
if errors.Is(err, e2) {
fmt.Println(err)
}
}
}
Go1.13で変わった点の一つは型アサーションを用いた条件分岐をerror.Is()関数を使って行うこととなった点である。
試したところ、以前のようにerr.(type)をSwith文を用いて実装することも場合によりますが可能なようです。
ここまででは、Is関数を使うメリットが見えてきませんが、Go1.13でともに追加されたWrapを使うときにIs関数が生きてきます。
##errorsを使ってerrorをWrapする
今まで fmt.Errorf("%v",err)
のように%vを使っていたのが大半でした。Go1.13からWrapをするための%w
が登場しました。
Errorfは以下のように第1引数にフォーマットを決めるstring型、第2引数にinterface{}型で、返り値はerror型となっている。
fmt.Errorf("%w",err)error{}
書式指定子にエラーオペランドを持つ%w
が含まれている場合、返されるエラーはUnwrapメソッドを実装している。(fmtパッケージ公式ドキュメント参照)
%w
の登場によって、エラーをラップすることができ、ラップしてきたエラーを順にUnWrap()関数で出力することが可能となった。そのため、どこでエラーが発生したのか特定しやすくなったのではないか。
func WrapMyError() error {
err := MySimpleError()
return fmt.Errorf("%w", err)//型は*fmt.wrapError
}
ここで、エラーをラップしてしまうとどのようなエラーでも*fmt.wrapErrorになってしまう
そのため、
func main(){
if err := WrapMyError(); err != nil {
switch e := err.(type) {
case *MyError1:
log.Printf("case1:%v", e)
case *MyError2:
log.Printf("case2:%v", e)
default:
log.Print("default:%v",e)
}
}
}
このコードのSwitch分では常にdefaultに条件分岐してしまう。上記でも示したように fmt.Errorf("%w",err)
がfmt.wrapError型を返すからです。ここでIs()関数が生きてきます。
func main(){
if err := WrapMyError(); err != nil {
//if err == MyError1{}と同じ
if errors.Is(err,MyError1){...}
//if err == MyError2{}と同じ
if errors.Is(err,MyError2){...}
}
}
このようにすることで、型がMyError1,MyError2なのか識別することができる。
また、if err != MyError1{}
のように使いたい場合は今までと同様に!=
を使えばいいそうだ。
次にUnWrap()関数について。
package main
import (
"errors"
"fmt"
)
var ErrNotFound = &SampleError{
statusCode: 404,
level: "Error",
msg: "not found",
}
type SampleError struct {
level string
statusCode int
msg string
}
func (e *SampleError) Error() string {
return fmt.Sprintf("%s: code=%d, msg=%s", e.level, e.statusCode, e.msg)
}
func main() {
err := func1()
if err != nil {
var sampleErr *SampleError
if errors.As(err, &sampleErr) { //errが&sampleErr型ならerrの値がvar sampleErrに代入される
switch sampleErr.level {
case "Fatal":
fmt.Printf("Fatal! %v\n", sampleErr)
case "Error":
fmt.Printf("Error! %v\n", sampleErr)
case "Warning":
fmt.Printf("Warning! %v\n", sampleErr)
}
}
fmt.Printf("%+v\n", err)
err = errors.Unwrap(err)
fmt.Printf("%+v\n", err)
err = errors.Unwrap(err)
fmt.Printf("%+v\n", err)
err = errors.Unwrap(err)
fmt.Printf("%+v\n", err)
return
}
fmt.Printf("エラーなし\n")
}
func func1() error {
err := func2()
if err != nil {
return fmt.Errorf("func1 error: %w", err)
}
return nil
}
func func2() error {
err := func3()
if err != nil {
return fmt.Errorf("func2 error: %w", err)
}
return nil
}
func func3() error {
return ErrNotFound
}
参考文献[4]のサンプルコードを少しかえたものです「playground」。このサンプルコードが全てを語ってくれていると思う。実行結果は
Error! Error: code=404, msg=not found// <- func3のエラー
func1 error: func2 error: Error: code=404, msg=not found//<func1のエラー
func2 error: Error: code=404, msg=not found//<-func2のエラー
Error: code=404, msg=not found //<-func3のエラー
<nil>
のようになる。Unwrap()関数は
14 func Unwrap(err error) error {
15 u, ok := err.(interface {
16 Unwrap() error
17 })
18 if !ok {
19 return nil
20 }
21 return u.Unwrap()
22 }
と定義されているため、err = errors.Unwrap(err)
はラップされている次のエラーを変数errに格納している。そのため上記の実行結果では、func1->func2->func3というWrapされた順番(つまり、Errorf("%w",err)された順に)に出力されていることがわかる。
また、errors.As(err,&sampleErr)
ではラップされているerrorの中に&sampleErr型が存在するかどうかを確認し、存在する場合&sampleErrをそのエラー値に設定している。そのため、sampleErrを出力すると、ラップされたエラー値が出力されている。確かに
As finds the first error in err's chain that matches target, and if so, sets target to that error value and returns true.
errのチェーン内でtargetに一致する最初のエラーを検出し、検出された場合、targetをそのエラー値に設定し、trueを返します。
とGodocにあります。
以上のことをまとめると、
1.if err!=nil {}で条件分岐
var,err := MyFunction()
if err != nil{}
2.As関数
もしerrの形がos.PathErrorならば、errの値がosPathErrorに代入される。
okはbool型で、型が一致していたらtrue
、していなかったらfalse
if e, ok := err.(*os.PathError); ok{}
to
var e *os.PathError
if errors.As(err, &e)
3.Is関数
if err == io.ErrUnexpectedEOF
to
if errors.Is(err, io.ErrUnexpectedEOF)
4.Wrap
fmt.Errorf("anything: %w", err)
%w
を用いてラッピングをする。
5.Unwrap
err = errors.UnWrap(err)
この記述をすることで、WrapされたエラーをWrapされた順番でerrに代入。
#My Best Answer
- アーキテクチャをしっかり考慮した上で
Unwrap
,As
,Is
を使った実装がベストであると感じた。
ここまでで、私が気づいたこととしては、関数の返り値(err)をwrapして上位層の関数へerrorを渡すことができるということ。つまり、アーキテクチャがしっかりしているコードで初めて効果をはっきするのではないか。私が現在実装中のコード(レイヤードアーキテクチャ)ではInfra層(責務:技術的関心事)->UseCase層(責務:業務的関心事)->Handler層のように値を渡していくため、Infra層のerrorをWrapしUseCase層に渡し、UseCase層のエラーをHandler層に渡し、Handler層でUnWrapして用いる。このように、アーキテクチャを考えて実装しているコード上ではこのerrorのWrapは非常に生きてくると考えている。
また、BadrequestなどのHandler層でのエラーなどはWrapする必要がないため、Switch文を使って処理をすることも可能なのではないかと感じた。
今回エラーを返すときに時間なども出力できたらと考え、fmt.Errorf()をエラー出力の際によく使っていたパッケージlog
とerrors.New()を使って代用できないかと検討したが、断念した。代案として、自分で作ったエラー型にtime.Time型のフィールドを作って、出力させることでパッケージlog
を使用しなくても詳細を出力できるのではないか。
- errの有無は
if err != nil{}
で確認 - 追加:statuscodeを用いたエラーハンドリングは
http.Error(w,"・・・",statusCode)
が便利
最後に
今回はエラーハンドリングについてまとめてみました。Go1.13にアップデートされたことから泥沼にハマってしまいましたが、なんとかまとめることができました。間違っている点があれば、教えていただけると幸いです。
#参考文献
[1]githubWiki~GolangErrorValueFAQ~
[2]Go1.13のError wrappingを触ってみる
[3]The Go Blog-Working with Errors in Go 1.13
[4]Goの新しいerrors パッケージ xerrors
[5]Godoc-errors