The Go Blog

エラー処理と Go

Andrew Gerrand
2011年7月12日

はじめに

Go のコードを書いたことがあるなら、おそらく組み込みの error 型に出会ったことがあるでしょう。Go のコードは、異常な状態を示すために error 値を使用します。たとえば、os.Open 関数は、ファイルを開くことができない場合、nil 以外の error 値を返します。

func Open(name string) (file *File, err error)

次のコードは、os.Open を使用してファイルを開きます。エラーが発生した場合、log.Fatal を呼び出してエラーメッセージを出力し、停止します。

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

error 型についてこれだけの知識があれば、Go で多くのことを達成できますが、この記事では error を詳しく見て、Go でのエラー処理の良い習慣について説明します。

error 型

error 型はインターフェース型です。error 変数は、自身を文字列として記述できる任意の値を表します。インターフェースの宣言は次のとおりです。

type error interface {
    Error() string
}

error 型は、すべての組み込み型と同様に、事前宣言されています(ユニバースブロック内)。

最も一般的に使用される error 実装は、errors パッケージのエクスポートされていない errorString 型です。

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

これらの値の1つは、errors.New 関数を使用して構築できます。これは、文字列を受け取り、それを errors.errorString に変換し、error 値として返します。

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

errors.New の使用方法の例を示します。

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

Sqrt に負の引数を渡す呼び出し元は、nil 以外の error 値(具体的な表現は errors.errorString 値)を受け取ります。呼び出し元は、errorError メソッドを呼び出すか、単に出力することによって、エラー文字列(「math: square root of…」)にアクセスできます。

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

fmt パッケージは、Error() string メソッドを呼び出すことによって、error 値をフォーマットします。

コンテキストを要約するのはエラー実装の責任です。os.Open によって返されるエラーは、「permission denied」だけでなく、「open /etc/passwd: permission denied」としてフォーマットされます。私たちの Sqrt によって返されるエラーには、無効な引数に関する情報がありません。

その情報を追加するために便利な関数は、fmt パッケージの Errorf です。これは、Printf のルールに従って文字列をフォーマットし、errors.New によって作成された error として返します。

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

多くの場合、fmt.Errorf で十分ですが、error はインターフェースであるため、任意のデータ構造をエラー値として使用して、呼び出し元がエラーの詳細を検査できるようにすることができます。

たとえば、私たちの仮定の呼び出し元は、Sqrt に渡された無効な引数を回復したい場合があります。errors.errorString を使用する代わりに、新しいエラー実装を定義することで、これを有効にすることができます。

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

洗練された呼び出し元は、型アサーションを使用して NegativeSqrtError をチェックし、それを特別に処理できます。一方、fmt.Println または log.Fatal にエラーを渡すだけの呼び出し元は、動作に変化が見られません。

別の例として、json パッケージは、json.Decode 関数が JSON blob の構文解析で構文エラーを検出したときに返す SyntaxError 型を指定します。

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }

Offset フィールドはエラーのデフォルトのフォーマットには表示されませんが、呼び出し元はそれを使用してファイルと行情報をエラーメッセージに追加できます。

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

(これは、Camlistore プロジェクトの実際のコードを少し簡略化したバージョンです。)

error インターフェースでは、Error メソッドのみが必要です。特定のエラー実装には、追加のメソッドがある場合があります。たとえば、net パッケージは、通常の規則に従って error 型のエラーを返しますが、一部のエラー実装には、net.Error インターフェースで定義された追加のメソッドがあります。

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

クライアントコードは、型アサーションを使用して net.Error をテストし、一時的なネットワークエラーと永続的なネットワークエラーを区別できます。たとえば、Webクローラーは、一時的なエラーが発生した場合にスリープして再試行し、そうでない場合はあきらめることがあります。

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

反復的なエラー処理の簡略化

Go では、エラー処理は重要です。言語の設計と規則により、エラーが発生した場所でエラーを明示的にチェックすることが推奨されます(他の言語の例外をスローし、場合によってはキャッチするという規則とは異なります)。場合によっては、これにより Go コードが冗長になりますが、幸いなことに、反復的なエラー処理を最小限に抑えるために使用できる手法がいくつかあります。

