2015年05月01日

Go ではエラーを文字列比較する?という話について

はてなブックマークに登録

Go で関数の戻り値のエラーを判別するときに、エラーメッセージの文字列をチェックするコードが存在します。 ()

これは、 Go が言語設計としてエラー処理が貧弱だったり、標準ライブラリがエラー処理を軽視しているからでしょうか? 言語設計や標準ライブラリのAPIの設計をみて行きましょう。

TL;DR

  • 言語設計としては、Java的例外機構と同等以上の(文字列比較によらない)エラー検査が可能
  • ただし Go のエラーに関する哲学により、公開されていないエラーが多い
  • 実際にエラーを文字列比較されている実例についての解説

Go のエラー検査方法

Java の例外機構では、例外をキャッチするために専用の構文が用意されており、型によりマッチングすることができます。 これはクラスのツリー構造を利用してサブクラスをまとめて分岐することもできます。 一方で、同じクラスでも値によりエラー処理が異なる場合には、 try をネストしないといけないケースが有ります。

作為的な例ですが、例外が Errno クラスで、なおかつその errno が EINTR の場合はリトライし、それ以外のケースでは終了するコードはこうなります。

    try {
        while (true) {
            try {
                // ...
            } catch (Errno e) {
                if (e.getErrno() == Errno.EINTR) {
                    continue;
                }
                throw e;
            }
        }
    } catch (Exception e) {
        System.err.println(e.getMessage());
        System.exit(1);
    }

Go の場合、一般的にエラーは error というインターフェイスによって返されます。 Go にはエラー検査専用の構文はありませんが、インターフェイス一般に使える conversion 構文が Java の catch と同等以上の機能を持っているので、問題になりません。conversion には次の2種類の書き方があり、場面によって使い分けます。

    // シンプルな conversion.主に if 文と組み合わせて使う。型以外の条件式を if 文に書ける。
    if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
        continue
    }

    // Type switch. 複数の型の振り分けを見やすく書ける
    switch e := err.(type) {
    case net.Error:
        if e.Temporary() {
            continue
        }
    // ...
    }

実際には複数のエラー型を扱うケースはあまりないのと、 io.EOF の様に型ではなく定数と比較するエラーがあるので、シンプルな conversion と if 文の組み合わせがよく使われます。

conversion は具体型だけでなく別の interface への変換も可能なので、 interface を抽象型として使うことで、 Java の例外階層と同じように複数の型のエラーをまとめて扱うことができます。例えば、 syscall.Errnonet.Error インターフェイスを実装しているので、先ほどのコードは次のようにより一般的に書くことができます。

    defer r.Close()
    for {
        data, err := r.Read()
        // 最後のデータを読み込みつつEOFが返る事があるので、エラーチェックよりdataチェックが先
        if len(data) > 0 {
            received <- data
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
                continue
            }
            log.Error(err)
            break
        }
    }

Go のエラーに関する哲学

Go の作者の一人である Dave Cheney 氏のBlog に、Goのエラーに対する重要な哲学が紹介されています。

In the majority of cases, error values returned from functions should be opaque to the caller. That is to say, a test that error is nil indicates if the call succeeded or failed, and that’s all there is to it.

A small number of cases, generally revolving around interactions with the world outside your process, like network activity, require that the caller investigate the nature of the error to decide if it is reasonable to retry the operation.

訳:

ほとんどのケースにおいて、関数が返した error 値は呼び出し元にとって不透明であるべきです。 error が nil かどうかチェックすることで呼び出しが成功したかどうかを知ることができ、そしてそれ以上のことはできません。

少ないケースにおいて、主にネットワークなどプロセス外の世界とやりとりする場合に、呼び出し側がエラーの種別を調べて、操作をリトライするべきかどうかを決める必要があります。

この "error ... should be opaque to the caller" の部分は、後で紹介する issue でも Go の philosophy だと言及されています。 この Blog 記事は改めて翻訳したいと思いますが、要約すると次のような方針になっているようです。

  • 可能な限り個別の具体的なエラー型やエラー定数はパッケージ外に公開しない
  • エラー処理が必要な場合は、代わりに Timeout() boolTemporary() bool といった目的に応じたメソッドを定義する

この設計思想のために、特定のエラーを判別するためにエラーメッセージを文字列マッチするしか方法がないケースがあります。

Go で文字列でエラーを判別している実例

一番よく文字列比較されてしまっているエラーが、たぶん net.errClosing です。これは次のように非公開で定義されているエラー定数です。

	errClosing                = errors.New("use of closed network connection")

このエラーは、 Close された ListenerConnAccept(), Read(), Write(), Close() などのメソッドを呼び出した時に返されます。

このエラーは net パッケージ自身も戻り値として返す場合とテスト中でしか使われておらず、 if err == errClosing のような使い方はされていません。 このエラーを受け取った呼び出し側はができる回復処理は無いので、 "should be opaque" なのだと思います。

一方で、 Go はネットワークのブロッキングIOをランタイムで隠蔽してノンブロッキングIOのAPIを提供している言語なので、 Listener.Accept()Conn.Read() は呼び出し側をブロックします。これらのメソッドを利用するときは独立した goroutine を使います。そして Accept()Read() している goroutine を止める手段は、その ListenerConnClose() することです。一般的な設計のネットワークプログラムで、正常系として、 errClosing が返るのです。

errClosing のような回復不能なエラーが発生した時、一般的な後処理は、Connであればそのセッションを捨てる、Listener であればプログラム自体を止めることですが、その際にエラーをログに書くべきです。しかし、 errClosing のような正常系で発生するエラーをエラーログに書いていたら、より重要なエラーログが埋もれてしまいます。 Close() したあとの Accept()Read() のエラーを無視することもできますが、 Close() するのは別の goroutine なので面倒ですし、 errClosing 以外のエラーまで無視してしまう可能性がある気持ち悪さも残ります。

この errClosing を公開してほしいという Issue golang/go#4373 があり、エラーログに書きたくないというユースケースが投稿された結果、 Won't fix ラベルが解除されました。まだ修正されると決まったわけではありませんが、設計者の想定(回復できないエラーを個別に見せる必要がない)と実際のユースケース(このエラーはログに書きたくない)のミスマッチの一番の例だと思います。

go#4373 と関連する Issue として、golang/go#9405 もあります。 こちらは、 http クライアントをタイムアウト付きで利用した際に (Timeout() bool を実装していない) errClosing がそのまま返されてしまうという問題で、開発中の Go 1.5 では修正済みの問題なのですが、現在リリースされている Go 1.4 までではタイムアウトしたかどうかを判定するために errClosing の文字列比較が必要になってしまっています。

このように、決して多くは無いものの、設計者の想定していないユースケースやバグによりエラーを文字列比較で判定しているケースが残念ながら存在します。 ただし、あくまでもこれらは例外であって、 Go のエラー処理が弱いからエラーを文字列比較しないといけないといったようなことはなく、 Go の哲学は現実的にうまく機能していると思います。


@methane

songofacandy at 14:45│Comments(0)TrackBack(0)golang 

トラックバックURL

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント