Go のカスタムエラー型の作成

Go の error インターフェースは Error() string メソッドさえ実装すれば満たせる。この柔軟性を活かして、追加情報を持つカスタムエラー型を作成できる。

基本的なカスタムエラー型

構造体に Error() メソッドを実装すれば、それがエラー型になる。

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}

これで ValidationError は error インターフェースを満たす。エラーメッセージだけでなく、どのフィールドで問題が起きたかという情報も保持できる。

カスタムエラーの使用例

func validateUser(u *User) error {
    if u.Name == "" {
        return &ValidationError{
            Field:   "name",
            Message: "名前は必須です",
        }
    }
    if u.Age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "年齢は0以上である必要があります",
        }
    }
    return nil
}

呼び出し側では errors.As を使ってカスタムエラー型を取り出せる。

err := validateUser(user)
var valErr *ValidationError
if errors.As(err, &valErr) {
    fmt.Printf("フィールド '%s' でエラー: %s\n", valErr.Field, valErr.Message)
}

Unwrap メソッドの実装

カスタムエラーが他のエラーをラップしている場合、Unwrap メソッドを実装する。

type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string {
    return fmt.Sprintf("query error [%s]: %v", e.Query, e.Err)
}

func (e *QueryError) Unwrap() error {
    return e.Err
}

Unwrap を実装すると、errors.Iserrors.As がエラーチェーンをたどれるようになる。

func executeQuery(q string) error {
    _, err := db.Exec(q)
    if err != nil {
        return &QueryError{Query: q, Err: err}
    }
    return nil
}

// 呼び出し側
err := executeQuery("SELECT * FROM users")
if errors.Is(err, sql.ErrNoRows) {
    // QueryError でラップされていても検出できる
}

Is メソッドのカスタマイズ

errors.Is の比較ロジックをカスタマイズしたい場合、Is メソッドを実装する。

type HTTPError struct {
    StatusCode int
    Message    string
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message)
}

func (e *HTTPError) Is(target error) bool {
    t, ok := target.(*HTTPError)
    if !ok {
        return false
    }
    return e.StatusCode == t.StatusCode
}

この実装では、ステータスコードが同じなら同一エラーとみなす。

err1 := &HTTPError{StatusCode: 404, Message: "page not found"}
err2 := &HTTPError{StatusCode: 404, Message: "user not found"}

errors.Is(err1, err2) // true(ステータスコードが同じ)

実践的な設計パターン

複数のエラー情報を持つ構造体を定義し、アプリケーション全体で一貫したエラーハンドリングを行う例を示す。

type AppError struct {
    Code    string
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// エラーコード定数
const (
    ErrCodeNotFound     = "NOT_FOUND"
    ErrCodeUnauthorized = "UNAUTHORIZED"
    ErrCodeInternal     = "INTERNAL"
)

エラーコードを使うことで、API レスポンスへの変換やログ出力が統一的に行える。カスタムエラー型は、エラーに意味のある構造を持たせたいときに有効な手段だ。