Golangエラーハンドリング仕様

エラータイプ

エラーを宣言するためのいくつかのオプションがあります。使用ケースに最適なオプションを選択する前に、以下を考慮してください:

  • 呼び出し側がエラーをマッチングして処理する必要がありますか?もしそうなら、トップレベルのエラー変数またはカスタムタイプを宣言して、errors.Iserrors.As関数をサポートする必要があります。
  • エラーメッセージは静的な文字列か、文脈情報が必要な動的な文字列ですか?静的な文字列の場合はerrors.Newを使用できますが、後者の場合はfmt.Errorfやカスタムエラータイプを使用する必要があります。
  • 下流の関数から返される新しいエラーを渡していますか?それであれば、エラーラッピングセクションを参照してください。
エラーマッチング エラーメッセージ ガイダンス
いいえ 静的 errors.New
いいえ 動的 fmt.Errorf
はい 静的 errors.Newを使用したトップレベルvar
はい 動的 カスタムerrorタイプ

たとえば、静的な文字列を持つエラーを表す場合は、errors.Newを使用します。呼び出し側がこのエラーをマッチングしてハンドリングする必要がある場合は、errors.Isとのマッチングをサポートするために変数としてエクスポートします。

エラーマッチングなし

// パッケージ foo

func Open() error {
  return errors.New("開けませんでした")
}

// パッケージ bar

if err := foo.Open(); err != nil {
  // エラーを処理できません。
  panic("未知のエラー")
}

エラーマッチング

// パッケージ foo

var ErrCouldNotOpen = errors.New("開けませんでした")

func Open() error {
  return ErrCouldNotOpen
}

// パッケージ bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // エラーを処理
  } else {
    panic("未知のエラー")
  }
}

動的な文字列を持つエラーの場合、呼び出し側がそれをマッチングする必要がない場合は、fmt.Errorfを使用します。呼び出し側がそれをマッチングする必要がある場合は、カスタムのerrorを使用します。

エラーマッチングなし

// パッケージ foo

func Open(file string) error {
  return fmt.Errorf("%qファイルが見つかりません", file)
}

// パッケージ bar

if err := foo.Open("testfile.txt"); err != nil {
  // エラーを処理できません。
  panic("未知のエラー")
}

エラーマッチング

// パッケージ foo

type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("%qファイルが見つかりません", e.File)
}

func Open(file string) error {
  return &NotFoundError{File: file}
}


// パッケージ bar

if err := foo.Open("testfile.txt"); err != nil {
  var notFound *NotFoundError
  if errors.As(err, &notFound) {
    // エラーを処理
  } else {
    panic("未知のエラー")
  }
}

パッケージからエラー変数またはタイプをエクスポートすると、それらはパッケージの公開APIの一部になります。

エラーのラッピング

他のメソッドを呼び出す際にエラーが発生した場合、通常は次の3つの方法でそのエラーを処理します。

  • 元のエラーをそのまま返す。
  • %wを使ってエラーに文脈を追加し、fmt.Errorfを使用してそれを返す。
  • %vを使ってエラーに文脈を追加し、それを返す。

追加の文脈がない場合は、元のエラーをそのまま返します。これにより、元のエラーの種類とメッセージが保持されます。これは、基になるエラーメッセージに十分な情報が含まれており、エラーの発生元を追跡するのに十分な場合に特に適しています。

そうでない場合は、"接続が拒否されました"のような曖昧なエラーが発生しないように、できるだけエラーメッセージに文脈を追加してください。代わりに、"サービス fooを呼び出す際: 接続が拒否されました"のようなより有用なエラーを受け取ることができます。

エラーに文脈を追加する際に、呼び出し元がルート原因を一致および抽出できるかどうかに基づいて、fmt.Errorfを使用してエラーに文脈を追加し、%wまたは%vの動詞を選択してください。

  • 呼び出し元が基になるエラーにアクセスできる場合は%wを使用します。これはほとんどのラッピングエラーにとって良いデフォルトですが、呼び出し側がこの動作に依存する可能性があることを認識してください。したがって、既知の変数やタイプのラッピングエラーについては、関数の契約の一部としてそれらを記録およびテストしてください。
  • 基になるエラーを隠蔽するために%vを使用します。呼び出し元はそれを一致させることができませんが、必要に応じて将来%wに切り替えることができます。

返されたエラーに文脈を追加する際に、文脈を簡潔に保つために"failed to"のようなフレーズを避けてください。エラーがスタックを通過すると、階層ごとに積み重なっていきます。

非推奨:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %w", err)
}

// failed to x: failed to y: failed to create new store: the error

推奨:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %w", err)
}
// x: y: new store: the error

しかし、エラーが別のシステムに送信されると、メッセージがエラーであることが明確になるようにしてください(たとえばログで"err"タグや"Failed"の接頭辞など)。

