はじめに
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
変数は、自身を文字列として説明できる任意の値を表します。インターフェースの宣言は次のとおりです:
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
関数を使用して、これらの値の1つを構築できます。この関数は、文字列を受け取り、それを 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
値です)。呼び出し元は、error
の Error
メソッドを呼び出すか、単にそれを印刷することでエラーストリング(「math: square root of…」)にアクセスできます。
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt パッケージは、error
値を Error() string
メソッドを呼び出すことでフォーマットします。
エラー実装は、コンテキストを要約する責任があります。os.Open
によって返されるエラーは「open /etc/passwd: permission denied」とフォーマットされ、単に「permission denied」ではありません。私たちの Sqrt
によって返されるエラーは、無効な引数に関する情報が欠けています。
その情報を追加するために、fmt
パッケージの Errorf
関数が便利です。この関数は、Printf
のルールに従って文字列をフォーマットし、error
として errors.New
によって作成された値を返します。
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 ブロブの構文エラーに遭遇したときに SyntaxError
型を返す json.Decode
関数を指定しています。
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
をテストし、一時的なネットワークエラーと永続的なエラーを区別できます。たとえば、ウェブクローラーは、一時的なエラーに遭遇したときにスリープして再試行し、それ以外の場合はあきらめるかもしれません。
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(「内部サーバーエラー」)でユーザーにシンプルなエラーメッセージを提示します。これは管理可能な量のコードに見えますが、さらにいくつかの 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
を返す関数を理解しません。これを修正するために、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
関数を呼び出し、返されたエラー(あれば)をユーザーに表示します。メソッドのレシーバー fn
が関数であることに注意してください。(Go はそれができます!)メソッドは、fn(w, r)
の式でレシーバーを呼び出すことで関数を呼び出します。
これで、http パッケージに viewRecord
を登録する際に、Handle
関数を使用します(HandleFunc
の代わりに)appHandler
は http.Handler
であり(http.HandlerFunc
ではありません)。
func init() {
http.Handle("/view", appHandler(viewRecord))
}
この基本的なエラーハンドリングインフラストラクチャが整ったので、ユーザーフレンドリーにすることができます。エラーストリングを表示するだけでなく、ユーザーに適切な HTTP ステータスコードを持つシンプルなエラーメッセージを提供し、デバッグ目的で App Engine 開発者コンソールに完全なエラーをログに記録する方が良いでしょう。
これを行うために、appError
構造体を作成し、error
と他のフィールドを含めます:
type appError struct {
Error error
Message string
Code int
}
次に、appHandler 型を変更して *appError
値を返すようにします:
type appHandler func(http.ResponseWriter, *http.Request) *appError
(エラーの具体的な型を error
ではなく返すことは通常間違いですが、Go FAQ で説明されている理由から、ここでは ServeHTTP
がその値を見てその内容を使用する唯一の場所であるため、正しいことです。)
appHandler
の ServeHTTP
メソッドを変更して、appError
の Message
をユーザーに正しい 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」としてコンソールにログに記録し、ユーザーには「重大なエラーが発生しました」と伝える。これは、プログラミングエラーによって引き起こされる理解不能なエラーメッセージをユーザーにさらさないための良い工夫です。詳細については、Defer, Panic, and Recover 記事を参照してください。
結論
適切なエラーハンドリングは、良いソフトウェアの必須要件です。この投稿で説明したテクニックを使用することで、より信頼性が高く簡潔な Go コードを書くことができるはずです。