every Tech Blog

株式会社エブリーのTech Blogです。

Go のエラーの扱いを振り返る

目次

この記事は every Tech Blog Advent Calendar 2025 の 1 日目の記事です。

はじめに

こんにちは、開発本部開発 1 部トモニテグループのエンジニアの rymiyamoto です。アドベントカレンダートップバッターを務めさせていただきます!

今回はまだ時期尚早ですが Go1.26 で errors.AsType が導入されることが予定されており、それに伴うエラーの扱いについて振り返ってみたいと思います。

tip.golang.org

※ この記事は執筆時点で最新の Go1.25.4 をベースに書いています。

Go でのエラー構造

Go のエラーは単なる Error メソッドを持つだけのインターフェースです。
このインターフェースを担保した型は error 型として扱うことができます。

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

github.com

この状態でのエラーでは単純に Error メソッドを呼び出して文字列を取得するだけです。そのため、エラーの種類を識別するために文字列の比較を行うことになってしまいます。またそのままエラー同士の比較もできますが、これはエラーの値が完全に一致しない限り false になってしまいます。

そのため Go1.13 のタイミングで再帰的エラーハンドリングが導入されました。

go.dev

再帰的エラーハンドリング

発生したエラーに対して新たな情報を追加し、エラーチェーンを構築するアプローチです。この手法により、エラーが発生した元のコンテキストから、そのエラーをキャッチして処理した箇所までの全体像を把握することが可能になります。

実際に fmt.Errorf%w 返しているエラーの型は以下のようになっています。

type wrapError struct {
    msg string // 全体のエラーメッセージ
    err error // ラップ元のエラー
}

func (e *wrapError) Error() string {
    return e.msg
}
func (e *wrapError) Unwrap() error {
    return e.err
}

github.com

これによりラップする前のエラーを Unwrap メソッドで取得することができ、階層的な構造になっていいてもエラーを辿ることができます。

エラーハンドリングのパターン

errors.As で値取り出してチェック

errors.As の実装は以下のようになっており、処理の流れをまとめるとこのようになります。

import (
    "internal/reflectlite"
)

func As(err error, target any) bool {
    if err == nil {
        return false
    }
    if target == nil {
        panic("errors: target cannot be nil")
    }
    val := reflectlite.ValueOf(target)
    typ := val.Type()
    if typ.Kind() != reflectlite.Ptr || val.IsNil() {
        panic("errors: target must be a non-nil pointer")
    }
    targetType := typ.Elem()
    if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
        panic("errors: *target must be interface or implement error")
    }
    return as(err, target, val, targetType)
}

func as(err error, target any, targetVal reflectlite.Value, targetType reflectlite.Type) bool {
    for {
        // 現在のエラー値が、ターゲットの型に代入可能かをチェック
        if reflectlite.TypeOf(err).AssignableTo(targetType) {
            targetVal.Elem().Set(reflectlite.ValueOf(err))
            return true
        }
        // エラーが独自の As メソッドを実装している場合、それを呼び出してチェック
        if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
            return true
        }
        // エラーチェーンを辿る: Unwrap() メソッドでラップされた下位のエラーを取得
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            // 単一のエラーをラップしている場合: アンラップして次のループで再チェック
            err = x.Unwrap()
            if err == nil {
                return false
            }
        case interface{ Unwrap() []error }:
            // 複数のエラーをラップしている場合: それぞれに対して再帰的にチェック
            for _, err := range x.Unwrap() {
                if err == nil {
                    continue
                }
                if as(err, target, targetVal, targetType) {
                    return true
                }
            }
            return false
        default:
            return false
        }
    }
}

var errorType = reflectlite.TypeOf((*error)(nil)).Elem()

github.com

これにより比較対象のエラーの型に代入可能かをチェックし、可能であればそのエラーの値を取得することができます。

var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) {
    fmt.Println("MySQL error occurred:", mysqlErr.Number)
}

細かいですが errors.As のターゲットは必ずポインタである必要があります。これは、Go が関数の引数を値渡しするため、エラーチェーン内の見つかったエラーをターゲット変数に実際に書き込む(代入する)ためには、呼び出し元で定義した変数のメモリアドレス(ポインタ)を渡す必要があるためです。

