はじめに

Go コードを書いたことがあるなら、error 型に出会ったことがあるでしょう。Go コードは、異常な状態を示すために error 値を使用します。たとえば、os.Open 関数は、ファイルを開くことに失敗した場合に非 nil の error 値を返します。

  1. func Open(name string) (file *File, err error)

次のコードは、os.Open を使用してファイルを開きます。エラーが発生した場合、log.Fatal を呼び出してエラーメッセージを表示し、処理を停止します。

  1. f, err := os.Open("filename.ext")
  2. if err != nil {
  3. log.Fatal(err)
  4. }
  5. // do something with the open *File f

error 型についてこれだけ知っていれば、Go で多くのことができますが、この記事では error を詳しく見て、Go におけるエラーハンドリングの良い実践について議論します。

エラー型

error 型はインターフェース型です。error 変数は、自身を文字列として説明できる任意の値を表します。インターフェースの宣言は次のとおりです:

  1. type error interface {
  2. Error() string
  3. }

error 型は、すべての組み込み型と同様に、事前宣言された ユニバースブロック にあります。

最も一般的に使用される error 実装は、errors パッケージの非公開 errorString 型です。

  1. // errorString is a trivial implementation of error.
  2. type errorString struct {
  3. s string
  4. }
  5. func (e *errorString) Error() string {
  6. return e.s
  7. }

errors.New 関数を使用して、これらの値の1つを構築できます。この関数は、文字列を受け取り、それを errors.errorString に変換し、error 値として返します。

  1. // New returns an error that formats as the given text.
  2. func New(text string) error {
  3. return &errorString{text}
  4. }

errors.New を使用する方法は次のとおりです:

  1. func Sqrt(f float64) (float64, error) {
  2. if f < 0 {
  3. return 0, errors.New("math: square root of negative number")
  4. }
  5. // implementation
  6. }

Sqrt に負の引数を渡す呼び出し元は、非 nil の error 値を受け取ります(その具体的な表現は errors.errorString 値です)。呼び出し元は、errorError メソッドを呼び出すか、単にそれを印刷することでエラーストリング(「math: square root of…」)にアクセスできます。

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

fmt パッケージは、error 値を Error() string メソッドを呼び出すことでフォーマットします。

エラー実装は、コンテキストを要約する責任があります。os.Open によって返されるエラーは「open /etc/passwd: permission denied」とフォーマットされ、単に「permission denied」ではありません。私たちの Sqrt によって返されるエラーは、無効な引数に関する情報が欠けています。

その情報を追加するために、fmt パッケージの Errorf 関数が便利です。この関数は、Printf のルールに従って文字列をフォーマットし、error として errors.New によって作成された値を返します。

  1. if f < 0 {
  2. return 0, fmt.Errorf("math: square root of negative number %g", f)
  3. }

多くの場合、fmt.Errorf は十分ですが、error がインターフェースであるため、呼び出し元がエラーの詳細を調査できるように、任意のデータ構造をエラー値として使用できます。

たとえば、仮想の呼び出し元は、Sqrt に渡された無効な引数を回復したいかもしれません。errors.errorString を使用する代わりに、新しいエラー実装を定義することでそれを可能にできます:

  1. type NegativeSqrtError float64
  2. func (f NegativeSqrtError) Error() string {
  3. return fmt.Sprintf("math: square root of negative number %g", float64(f))
  4. }

洗練された呼び出し元は、型アサーション を使用して NegativeSqrtError をチェックし、特別に処理できますが、fmt.Println または log.Fatal にエラーを渡すだけの呼び出し元は、動作に変化はありません。

別の例として、json パッケージは、JSON ブロブの構文エラーに遭遇したときに SyntaxError 型を返す json.Decode 関数を指定しています。

  1. type SyntaxError struct {
  2. msg string // description of error
  3. Offset int64 // error occurred after reading Offset bytes
  4. }
  5. func (e *SyntaxError) Error() string { return e.msg }

Offset フィールドは、エラーのデフォルトフォーマットには表示されませんが、呼び出し元はそれを使用してエラーメッセージにファイルと行の情報を追加できます:

  1. if err := dec.Decode(&val); err != nil {
  2. if serr, ok := err.(*json.SyntaxError); ok {
  3. line, col := findLine(f, serr.Offset)
  4. return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
  5. }
  6. return err
  7. }

(これは、Camlistore プロジェクトからの実際のコードの少し簡略化されたバージョンです。)

error インターフェースは、Error メソッドのみを要求します。特定のエラー実装には追加のメソッドがある場合があります。たとえば、net パッケージは、通常の慣例に従って error 型のエラーを返しますが、一部のエラー実装には net.Error インターフェースによって定義された追加のメソッドがあります:

  1. package net
  2. type Error interface {
  3. error
  4. Timeout() bool // Is the error a timeout?
  5. Temporary() bool // Is the error temporary?
  6. }

クライアントコードは、型アサーションを使用して net.Error をテストし、一時的なネットワークエラーと永続的なエラーを区別できます。たとえば、ウェブクローラーは、一時的なエラーに遭遇したときにスリープして再試行し、それ以外の場合はあきらめるかもしれません。

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

繰り返しのエラーハンドリングの簡素化

