11.1 エラー処理

Go言語の主な設計方針は:簡潔、明瞭です。簡潔とは文法がCと似ていて、かなり簡単であるということです。明瞭とはいかなるキーワードも分かりやすいということを指しています。どのような隠された意味も含まず、エラー処理の設計でもこの思想は一貫しています。C言語では-1またはNULLをといった情報を返すことでエラーを表していることをご存知だと思います。しかしユーザからすると、対応するAPIの説明ドキュメントを見なければ、この戻り値がいったいどういう意味を表しているのかそもそもよくわかりません。例えば:0を返すと成功するのか失敗するのかといったことです。Goではerrorと呼ばれる型を定義することで、エラーを表しています。使用する際は、返されるerror変数とnilを比較することで操作が成功したか判断します。例えば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)
}

os.Open関数に似て、標準パッケージのすべてのエラーを発生させうるAPIはどれもerror変数を返すので、簡単にエラー処理を行うことができます。この節ではerror型の設計を詳細にご紹介し、Webアプリケーションの開発におけるよりよいerror処理について論じます。

Error型

error型はインターフェース型の一つです。定義は:

type error interface {
    Error() string
}

errorはビルトインのインターフェース型のひとつです。/builtin/パッケージの下に対応する定義を探すことができます。多くの内部パッケージにおいて使用される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を通して文字列を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をコールした際に負の数を渡し、non-nilなerrorオブジェクトを取得しています。このオブジェクトをnilと比較し、結果としてtrueを得ることでfmt.Println(fmtパッケージはerrorを処理する際Errorメソッドをコールします)がコールされ、エラーを出力します。下のコールのコード例をご覧ください:

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

カスタム定義のError

上のご紹介で我々はerrorが単なるinterfaceだとわかりました。そのため自分のパッケージを実装する際、このインターフェースを実装する構造体を定義することで、自分のエラー定義を実装することができます。Jsonパッケージの例をご覧ください:

type SyntaxError struct {
    msg    string // エラーの説明
    Offset int64  // エラーが発生した場所
}

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

OffsetフィールドはErrorをコールする時には出力されません。しかし型アサーションを通してエラーの型を取得することができますので、対応するエラー情報を出力することができます。下の例をご覧ください:

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
}

関数がカスタム定義のエラーを返す時は戻り値にerror型を設定するようおすすめします。特に前もって宣言しておく必要のないエラー型の変数には注意が必要です。例えば:

func Decode() *SyntaxError { // エラー、上のレイヤでコールした利用者によるerr!=nilの判断が永遠にtrueになります。
    var err *SyntaxError     // 予めエラー変数を宣言します
    if エラー条件 {
        err = &SyntaxError{}
    }
    return err               // エラー、errは永久にnilではない値と等しくなり、上のレイヤでコールした利用者によるerr!=nilの判断が常にtrueとなります
}

原因はこちら http://golang.org/doc/faq#nil_error

上の例による簡単なデモでError型をどのようにして自分で定義するかお見せしました。しかしもしもっと複雑なエラー処理を必要とした場合はどうすればよいのでしょうか?netパッケージが採用している方法を参考にします:

package net

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

コールされる場所ではerrがnet.Errorかどうか型アサーションにより判断することで、エラーの処理を細分化しています。例えば下の例ではもしネットワークに一時的にエラーが発生している場合にsleep 1秒を行なってリトライを行います:

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

エラー処理

Goはエラー処理においてCに似た戻り値を検査する方法を採用しており、その他の大多数の主流な言語が採用する例外方式ではありません。これはコードを書く上でとても大きな欠点の一つです。エラー処理コードの冗長さは、このような状況において我々が検査関数を再利用することによって似たようなコードを減らします。

このコード例をご確認ください:

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)
    }
}

上の例ではデータの取得とテンプレートの展開をコールする時にエラーの検査を行なっています。エラーが発生した場合は共通の処理関数であるhttp.Errorをコールし、クライアントに500エラーを返して対応するエラーデータを表示します。しかしHnadleFuncが追加されるに従って、このようなエラー処理ロジックのコードが多くなってきてしまいます。実は我々は自分で定義したルータを使ってコードを短縮させることができます(実装のやり方は第三章のHTTPの詳しい説明をご参考下さい)。

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

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

上では自分でルータを定義しています。以下の方法によって関数を登録することができます:

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

/viewをリクエストした時、我々のロジック処理は以下のようなコードに変わります。はじめの実装方法と比べるとだいぶ簡単になっています。

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)
}

上の例でエラー処理を行った場合すべてのエラーはユーザに500エラーとして返ります。その後対応するエラーコードを出力します。このエラー情報の定義はもっとユーザビリティを上げることもできます。デバッグする際に問題の箇所を確定するのに便利です。自分で返るエラー型を定義することもできます:

type appError struct {
    Error   error
    Message string
    Code    int
}

自分で定義したルータを以下のようなメソッドに変更します:

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

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)
    }
}

このように自分で定義したエラーを修正した後、ロジックは以下のようなメソッドに修正できます:

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
}

上で示したとおり、viewにアクセスした際異なる状況によって異なるエラーコードとエラー情報を取得することができます。これははじめのバージョンに比べてコード量にさほど変化はありませんが、これが表示するエラーはよりわかりやすくなっています。提示されるエラー情報のユーザビリティが高められ、拡張性もはじめのものに比べてよくなっています。

まとめ

プログラムの設計において障害の許容は重要な仕事の一部です。Goではエラー処理によってこれを実現します。errorはひとつのインターフェースに過ぎませんが、多くに変化させることができます。自分の需要に合わせて異なる処理を実装することができます。最後にご紹介したエラー処理の方法で、皆様によりよいWebエラーの処理の方法を設計するにあたってご助力になれば幸いです。

results matching ""

    No results matching ""