不正な命名

グローバル変数として格納されたエラー値の場合、エクスポートされているかどうかに基づいて接頭辞にErrまたはerrを使用してください。ガイドラインを参照してください。エクスポートされていないトップレベルの定数や変数の場合は、アンダースコア(_)を接頭辞として使用します。

var (
  // 以下の2つのエラーをエクスポートして、このパッケージのユーザーがerrors.Isでそれらを一致させることができるようにします。
  ErrBrokenLink = errors.New("link is broken")
  ErrCouldNotOpen = errors.New("could not open")

  // このエラーは公開APIの一部としたくないため、エクスポートされていません。パッケージ内でerrorsを使用することがあります。
  errNotFound = errors.New("not found")
)

カスタムエラータイプの場合は、接尾辞にErrorを使用してください。

// 同様に、このエラーはエクスポートされているため、このパッケージのユーザーがerrors.Asで一致させることができます。
type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

// このエラーは公開APIの一部としたくないため、エクスポートされていません。パッケージ内でerrors.Asを使用することがあります。
type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("resolve %q", e.Path)
}

エラーの処理

呼び出し元が呼び出し先からエラーを受信した場合、エラーの理解に基づいてさまざまな方法でエラーを処理することができます。

これには以下が含まれますが、これに限定されません:

  • 呼び出し先が特定のエラー定義に合意している場合、errors.Isまたはerrors.Asでエラーをマッチングし、異なる方法で分岐を処理する
  • エラーをログに記録し、回復可能なエラーの場合は優雅に劣化させる
  • ドメイン固有の失敗状態を表す場合、よく定義されたエラーを返す
  • エラーがラップされていてもそのままでも、それを返す

呼び出し元がエラーをどのように処理するかに関係なく、通常は各エラーに対して一度だけ処理するべきです。例えば、呼び出し元はエラーをログに記録してから返すべきではなく、その呼び出し元もエラーを処理するかもしれません。

たとえば、以下のシナリオを考えてみましょう:

NG: エラーをログに記録して返す

スタックの上位の他の呼び出し元もこのエラーに類似のアクションを起こすかもしれません。これは、アプリケーションログに多くのノイズを生み出し、ほとんど利益がありません。

u, err := getUser(id)
if err != nil {
  // NG: 説明を参照
  log.Printf("Could not get user %q: %v", id, err)
  return err
}

OK: エラーをラップして返す

上位のスタックでこのエラーが処理されます。%wを使用することで、それらが関連する場合には、errors.Isまたはerrors.Asとエラーをマッチングできることを保証します。

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("get user %q: %w", id, err)
}

OK: エラーをログに記録し、優雅に劣化させる

操作が絶対に必要でない場合は、中断せずに回復することで優雅に劣化させることができます。

if err := emitMetrics(); err != nil {
  // メトリクスを書き込むのに失敗しても
  // アプリケーションを中断させてはいけません。
  log.Printf("Could not emit metrics: %v", err)
}

OK: エラーのマッチングと適切な優雅な劣化

呼び出し先が特定のエラーをその合意で定義し、失敗が回復可能な場合は、そのエラーケースをマッチングして優雅に劣化させます。その他の場合は、エラーをラップして返します。上位のスタックで他のエラーが処理されます。

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // ユーザーが存在しません。UTCを使用します。
    tz = time.UTC
  } else {
    return fmt.Errorf("get user %q: %w", id, err)
  }
}

アサーションの失敗の処理

型アサーションは、不正な型検出の場合に単一の返り値でパニックを発生させます。したがって、常に "comma, ok" のイディオムを使用してください。

非推奨:

t := i.(string)

推奨:

t, ok := i.(string)
if !ok {
  // エラーを優雅に処理する
}

panicの使用を避ける

プロダクション環境で実行されるコードはpanicを避ける必要があります。panicは連鎖的な障害の主要な原因です。エラーが発生した場合、関数はそのエラーを返し、呼び出し側がそのエラーをどのように処理するかを決定できるようにしなければなりません。

非推奨:

func run(args []string) {
  if len(args) == 0 {
    panic("引数が必要です")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

推奨:

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("引数が必要です")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

panic/recoverはエラー処理の手段ではありません。回復できないイベント(たとえば、nilの参照)が発生した時にのみpanicすべきです。プログラムの初期化中に例外が発生する可能性がある場合は、その状況はプログラムの起動中に処理されるべきです。

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

テストコードでも、失敗がマークされるようにするためにpanicの代わりにt.Fatalまたはt.FailNowを使用することが好ましいです。

非推奨:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  panic("テストのセットアップに失敗しました")
}

推奨:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  t.Fatal("テストのセットアップに失敗しました")
}