前書き#
最近、GeekTime の毛剣先生の Go の高度なトレーニングキャンプを復習し、学習のまとめを行っています。これは、エンジニアリングと原理のレベルに偏ったコースであり、非常に多くの知識をカバーしているため、自分自身のまとめと参照のためにシリーズを作成することにしました。これは、シリーズの最初の記事である「Go エラー処理」です。
Go のエラー処理メカニズム#
Go の組み込みエラー#
Go 言語のerror
は、通常のインターフェースであり、値を表します。
// http://golang.org/pkg/builtin/#error
// エラーインターフェースの定義
type error interface {
Error() string
}
// http://golang.org/pkg/errors/error.go
// エラーオブジェクトの構築
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
基本ライブラリには、Error: EOF
などのカスタムエラーが多数含まれており、errors.New()
は内部のerrorString
オブジェクトのポインタを返します。
エラーと例外#
Java や C++ などの他の言語とは異なり、Go では例外を導入せず、複数の戻り値を返すことでエラーを処理します。そのため、関数内でエラーインターフェースオブジェクトを呼び出し元に渡すことができます。
func handle() (int, error) {
return 1, nil
}
func main() {
i, err := handle()
if err != nil {
return
}
// 他の処理ロジック
}
注意すべきは、Go には panic のメカニズムがあり、recovery と組み合わせてtry...exception...
のような効果を実現できますが、Go の panic は例外とは異なり、例外は通常呼び出し元に処理を委ねるのに対して、Go の panic は真の例外の場合(インデックスの範囲外、スタックオーバーフロー、回復不可能な環境の問題など)に対応しており、コードが継続実行できないことを意味します。呼び出し元が panic を解決するとは想定しないでください。
Go の複数の戻り値によるエラー処理の方法は、開発者に大きな柔軟性を提供しています。以下の利点があります。
- シンプル
- 成功ではなく失敗を計画する
- 隠れた制御フローはありません
- エラーは完全に開発者に制御されます
- エラーは値なので、柔軟な処理が可能です
Go のエラー処理のベストプラクティス#
panic#
panic は真の例外の場合にのみ使用されます。例えば、
- プログラムの起動時に、依存関係のあるサービスが障害を起こした場合に panic で終了します。
- プログラムの起動時に、明らかに要件に合わない設定が見つかった場合に panic で終了します(防御プログラミング)。
- プログラムのエントリポイントで、gin のミドルウェアなどで panic を防ぐために recovery を使用します。
なぜなら、panic はプログラムを直接終了させるため、recovery を使用して処理する場合はパフォーマンスが悪く制御できないからです。したがって、他の場合は不可回復のプログラムエラーでない限り、直接 panic するべきではなく、エラーを返して開発者に処理させるべきです。
error#
一般的に、アプリケーションエラーを処理するためにgithub.com/pkg/errors
を使用しますが、公共のライブラリでは使用しないように注意してください。
エラーを判断するために複数の戻り値を使用する場合、error
は関数の最後の戻り値であるべきであり、error
がnil
でない場合、他の戻り値は使用できない状態であるべきであり、それらに対して追加の処理を行うべきではありません。エラー処理では、エラーを最初にチェックし、if err != nil
の場合にはすぐにエラーを返すことで、コードのネストを避けるべきです。
// エラーの例
func f() error {
ans, err := someFunc()
if err == nil {
// 他のロジック
}
return err
}
// 正しい例
func f() error {
ans, err := someFunc()
if err != nil {
return err
}
// 他のロジック
return nil
}
プログラムでエラーが発生した場合、errors.New
またはerrors.Errorf
を使用してエラー値を返します。
func someFunc() error {
res := anotherFunc()
if res != true {
errors.Errorf("結果が正しくありません。試行回数:%d", count)
}
// 他のロジック
return nil
}
他の関数を呼び出す際に問題が発生した場合は、直接エラーを返し、追加の情報が必要な場合はerrors.WithMessage
を使用します。
func someFunc() error {
res, err := anotherFunc()
if err != nil {
return errors.WithMessage(err, "その他の情報")
}
}
他のライブラリ(標準ライブラリ、企業の共有ライブラリ、オープンソースのサードパーティライブラリなど)からエラーが発生した場合は、errors.Wrap
を使用してスタック情報を追加します。これはエラーが最初に発生したときにのみ使用し、基本ライブラリや多くの参照されるサードパーティライブラリで使用しないようにする必要があります。
func f() error {
err := json.Unmashal(&a, data)
if err != nil {
return errors.Wrap(err, "その他の情報")
}
// 他のロジック
return nil
}
エラーを判断する必要がある場合は、errors.Is
を使用して比較します。
func f() error {
err := A()
if errors.Is(err, io.EOF){
return nil
}
// 他のロジック
return nil
}
エラータイプを判断する場合は、errors.As
を使用して代入します。
func f() error {
err := A()
var errA errorA
if errors.As(err, &errA){
// ...
}
// 他のロジック
return nil
}
ビジネスロジックのエラー(入力エラーなど)の場合は、独自のエラーディクショナリを作成し、エラーコードを含めてログに印刷できるようにし、明確なドキュメントを作成することが最善です。
エラー処理を補助するためにログを使用することがよくあります。返されない、無視されるエラーはログに出力する必要がありますが、エラーが発生する場所ごとにログを出力することは禁止されています。同じ場所でエラーが繰り返し発生する場合は、エラーの詳細を 1 回だけ出力し、発生回数を出力することが最善です。
まとめ#
以上が Go のエラー処理とベストプラクティスの要点です。今後は、エラータイプ、エラーラッピング、および一般的な問題についてもまとめていきます。