Golang でのエラー処理
JavaScript (try… catch
文を使用) や Python (try… except
ブロックを使用) などの他の主流のプログラミング言語での従来の方法とは異なり、Go でエラーに対処するには別のアプローチが必要です。 なぜでしょうか? このブログの投稿では、Go アプリケーションでエラーを処理するために使用できるベスト プラクティスを紹介します。 この記事を理解するために必要なのは、Go がどのように動作するかの基本的な理解だけです。いつかは行き詰まると感じたら、時間をかけて未知の概念を研究してもかまいません。 宣言の中で他の識別子と同様に使用することができるが、バインディングを導入することはない。 空白識別子は、代入で左辺の値を無視し、プログラム内の未使用のインポートや変数に関するコンパイラ・エラーを回避する方法を提供します。 エラーを適切に処理する代わりに空白の識別子に割り当てる習慣は、定義された関数の値を明示的に無視することを決定したことになるので安全ではありません。
result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}
おそらくこれを行う理由は、関数からのエラー(または発生するかもしれないいかなるエラー)を期待していないからですが、これはプログラム内で連鎖効果を作成することができます。
Handling errors through multiple return values
エラーを処理する 1 つの方法は、Go の関数が複数の戻り値をサポートするという事実を利用することです。
func iterate(x, y int) (int, error) {}
上のコード例では、関数が失敗する可能性がある場合、定義済みの error
変数を返さなければなりません。 error
はGoのbuilt-in
パッケージで宣言されたインターフェース型で、そのゼロ値はnil
である。
type error interface { Error() string }
通常、エラーを返すことは問題があったことを意味し、nil
を返すことはエラーがなかったことを意味します。
result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }
したがって、関数 iterate
が呼ばれ err
が nil
と等しくない場合はいつでも、返されたエラーは適切に処理されるべきです – オプションとして、リトライまたはクリーンアップメカニズムのインスタンスを作成できます。 この方法でエラーを処理する唯一の欠点は、Goコンパイラからの強制力がないことで、作成した関数がどのようにエラーを返すかを決めなければなりません。 エラー構造体を定義して、それを戻り値の位置に配置することができます。 この方法の一つは組み込みのerrorString
構造体を使うことです(このコードはGoのソースコードにもあります):
package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }
上のコードサンプルでは、errorString
はError
メソッドが返すstring
を埋め込んでいます。
// Define an error structtype CustomError struct { msg string}// Create a function Error() string and associate it to the struct.func(error * CustomError) Error() string { return error.msg}// Then create an error object using MyError struct.func CustomErrorInstance() error { return &CustomError { "File type not supported" }}
新しく作成したカスタム エラーを再構築して、組み込みの error
構造体を使用することができます。 このため、どこでエラーが発生したかを特定することが非常に困難である。 エラーは出力されるまでに多くの関数を通過する可能性があります。 これを扱うには、スタックトレースの記録、エラーのラッピング、アンラッピング、 フォーマットといった基本的なエラー処理のプリミティブを提供する pkg/errors
パッケージをインストールするとよいだろう。 このパッケージをインストールするには、ターミナルで次のコマンドを実行する:
go get github.com/pkg/errors
スタックトレースやその他のデバッグを容易にする情報をエラーに追加する必要がある場合、New
または Errorf
関数を使用してスタックトレースを記録するエラーを提供することができる。 Errorf
は fmt.Formatter
インターフェイスを実装しており、fmt
パッケージルーン (%s
, %v
, %+v
など) を用いてエラーをフォーマットすることができます:
import( "github.com/pkg/errors" "fmt")func X() error { return errors.Errorf("Could not write to file")}func customError() { return X()}func main() { fmt.Printf("Error: %+v", customError())}
エラーメッセージではなくスタックトレースを表示するには、フォーマットパターンに %v
の代わりに %+v
を使用しなければならず、以下のコードのようなスタックトレースが出力されます。
Error: Could not write to filemain.X /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7main.customError /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15main.main /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19runtime.main /usr/local/opt/go/libexec/src/runtime/proc.go:192runtime.goexit /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471
Defer, panic, and recover
Go には例外はありませんが、「Defer, panic, and recover」と呼ばれる似たようなメカニズムが備わっています。 Go の思想は、JavaScript の try/catch/finally
文のような例外を追加すると、コードが複雑になり、ファイルを開くのに失敗するなど、あまりに多くの基本的なエラーを例外としてラベル付けするようプログラマーを促すことになるというものです。 throw/catch/finally
のように defer/panic/recover
を使うべきではありません。予期せぬ、回復不可能な失敗の場合のみです。
Defer
は、関数呼び出しをスタックに格納する言語メカニズムです。 各遅延関数は、パニックが呼び出されたかどうかにかかわらず、ホスト関数が終了したときに逆順に実行される。 遅延メカニズムはリソースの掃除に非常に便利です:
package mainimport ( "fmt")func A() { defer fmt.Println("Keep calm!") B()}func B() { defer fmt.Println("Else...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") D()}func D() { defer fmt.Println("If it's more than 30 degrees...")}func main() { A()}
This would compile as:
If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!
Panic
is a built-in function that stopped the normal execution flow. コードの中でpanic
を呼び出すということは、呼び出し側が問題を解決できないと判断したことを意味する。 したがって、panic
はあなたのコードやあなたのコードを統合する人がその時点で続行することが安全でないようなまれなケースにのみ使用されるべきです。
package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}
上記のサンプルは次のようにコンパイルされます:
If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Then we can't save the earth!panic: Global Warming!!!goroutine 1 :main.Break() /tmp/sandbox186240156/prog.go:22 +0xe0main.C() /tmp/sandbox186240156/prog.go:18 +0xa0main.B() /tmp/sandbox186240156/prog.go:14 +0xa0main.A() /tmp/sandbox186240156/prog.go:10 +0xa0main.main() /tmp/sandbox186240156/prog.go:26 +0x20Program exited: status 2.
上記に示すように、panic
が使用されて処理されない場合、実行フローは停止し、すべての遅延関数が逆順で実行され、スタックトレースが出力されます。 recover
は常に defer
関数内で呼び出されなければならず、そうでなければ nil
を返します:
package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") defer func() { if x := recover(); x != nil { fmt.Printf("Panic: %+v\n", x) } }() B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}
上のコード例でわかるように、recover
は panic
関数を放り込んでコンパイラーが返してしまうために全体の実行フローが止まってしまうことを防ぎます。
If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Panic: Global Warming!!!Then we can't save the earth!Program exited.
エラーを戻り値として報告するには、panic
関数が呼び出されたのと同じゴルーチン内でrecover
関数を呼び出し、recover
関数からエラー構造体を取得し、変数に渡す必要があります:
package mainimport ( "errors" "fmt")func saveEarth() (err error) { defer func() { if r := recover(); r != nil { err = r.(error) } }() TooLate() return}func TooLate() { A() panic(errors.New("Then there's nothing we can do"))}func A() { defer fmt.Println("If it's more than 100 degrees...")}func main() { err := saveEarth() fmt.Println(err)}
すべての遅延関数は関数呼び出し後かつ return 文の前に実行されることになります。 つまり、return文が実行される前に戻り値の変数を設定することができる。 上記のコードサンプルは次のようにコンパイルされます:
If it's more than 100 degrees...Then there's nothing we can doProgram exited.
Error wrapping
Previously error wrapping in Go was only access via using packages such as pkg/errors
. しかし、Go の最新リリースであるバージョン 1.13 では、エラーの折り返しがサポートされています。 リリース ノートによると、
エラー
e
は、w
を返すUnwrap
メソッドを提供することにより、別のエラーw
をラップすることができます。e
とw
の両方がプログラムで利用可能であり、e
がw
に追加のコンテキストを提供したり、w
に基づいてプログラムが判断を下すことを可能にしながらも、w
を再解釈することを可能にする。
ラップされたエラーを作成するために、fmt.Errorf
は %w
動詞を持ち、エラーを検査しラップを解くために、いくつかの関数が error
パッケージに追加されました:
errors.Unwrap
: この関数は基本的にプログラム中の基本エラーを検査し明らかにするものです。 これは Err
の Unwrap
メソッドを呼び出した結果を返す。 Err の型がエラーを返す Unwrap
メソッドを含んでいる場合。 それ以外の場合、Unwrap
はnil
を返す。
package errorstype Wrapper interface{ Unwrap() error}
以下はUnwrap
メソッドの実装例である:
func(e*PathError)Unwrap()error{ return e.Err}
errors.Is
: この関数では、センチネル値に対するエラー値を比較することができます。 この関数が通常のエラーチェックと異なるのは、センチネル値を1つのエラーと比較するのではなく、エラーチェーン内のすべてのエラーと比較する点である。
func Is(err, target error) bool
上記の基本的な実装では、Is
は err
またはそのチェーンの errors
が target (センチネル値) と等しいかどうかをチェックし報告します。
errors.As
: この関数は特定のエラー タイプにキャストする方法を提供します。 エラー チェーンでセンチネル値に一致する最初のエラーを探し、見つかった場合は、センチネル値をそのエラー値に設定し、true
:
package mainimport ( "errors" "fmt" "os")func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *os.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }}
このコードは Go のソース コードで見つけることができます。
コンパイラー結果:
Failed at path: non-existingProgram exited.
エラーは、その具体値がセンチネル値によってポイントされている値に割り当て可能であればセンチネル値に一致します。 As
は、センチネル値が error を実装する型または任意のインタフェース型への非 Nil ポインタでない場合、パニックを起こします。 As
は、err
が nil
の場合は false を返します。
Summary
Go コミュニティは最近、さまざまなプログラミング概念のサポートと、さらに簡潔で簡単なエラー処理方法の導入により、目覚ましい進歩を遂げています。 Go プログラムで発生する可能性のあるエラーを処理する方法について、何かアイデアはありますか?
Resources:
Go’s programming language specification on Type assertion
Marcel van Lohuizen’s talk at dotGo 2019 – Go 2 error values today
Go 1.13 release notes
LogRocket.LogRocket.Go 1.13 リリースノート
Go 1.13 リリースノート LogRocket.Go 1.13 リリースノート
LogRocket.Go 1.14 リリースノート Web アプリケーションの完全な可視性
LogRocket は、問題が自分のブラウザーで発生したかのように再生できるフロントエンド アプリケーション監視ソリューションです。 LogRocket は、エラーが発生した理由を推測したり、ユーザーにスクリーンショットやログダンプを要求したりする代わりに、セッションを再生して何が問題だったのかをすばやく理解することを可能にします。 また、Redux、Vuex、および @ngrx/store から追加のコンテキストを記録するプラグインもあります。
Redux のアクションと状態の記録に加え、LogRocket はコンソール ログ、JavaScript エラー、スタック トレース、ヘッダーとボディによるネットワーク リクエスト/レスポンス、ブラウザ メタデータ、カスタム ログを記録します。 また、DOM を計測してページ上の HTML と CSS を記録し、最も複雑な単一ページ アプリケーションのピクセルパーフェクトなビデオを再現します。
LogRocket は、問題が自分のブラウザーで発生したかのように再生できるフロントエンド アプリケーション監視ソリューションです。 LogRocket は、エラーが発生した理由を推測したり、ユーザーにスクリーンショットやログダンプを要求したりする代わりに、セッションを再生して何が問題だったのかをすばやく理解することを可能にします。 また、Redux、Vuex、および @ngrx/store から追加のコンテキストを記録するプラグインもあります。
Redux のアクションと状態の記録に加え、LogRocket はコンソール ログ、JavaScript エラー、スタック トレース、ヘッダーとボディによるネットワーク リクエスト/レスポンス、ブラウザ メタデータ、カスタム ログを記録します。 また、DOM を計測してページ上の HTML と CSS を記録し、最も複雑な単一ページ アプリケーションのピクセルパーフェクトなビデオを再現します。