errors.Is で値の一致

errors.Is の実装は以下のようになっており、基本の流れは errors.As と同じですが、比較対象のエラーの型に代入可能かをチェックする代わりに、現在のエラー値が、比較対象のターゲットエラー(target)と厳密に等しいか(err == target)をチェックします。

func Is(err, target error) bool {
    if err == nil || target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    return is(err, target, isComparable)
}

func is(err, target error, targetComparable bool) bool {
    for {
        // 現在のエラー値が、ターゲットエラーと厳密に等しいか(err == target)をチェック
        if targetComparable && err == target {
            return true
        }
        // エラーが独自の Is メソッドを実装している場合、それを呼び出してチェック
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        // エラーチェーンを辿る: Unwrap() メソッドでラップされた下位のエラーを取得
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            // 単一のエラーをラップしている場合: アンラップして次のループで再チェック
            err = x.Unwrap()
            if err == nil {
                return false
            }
        case interface{ Unwrap() []error }:
            // 複数のエラーをラップしている場合: それぞれに対して再帰的にチェック
            for _, err := range x.Unwrap() {
                if is(err, target, targetComparable) {
                    return true
                }
            }
            return false
        default:
            return false
        }
    }
}

github.com

これにより比較対象のエラーと厳密に等しいか(含んでいるか)をチェックし、等しい場合は true を返します。

var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) {
    fmt.Println("not found")
}

Go1.26 で追加予定の errors.AsType

これまで errors.As では都度エラーを代入するためのポインタ変数を定義する必要がありましたが、Go1.26 では errors.AsType が追加されることで、エラーを代入するための変数を定義する必要がなくなります。呼び出し方からわかるように Go 1.18 で導入されたジェネリックを活用しています。

// ~ Go1.25
func FindMysqlErrorCode(err error) (bool, uint16) {
    var mysqlErr *mysql.MySQLError
    if errors.As(err, &mysqlErr) {
        return true, mysqlErr.Number
    }
    return false, 0
}

// Go1.26 ~
func FindMysqlErrorCode(err error) (bool, uint16) {
    if mysqlErr, ok := errors.AsType[*mysql.MySQLError](err); ok {
        return true, mysqlErr.Number
    }
    return false, 0
}

実装の方も基本はこれまでの errors.As と同様になっており、処理の流れはこのようになります。

func AsType[E error](err error) (E, bool) {
    if err == nil {
        var zero E
        return zero, false
    }
    var pe *E // lazily initialized
    return asType(err, &pe)
}

func asType[E error](err error, ppe **E) (_ E, _ bool) {
    for {
        // 現在のエラー値が、型パラメータ E の型に一致するかをチェック
        if e, ok := err.(E); ok {
            return e, true
        }
        // エラーが独自の As メソッドを実装している場合、それを呼び出してチェック
        if x, ok := err.(interface{ As(any) bool }); ok {
            if *ppe == nil {
                *ppe = new(E)
            }
            if x.As(*ppe) {
                return **ppe, true
            }
        }
        // エラーチェーンを辿る: Unwrap() メソッドでラップされた下位のエラーを取得
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            // 単一のエラーをラップしている場合: アンラップして次のループで再チェック
            err = x.Unwrap()
            if err == nil {
                return
            }
        case interface{ Unwrap() []error }:
            // 複数のエラーをラップしている場合: それぞれに対して再帰的にチェック
            for _, err := range x.Unwrap() {
                if err == nil {
                    continue
                }
                if x, ok := asType(err, ppe); ok {
                    return x, true
                }
            }
            return
        default:
            return
        }
    }
}

github.com

まとめ

error を普段から使うことは多かったですが、改めて実装の中身を除いてみると、開発者がエラーの抽出の仕方をあまり意識しなくても済むようになっていることがわかりました。

Go1.26 では errors.AsType が追加されることで、エラーを代入するための変数を定義する必要がなくなります。これによりエラーの抽出の仕方をより簡潔に、柔軟にすることができると思います。

今後とも Go の進化に食らいついていきながら、より良いエラーハンドリングを実現していきたいと思います。