pseudoyu

pseudoyu

Blockchain | Programming | Photography | Boyi
github
twitter
telegram
mastodon
bilibili
jike

Go エラーハンドリングの概要と実践

前書き#

最近、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は関数の最後の戻り値であるべきであり、errornilでない場合、他の戻り値は使用できない状態であるべきであり、それらに対して追加の処理を行うべきではありません。エラー処理では、エラーを最初にチェックし、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 のエラー処理とベストプラクティスの要点です。今後は、エラータイプ、エラーラッピング、および一般的な問題についてもまとめていきます。

参考資料#

  1. Go のエラー処理のベストプラクティス
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。