寝ても覚めてもこんぴうた

プログラム書いたり、ネットワーク設計したり、サーバ構築したり、車いじったり、ゲームしたり。そんなひとにわたしはなりたい。 投げ銭は kyash_id : chidakiyo マデ

GoのWebサービスのエラーハンドリングについてGoのblogに書かれていた内容を日本語でまとめてみた

GoでWebサービスを実装する際、Errorのログをどこで出すのかーという話、 少々古い記事になるけどGoのBlogに書かれていたのでサラッと日本語にしてみた。

GAEの実装を例にされているのと、2011年頃の記事なので、いろいろモダンなGo技術(?)を使うといい感じにできる部分もあるけど一旦そのまま。

内容としては https://go.dev/blog/error-handling-and-go#simplifying-repetitive-error-handling に書かれているものです。

何度もやるエラーハンドリングを簡素化する

Goはエラーハンドリングが重要。言語のデザインや習慣として発生した場所でエラーを明示的にチェックすることが推奨される。
(スローして場合によってキャッチする他の言語規則とは異なる)

場合によっては冗長になるが、度重なるエラー処理を最小限に抑える手法がある。
データストアからレコードを取得するAppEngineのコードで考えてみましょう。

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/viewTemplate.Executeから返されるエラーを処理する。
いずれもHTTP 500のメッセージが表示される。

これらは扱いやすいコード量に見えるが、更にHTTPハンドラを追加するとエラーハンドリングコードがたくさん作られる。

繰り返しを減らすため、エラーの戻り値を含む appHandler 型を定義する

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

そして、viewRecordがerrorを返すように変更する

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を返す関数を利用できずないため、
http.HandlerインターフェースのServeHTTPをappHandlerに実装します

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関数を呼び出し、返されたエラーをユーザに表示します。

appHandlerはhttp.Handlerなので、viewRecordをhttpパッケージに登録する際にはhttp.Handle関数を利用します。

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

このようなベーシックなエラーハンドリング基盤を配置することでユーザフレンドリーにすることができる。
エラーを表示するだけではなく、適切なHTTPステータスを含むシンプルなエラーメッセージを表示し、
デバッグのために完全なログを出力することもできる。

それを行うためには、エラーとその他のフィールドを含むappErrorを作成する

type appError struct {
    Error   error
    Message string
    Code    int
}

そして、 *appError を返すようにappHandler型を変更する

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

( こちら に書かれているように、errorではなくerrorのコンクリートタイプを返すことは通常は間違いですが、ServeHTTPは値を見てその内容を使用する唯一の場所になるためここで行うことは正しいといえます)

appHandlerのServeHTTPメソッドがappErrorのメッセージをとエラーコードをログにも落とせるようにします。

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // eは *appError、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テンプレートを与える
  • ユーザが管理者であればスタックトレースをレスポンスに含めるなどが可能(デバッグが容易になる)
  • デバッグを容易にするスタックトレースを持つappErrorコンストラクタを記述する
  • appHandler内のパニックからリカバーし、クリティカルのログを出力するし、重大なエラーが発生した旨をユーザに伝える。プログラミングエラーによって引き起こされる得体のしれないメッセージからユーザを守る