Go ブログ

エラーは値です

ロブ・パイク
2015年1月12日

Go プログラマー、特にこの言語に慣れていない人々の間でよく議論されるのは、エラーの処理方法です。会話はしばしば、

if err != nil {
    return err
}

というシーケンスが何回出現するかという嘆きになります。最近、私たちが見つけることができたすべてのオープンソースプロジェクトをスキャンしたところ、このスニペットは1ページまたは2ページに1回しか出現せず、一部の人が信じているほど頻繁ではないことがわかりました。それでも、

if err != nil

を常にタイプしなければならないという認識が根強く残っている場合、何かが間違っているに違いなく、明白な対象は Go そのものです。

これは残念であり、誤解を招くものであり、簡単に修正できます。おそらく、Go に慣れていないプログラマーが「エラーをどのように処理するのか」と尋ね、このパターンを学び、そこで止まっているのでしょう。他の言語では、try-catch ブロックなどのメカニズムを使用してエラーを処理するかもしれません。したがって、プログラマーは、以前の言語で try-catch を使用していた場合は、Go では if err != nil と入力するだけだと考えます。時間の経過とともに、Go コードにはそのようなスニペットが多数集まり、結果としてぎこちなく感じられます。

この説明が当てはまるかどうかにかかわらず、これらの Go プログラマーはエラーに関する基本的な点を理解していないことは明らかです。エラーは値です。

値はプログラムできます。そして、エラーは値なので、エラーはプログラムできます。

もちろん、エラー値に関連する一般的なステートメントは、それが nil かどうかをテストすることですが、エラー値でできることは他にも無数にあり、それらの他のことのいくつかを適用することで、プログラムを改善し、すべてのエラーが機械的な if ステートメントでチェックされる場合に発生するボイラープレートの多くを排除できます。

bufio パッケージの Scanner 型からの簡単な例を次に示します。Scan メソッドは、基礎となる I/O を実行しますが、もちろんエラーが発生する可能性があります。それでも、Scan メソッドはエラーをまったく公開しません。代わりに、ブール値を返し、スキャンの最後に実行される別のメソッドがエラーが発生したかどうかを報告します。クライアントコードは次のようになります

scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // process token
}
if err := scanner.Err(); err != nil {
    // process the error
}

確かに、エラーの nil チェックはありますが、1回だけ出現して実行されます。代わりに、Scan メソッドは次のように定義することもできました

func (s *Scanner) Scan() (token []byte, error)

そして、サンプルのユーザーコードは(トークンの取得方法に応じて)次のようになります

scanner := bufio.NewScanner(input)
for {
    token, err := scanner.Scan()
    if err != nil {
        return err // or maybe break
    }
    // process token
}

これはそれほど違いはありませんが、1つの重要な違いがあります。このコードでは、クライアントはすべての反復でエラーをチェックする必要がありますが、実際の Scanner API では、エラー処理は主要な API 要素(トークンを反復処理すること)から抽象化されています。したがって、実際の API を使用すると、クライアントのコードはより自然に感じられます。完了するまでループし、エラーについて心配します。エラー処理は制御の流れを不明瞭にしません。

内部的には、もちろん、Scan が I/O エラーに遭遇するとすぐに、それを記録して false を返します。別のメソッド Err は、クライアントが要求したときにエラー値を報告します。これは些細なことですが、

if err != nil

をどこにでも配置したり、クライアントにすべてのトークンの後にエラーをチェックするように要求したりするのとは同じではありません。これはエラー値を使用したプログラミングです。単純なプログラミングですが、それでもプログラミングです。

設計がどのようなものであっても、プログラムがどのように公開されてもエラーをチェックすることが重要であることを強調する価値があります。ここでの議論は、エラーチェックを回避する方法ではなく、言語を使用してエラーを適切に処理する方法についてです。

反復的なエラーチェックコードのトピックは、2014年秋の東京での GoCon に参加したときに発生しました。Twitter で @jxck_ と名乗る熱心なゴーファーが、エラーチェックに関するおなじみの嘆きを繰り返しました。彼には、概略的に次のようなコードがありました

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

非常に反復的です。実際のコードはもっと長かったので、ヘルパー関数を使用してこれをリファクタリングするのは簡単ではありませんが、この理想化された形式では、エラー変数をクロージャする関数リテラルが役立ちます

var err error
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

このパターンはうまく機能しますが、書き込みを行う各関数でクロージャが必要です。別のヘルパー関数は、err 変数を呼び出し全体で維持する必要があるため、使用するのがより厄介です(試してみてください)。

上記の Scan メソッドからアイデアを借りることで、これをよりクリーンで、より一般的で、再利用可能にすることができます。議論の中でこのテクニックについて言及しましたが、@jxck_ はそれをどのように適用するかわかりませんでした。言語の壁によって多少妨げられた長いやりとりの後、彼のラップトップを借りてコードを入力して見せてよいかどうか尋ねました。

次のような errWriter と呼ばれるオブジェクトを定義しました

type errWriter struct {
    w   io.Writer
    err error
}

そして、それに1つのメソッド write を与えました。標準の Write シグネチャを持つ必要はなく、区別を強調するために partly 小文字になっています。write メソッドは、基礎となる WriterWrite メソッドを呼び出し、最初のエラーを将来の参照のために記録します

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

エラーが発生するとすぐに、write メソッドはノーオペレーションになりますが、エラー値は保存されます。

errWriter 型とその write メソッドが与えられると、上記のコードはリファクタリングできます

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

これは、クロージャの使用と比較してもクリーンであり、ページ上で行われている実際の書き込みシーケンスが見やすくなります。もう雑然としていません。エラー値(およびインターフェース)を使用したプログラミングにより、コードがよりきれいになりました。

同じパッケージ内の他のコードがこのアイデアに基づいて構築したり、errWriter を直接使用したりできる可能性があります。

また、errWriter が存在すれば、特に人工的でない例では、さらに多くのことができます。バイト数を累積できます。書き込みを単一のバッファに統合して、アトミックに送信できます。そして、もっとたくさん。

実際、このパターンは標準ライブラリによく見られます。archive/zip および net/http パッケージはこれを使用します。この議論により関連性が高いのは、bufio パッケージの Writer は実際には errWriter のアイデアの実装です。bufio.Writer.Write はエラーを返しますが、それは主に io.Writer インターフェースを尊重することです。bufio.WriterWrite メソッドは、上記の errWriter.write メソッドと同様に動作し、Flush がエラーを報告するため、この例は次のように記述できます

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
    return b.Flush()
}

このアプローチには、少なくとも一部のアプリケーションにとって、1つの重大な欠点があります。エラーが発生する前に処理がどれだけ完了したかを知る方法がありません。その情報が重要な場合は、よりきめ細かいアプローチが必要です。ただし、多くの場合、最後に all-or-nothing チェックを行うだけで十分です。

反復的なエラー処理コードを回避するための1つの手法だけを見てきました。errWriter または bufio.Writer の使用は、エラー処理を簡素化するための唯一の方法ではなく、このアプローチはすべての状況に適しているわけではないことに注意してください。ただし、重要な教訓は、エラーは値であり、Go プログラミング言語のすべての機能を使用して処理できることです。

言語を使用してエラー処理を簡素化します。

ただし、覚えておいてください。何をするにしても、常にエラーをチェックしてください!

最後に、彼が録画した短いビデオを含む、@jxck_ とのやりとりの全容については、彼のブログ にアクセスしてください。

次の記事:パッケージ名
前の記事:GothamGo:ビッグアップルのゴーファー
ブログインデックス