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
}

これらの値は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値のError() stringメソッドを呼び出してフォーマットします。

コンテキストを要約するのはエラー実装の責任です。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ブロブのパース中に構文エラーに遭遇したときに返す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コードが冗長になりますが、幸いなことに、反復的なエラー処理を最小限に抑えるために使用できるいくつかのテクニックがあります。

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

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(「Internal Server Error」)とともに、ユーザーに簡単なエラーメッセージを表示します。これは管理可能なコード量に見えますが、さらにHTTPハンドラーを追加すると、すぐに同じエラー処理コードが大量に複製されることになります。

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

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)という式でレシーバーを呼び出すことで関数を呼び出します。

これで、httpパッケージにviewRecordを登録する際、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メソッドが、appErrorMessageを適切なHTTPステータスCodeでユーザーに表示し、完全な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内のパニックから回復し、エラーを「Critical」としてコンソールにログ記録し、ユーザーには「a serious error has occurred.」と伝える。これは、プログラミングエラーによって引き起こされる解読不能なエラーメッセージをユーザーにさらさないための良い配慮です。詳細については、Defer, Panic, and Recoverの記事を参照してください。

まとめ

適切なエラー処理は、優れたソフトウェアの必須要件です。この記事で説明されているテクニックを使用することで、より信頼性が高く簡潔なGoコードを書くことができるはずです。

次の記事: Go for App Engineが一般公開されました
前の記事: Goにおけるファーストクラス関数
ブログインデックス