Go の errors.New と fmt.Errorf の使い分け

Go でエラーを生成する方法として、errors.New と fmt.Errorf の2つがよく使われる。どちらもエラーを作成するが、用途が異なる。

errors.New の基本

errors.New は最もシンプルなエラー生成関数だ。固定のエラーメッセージを持つ error を返す。

import "errors"

var ErrNotFound = errors.New("item not found")

func findItem(id int) (string, error) {
    if id < 0 {
        return "", ErrNotFound
    }
    return "item", nil
}

errors.New で作成したエラーはパッケージレベルの変数として定義することが多い。これを「センチネルエラー」と呼び、errors.Is で比較できる。

fmt.Errorf の基本

fmt.Errorf は書式指定付きでエラーメッセージを生成できる。動的な情報をエラーに含めたいときに使う。

import "fmt"

func openFile(path string) error {
    if path == "" {
        return fmt.Errorf("invalid path: %s", path)
    }
    return nil
}

Printf と同じ書式指定子が使えるため、変数の値をエラーメッセージに埋め込める。

使い分けの基準

errors.New

固定メッセージのエラー。センチネルエラーとして定義し、後で比較したい場合に適している。

fmt.Errorf

動的な情報を含むエラー。ファイルパスや ID など、コンテキストをメッセージに埋め込みたい場合に使う。

エラーのラップ(Go 1.13 以降)

fmt.Errorf では %w 動詞を使ってエラーをラップできる。

func readConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("failed to read config %s: %w", path, err)
    }
    // ...
    return nil
}

%w でラップされたエラーは、errors.Unwrap や errors.Is で元のエラーを取り出せる。これにより、エラーにコンテキストを追加しながら、元のエラー情報も保持できる。

実践的な例

以下はデータベースからユーザーを取得する関数の例だ。

var ErrUserNotFound = errors.New("user not found")

func GetUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid user id: %d", id)
    }
    
    user, err := db.FindUser(id)
    if err != nil {
        return nil, fmt.Errorf("database error for user %d: %w", id, err)
    }
    
    if user == nil {
        return nil, ErrUserNotFound
    }
    
    return user, nil
}

固定のエラー(ErrUserNotFound)と動的なエラー(fmt.Errorf)を状況に応じて使い分けている。呼び出し側では errors.Is(err, ErrUserNotFound) でエラーの種類を判定できる。