Go では、エラーハンドリングが重要です。この言語の設計と慣習は、他の言語の例外をスローして時々キャッチするという慣習とは異なり、エラーが発生した場所で明示的にエラーをチェックすることを奨励します。このため、Go コードは冗長になることがありますが、幸いなことに、繰り返しのエラーハンドリングを最小限に抑えるために使用できるいくつかのテクニックがあります。

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

  1. func init() {
  2. http.HandleFunc("/view", viewRecord)
  3. }
  4. func viewRecord(w http.ResponseWriter, r *http.Request) {
  5. c := appengine.NewContext(r)
  6. key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
  7. record := new(Record)
  8. if err := datastore.Get(c, key, record); err != nil {
  9. http.Error(w, err.Error(), 500)
  10. return
  11. }
  12. if err := viewTemplate.Execute(w, record); err != nil {
  13. http.Error(w, err.Error(), 500)
  14. }
  15. }

この関数は、datastore.Get 関数と viewTemplateExecute メソッドによって返されるエラーを処理します。どちらの場合も、HTTP ステータスコード 500(「内部サーバーエラー」)でユーザーにシンプルなエラーメッセージを提示します。これは管理可能な量のコードに見えますが、さらにいくつかの HTTP ハンドラーを追加すると、すぐに同一のエラーハンドリングコードの多くのコピーができてしまいます。

繰り返しを減らすために、appHandler 型を定義して error 戻り値を含めることができます:

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

次に、viewRecord 関数を変更してエラーを返すようにします:

  1. func viewRecord(w http.ResponseWriter, r *http.Request) error {
  2. c := appengine.NewContext(r)
  3. key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
  4. record := new(Record)
  5. if err := datastore.Get(c, key, record); err != nil {
  6. return err
  7. }
  8. return viewTemplate.Execute(w, record)
  9. }

これは元のバージョンよりも簡単ですが、http パッケージは error を返す関数を理解しません。これを修正するために、http.Handler インターフェースの ServeHTTP メソッドを appHandler に実装できます:

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

ServeHTTP メソッドは appHandler 関数を呼び出し、返されたエラー(あれば)をユーザーに表示します。メソッドのレシーバー fn が関数であることに注意してください。(Go はそれができます!)メソッドは、fn(w, r) の式でレシーバーを呼び出すことで関数を呼び出します。

これで、http パッケージに viewRecord を登録する際に、Handle 関数を使用します(HandleFunc の代わりに)appHandlerhttp.Handler であり(http.HandlerFunc ではありません)。

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

この基本的なエラーハンドリングインフラストラクチャが整ったので、ユーザーフレンドリーにすることができます。エラーストリングを表示するだけでなく、ユーザーに適切な HTTP ステータスコードを持つシンプルなエラーメッセージを提供し、デバッグ目的で App Engine 開発者コンソールに完全なエラーをログに記録する方が良いでしょう。

これを行うために、appError 構造体を作成し、error と他のフィールドを含めます:

  1. type appError struct {
  2. Error error
  3. Message string
  4. Code int
  5. }

次に、appHandler 型を変更して *appError 値を返すようにします:

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

(エラーの具体的な型を error ではなく返すことは通常間違いですが、Go FAQ で説明されている理由から、ここでは ServeHTTP がその値を見てその内容を使用する唯一の場所であるため、正しいことです。)

appHandlerServeHTTP メソッドを変更して、appErrorMessage をユーザーに正しい HTTP ステータス Code で表示し、完全な Error を開発者コンソールにログに記録します:

  1. func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  2. if e := fn(w, r); e != nil { // e is *appError, not os.Error.
  3. c := appengine.NewContext(r)
  4. c.Errorf("%v", e.Error)
  5. http.Error(w, e.Message, e.Code)
  6. }
  7. }

最後に、viewRecord を新しい関数シグネチャに更新し、エラーに遭遇したときにより多くのコンテキストを返すようにします:

  1. func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
  2. c := appengine.NewContext(r)
  3. key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
  4. record := new(Record)
  5. if err := datastore.Get(c, key, record); err != nil {
  6. return &appError{err, "Record not found", 404}
  7. }
  8. if err := viewTemplate.Execute(w, record); err != nil {
  9. return &appError{err, "Can't display record", 500}
  10. }
  11. return nil
  12. }

このバージョンの viewRecord は元の長さと同じですが、今では各行が特定の意味を持ち、よりフレンドリーなユーザーエクスペリエンスを提供しています。

ここで終わりではありません。アプリケーションのエラーハンドリングをさらに改善できます。いくつかのアイデア:

  • エラーハンドラーに美しい HTML テンプレートを提供する、
  • ユーザーが管理者であるときにスタックトレースを HTTP 応答に書き込むことでデバッグを容易にする、
  • より簡単なデバッグのためにスタックトレースを保存する appError のコンストラクタ関数を書く、
  • appHandler 内でパニックから回復し、エラーを「Critical」としてコンソールにログに記録し、ユーザーには「重大なエラーが発生しました」と伝える。これは、プログラミングエラーによって引き起こされる理解不能なエラーメッセージをユーザーにさらさないための良い工夫です。詳細については、Defer, Panic, and Recover 記事を参照してください。

結論

適切なエラーハンドリングは、良いソフトウェアの必須要件です。この投稿で説明したテクニックを使用することで、より信頼性が高く簡潔な Go コードを書くことができるはずです。