はじめに
goをさわって数ヶ月ですが、雰囲気では書けていたものの
errorやエラーハンドリングについてはもやもやしたままだったので自分理解メモの②
関連
この記事の関連です。
アジェンダ
1. goのエラーについて
goのエラーについては以下に記載してみました。
【go】golangのエラー処理メモ - ①. errorとError型とカスタムErrorと
あえて項目にするまでもなかったけど前提として載せておきます
2. 一番簡単なエラーハンドリング
goは例外というモノがなく、
基本的には関数を呼び出した結果がエラーな場合はその場で処理します。
簡単なエラーハンドリング
その場で処理しちゃう例の一番簡単なものです。
サンプル
package main import ( "errors" "fmt" "os" ) func doSomething() error { return errors.New("doSomething is error.") } func main() { err := doSomething() // nilと比較してerrorオブジェクトが返ってたらエラーと見なす if err != nil { fmt.Println("main is failed") os.Exit(1) } fmt.Println("main is success") }
出力
$ go run src/go-error-handling/sample01/main.go main is failed exit status 1
エラーを無視する
また、エラーでも問題ない場合は_
や単に何も受け取らないことで無視する事もできます。
サンプル
package main import ( "errors" "fmt" ) func doSomething() error { return errors.New("doSomething is error.") } func doHoge() { err := doSomething() // nilと比較してerrorオブジェクトが返ってたらエラーと見なす if err != nil { fmt.Println("doHoge is failed") return } // 処理を続ける fmt.Println("doHoge is success") } func doFuga() { // _ でerrorを無視する _ = doSomething() // 処理を続ける fmt.Println("doFuga is success") } func doPiyo() { // 単に受け取らないことも doSomething() // 処理を続ける fmt.Println("doPiyo is success") } func main() { doHoge() doFuga() doPiyo() fmt.Println("main is success") }
出力
$ go run src/go-error-handling/sample02/main.go doHoge is failed doFuga is success doPiyo is success main is success
3. エラーハンドリングのパターン
上記のような簡単なエラーハンドリングでも事足りる事はあります。
しかし、
実際は下位のmethodで発生したエラーをハンドリングしては上位に返して…
を繰り返すと困る事がでてきます。
自分は以下のような事が困りました。
- 最上位でハンドリングで出力したものの根本原因がどこかわからない
- エラーによって分岐したいがどのようにするのがベストプラクティスかわからない
エラーハンドリングのパーターンとしては以下のサイトが非常に参考になります。
参考サイトを参考に、
エラーハンドリングは以下のようなパターンで行う事ができます。
エラーハンドリングのパターン
バッドノウハウなパターンも含まれてますが、
さっと書く使い捨てのscriptだったりではバッドノウハウパターンでも無いよりは良いかな、という印象です。
4. パターン1: errorの文字列で
errors.New
なりfmt.Errorf
の文字列を受け取り側で比較するというものです。
単純ですがバッドノウハウとされているパターンです。
サンプル
package main import ( "errors" "fmt" "os" ) const ( ERROR_MSG_01 = "doSomething is error. b is true" ERROR_MSG_02 = "doSomething is error. b is false" ) func doSomething(b bool) error { if b { return errors.New(ERROR_MSG_01) } else { return errors.New(ERROR_MSG_02) } return nil } func main() { err := doSomething(true) if fmt.Sprintf("%s", err) == ERROR_MSG_01 { fmt.Println("ERROR: ERROR_MSG_01に応じた処理を行う") os.Exit(1) } else if fmt.Sprintf("%s", err) == ERROR_MSG_02 { fmt.Println("ERROR: ERROR_MSG_02に応じた処理を行う") os.Exit(1) } fmt.Println("main is success") }
出力
$ go run src/go-error-handling/sample03/main.go ERROR: ERROR_MSG_01に応じた処理を行う exit status 1
困る点
ハンドリング後、さらに呼び出し元に返そうとするととたんに困ります…
// fmt.Errorfでさらに呼び出し元に返すとハンドリングできなく... if fmt.Sprintf("%s", err) == ERROR_MSG_01 { return fmt.Errorf("ERROR: err=%+v", err) }
5. パターン2: errors.Newのインスタンスで
errors.Newのインスタンスで比較するパターンです。
osパッケージなんかでも使われてたりします
https://golang.org/src/os/error.go#L11
サンプル
package main import ( "errors" "fmt" "os" ) const ( ERROR_MSG_01 = "doSomething is error. b is true" ERROR_MSG_02 = "doSomething is error. b is false" ) var ( ERROR_01 = errors.New(ERROR_MSG_01) ERROR_02 = errors.New(ERROR_MSG_02) ) func doSomething(b bool) error { if b { return ERROR_01 } else { return ERROR_02 } return nil } func main() { err := doSomething(false) if err == ERROR_01 { fmt.Println("ERROR: インスタンスERROR_01に応じた処理を行う") os.Exit(1) } else if err == ERROR_02 { fmt.Println("ERROR: インスタンスERROR_02に応じた処理を行う") os.Exit(1) } fmt.Println("main is success") }
コード自体はだいぶマシになった気はしてきます
出力
$ go run src/go-error-handling/sample04/main.go ERROR: インスタンスERROR_02に応じた処理を行う exit status 1
困る点
パターン1と同様ですがハンドリングの結果、上位にエラーを返すときに困ります。
そのまま返すとどこでエラーだったのかわかりずらくなり、
fmt.Errorfなどで新規にインスタンスを作ってしまうと別物になってしまいます。
// そのまま返し続けると結局どこのエラーだったのか...という感じに if err == ERROR_01 { return ERROR_01 }
// fmt.Errorfをかますと、違うインスタンスになってしまう if err == ERROR_01 { return fmt.Errorf("ERROR: err=%+v", ERROR_01) }
6. パターン3: カスタムエラーのインスタンスで
【go】golangのエラー処理メモ - ①. errorとError型とカスタムErrorと
でも触れたカスタムエラーを定義してそのインスタンスでハンドリングするパターンです。
1つのカスタムエラーを使い回してますが、カスタムエラー自体を複数用意しても良いです。
サンプル
package main import "fmt" const ( ERROR_MSG_01 = "doSomething is error. b is true" ERROR_MSG_02 = "doSomething is error. b is false" ) type MyError struct { Msg string Code int } func (err *MyError) Error() string { return fmt.Sprintf("%s, %d", err.Msg, err.Code) } var ( MyError_01 = &MyError{Msg: "MyError_001 is occur", Code: 30001} MyError_02 = &MyError{Msg: "MyError_002 is occur", Code: 30002} ) func doSomething(b bool) error { if b { return MyError_01 } else { return MyError_02 } return nil } func main() { err := doSomething(false) switch err { case MyError_01: fmt.Println("ERROR: インスタンスMyError_01に応じた処理") fmt.Printf("ERROR: %+v\n", err) case MyError_02: fmt.Println("ERROR: インスタンスMyError_02に応じた処理") fmt.Printf("ERROR: %+v\n", err) // ちなみにdoSomethingの返り値は // errorインターフェースであってMyError_01ではない。 // ココに以下のようなコードは書けない //fmt.Printf("ERROR: Msg=%s, Code=%d", err.Msg, err.Code) } fmt.Println("main is success") }
出力
$ go run src/go-error-handling/sample05/main.go ERROR: インスタンスMyError_02に応じた処理 ERROR: MyError_002 is occur, 30002 main is success
困る点
パターン2に同じ
7. パターン4: カスタムエラーの型で
カスタムエラーの型でハンドリングするパターンです。
err.(type)
という記述をしますが、
これはerrorがinterface型のために行えるらしいです。(知らなかった)
また、これをConversion構文というらしいです。
https://golang.org/ref/spec#Conversions
サンプル
package main import "fmt" type MyError_01 struct { Code int } func (err *MyError_01) Error() string { return fmt.Sprintf("this is MyError_01") } type MyError_02 struct { Code int } func (err *MyError_02) Error() string { return fmt.Sprintf("this is MyError_02") } func doSomething(b bool) error { if b { return &MyError_01{Code: 30001} } else { return &MyError_02{Code: 30002} } return nil } func main() { err := doSomething(false) switch e := err.(type) { case *MyError_01: fmt.Println("ERROR: MyError_01の型に応じた処理") fmt.Printf("ERROR: err=%+v, e=%+v, code=%d\n", err, e, e.Code) case *MyError_02: //fmt.Printf("ERROR: MyError_02のエラー, err=%+v, code=%d\n", e, e.Code) fmt.Println("ERROR: MyError_02の型に応じた処理") fmt.Printf("ERROR: err=%+v, e=%+v, code=%d\n", err, e, e.Code) } fmt.Println("main is success") }
出力
$ go run src/go-error-handling/sample06/main.go ERROR: MyError_02の型に応じた処理 ERROR: err=this is MyError_02, e=this is MyError_02, code=30002 main is success
困る点
これまでの中では一番すっきりした感があります。
しかし、参考サイトまんまの受け売りですが、
このままerrorを上に上に伝搬させていくと
上位ロジックが全てのerrorを把握している必要がある、という感じ。
ではどうするのか
pkg/errors
を使うと良いらしい
が、長くなったのでまた次の記事にします。
これもまた参考サイトが非常に参考になりました!
おわり
errorについても奥が深い!\(^o^)/