データストアからレコードを取得し、テンプレートでフォーマットする HTTP ハンドラーを持つ App Engine アプリケーションを考えてみましょう。

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

この関数は、datastore.Get 関数と viewTemplateExecute メソッドによって返されるエラーを処理します。どちらの場合も、HTTP ステータスコード 500(「内部サーバーエラー」)を使用して、ユーザーに簡単なエラーメッセージを表示します。これは管理しやすい量のコードのように見えますが、HTTP ハンドラーをさらに追加すると、すぐに同じエラー処理コードのコピーが多数作成されます。

繰り返しを減らすために、error 戻り値を含む独自の HTTP appHandler 型を定義できます。

type appHandler func(http.ResponseWriter, *http.Request) error

次に、viewRecord 関数を変更してエラーを返すことができます。

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

これは元のバージョンよりも単純ですが、http パッケージは error を返す関数を理解していません。これを修正するには、appHandlerhttp.Handler インターフェースの ServeHTTP メソッドを実装できます。

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

ServeHTTP メソッドは appHandler 関数を呼び出し、返されたエラー(ある場合)をユーザーに表示します。メソッドのレシーバーである fn は関数であることに注意してください。(Go はそれを実行できます!)メソッドは、式 fn(w, r) でレシーバーを呼び出すことによって関数を呼び出します。

これで、viewRecord を http パッケージに登録するときに、appHandlerhttp.Handlerhttp.HandlerFunc ではない)であるため、Handle 関数(HandleFunc ではなく)を使用します。

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

この基本的なエラー処理インフラストラクチャが整ったので、よりユーザーフレンドリーにすることができます。エラー文字列を表示するだけでなく、適切な HTTP ステータスコードでユーザーに簡単なエラーメッセージを表示し、デバッグのために App Engine デベロッパーコンソールに完全なエラーを記録する方が適切です。

これを行うために、error と他のいくつかのフィールドを含む appError 構造体を作成します。

type appError struct {
    Error   error
    Message string
    Code    int
}

次に、appHandler 型を変更して *appError 値を返すようにします。

type appHandler func(http.ResponseWriter, *http.Request) *appError

Go FAQ で説明されている理由により、error ではなくエラーの具体的な型を渡すのは通常は間違いですが、ServeHTTP が値を見てその内容を使用する唯一の場所であるため、ここでは正しいことです。)

そして、appHandlerServeHTTP メソッドが、正しい HTTP ステータス Code でユーザーに appErrorMessage を表示し、デベロッパーコンソールに完全な Error を記録するようにします。

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

最後に、viewRecord を新しい関数シグネチャに更新し、エラーが発生したときにさらにコンテキストを返すようにします。

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

このバージョンの viewRecord は元のバージョンと同じ長さですが、これらの行はそれぞれ特定の意味を持ち、よりフレンドリーなユーザーエクスペリエンスを提供しています。

それで終わりではありません。アプリケーションのエラー処理をさらに改善できます。いくつかのアイデアがあります。

  • エラーハンドラーにきれいな HTML テンプレートを提供する、

  • ユーザーが管理者の場合にスタックトレースを HTTP レスポンスに書き込むことによって、デバッグを容易にする、

  • デバッグを容易にするためにスタックトレースを格納する appError のコンストラクター関数を記述する、

  • appHandler 内のパニックから回復し、エラーをコンソールに「クリティカル」として記録し、ユーザーに「重大なエラーが発生しました」と伝える。これは、プログラミングエラーによって引き起こされる不可解なエラーメッセージがユーザーに公開されるのを防ぐための優れた方法です。詳細については、Defer、Panic、および Recover の記事を参照してください。

結論

適切なエラー処理は、優れたソフトウェアの不可欠な要件です。この記事で説明されている手法を採用することで、より信頼性が高く簡潔な Go コードを記述できるはずです。

次の記事:Go for App Engine が正式に利用可能になりました
前の記事:Go の第一級関数
ブログインデックス