エラー処理とGo
Error handling and Go by Andrew Gerrand
はじめに
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)
}
// 開いた *File である f に対して何らかの処理を行う
error 型について知っているだけでもGoで多くのことができますが、この記事では error について
もう少し詳しく見て、Goにおけるエラー処理の良い習慣についていくつか議論します。
error 型
error 型はインターフェース型です。error 型の変数は、自分自身を文字列として表現できる
任意の値を表します。インターフェースの宣言は次のとおりです。
type error interface {
Error() string
}
error 型は、他の組み込み型と同様に、ユニバースブロック内で
事前宣言されています。
最もよく使われる error の実装は、errors パッケージの非公開の errorString 型です。
// errorStringはerrorの単純な実装です。
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
これらの値は errors.New 関数を使って生成できます。この関数は文字列を受け取り、それを
errors.errorString に変換して error 値として返します。
// Newは指定されたテキストとしてフォーマットされるerrorを返します。
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")
}
// 実装
}
Sqrt に負の引数を渡した呼び出し元は、非nilの error 値を受け取ります
(その具体的な実体は errors.errorString の値です)。呼び出し元は error の Error メソッドを
呼び出す、あるいは単に出力することで、エラー文字列(“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 はインターフェースなので、任意のデータ構造を
error 値として使い、呼び出し元がエラーの詳細を調べられるようにすることもできます。
たとえば、仮に呼び出し元が Sqrt に渡された不正な引数を取り出したいとしましょう。
errors.errorString を使う代わりに新しい error の実装を定義することで、これを実現できます。
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のバイト列を解析中に
構文エラーに遭遇したときに json.Decode 関数が返す SyntaxError 型を定義しています。
type SyntaxError struct {
msg string // エラーの説明
Offset int64 // Offsetバイト読み込んだ後にエラーが発生
}
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 メソッドだけですが、個々の error の実装は
追加のメソッドを持つことがあります。たとえば、net パッケージは通常の慣習に従って
error 型のエラーを返しますが、いくつかの error の実装は net.Error インターフェースで
定義された追加のメソッドを持っています。
package net
type Error interface {
error
Timeout() bool // タイムアウトによるエラーかどうか
Temporary() bool // 一時的なエラーかどうか
}
クライアントのコードは、型アサーションで 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 関数と viewTemplate の Execute メソッドが返すエラーを
処理しています。どちらの場合も、HTTPステータスコード500(「Internal Server Error」)とともに
単純なエラーメッセージをユーザーに提示します。これくらいなら扱いやすいコード量に見えますが、
HTTPハンドラをいくつか増やせば、たちまち同じようなエラー処理のコードのコピーだらけになってしまいます。
この繰り返しを減らすために、error の戻り値を含む独自の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 を
返す関数を理解できません。これを解決するには、appHandler に http.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) という式でレシーバを呼び出すことで、
その関数を実行しています。
appHandler は(http.HandlerFunc ではなく)http.Handler なので、viewRecord を
httpパッケージに登録するときは(HandleFunc の代わりに)Handle 関数を使います。
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 だけなので、これが正しいやり方です。)
そして appHandler の ServeHTTP メソッドが、正しいHTTPステータス Code とともに
appError の Message をユーザーに表示し、完全な Error を開発者コンソールにログ出力する
ようにします。
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テンプレートを与える。
- ユーザーが管理者の場合はスタックトレースをHTTPレスポンスに書き出し、デバッグをしやすくする。
- デバッグをしやすくするためにスタックトレースを保存する
appError用のコンストラクタ関数を書く。 appHandlerの内部でpanicから回復(recover)し、コンソールには「Critical」としてエラーを ログ出力しつつ、ユーザーには「重大なエラーが発生しました」と伝える。これはプログラミングの ミスによる理解不能なエラーメッセージをユーザーにさらさないための、ちょっとした工夫です。 詳しくは Defer, Panic, and Recover の記事を 参照してください。
まとめ
適切なエラー処理は、優れたソフトウェアに欠かせない要件です。この記事で説明したテクニックを 活用すれば、より信頼性が高く簡潔なGoのコードを書けるようになるはずです。
By Andrew Gerrand