The Go Blog

エラーは値である

ロブ・パイク
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チェックはありますが、それは一度だけ現れて実行されます。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に参加したときに持ち上がりました。@jxck_というTwitterユーザーの熱心なGopherが、エラーチェックに関するおなじみの不満を繰り返しました。彼が持っていたコードは概略的に次のようでした。

_, 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シグネチャを持つ必要はなく、その区別を強調するために一部小文字になっています。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つの重大な欠点があります。それは、エラーが発生する前に処理がどれだけ完了したかを知る方法がないことです。その情報が重要である場合、より細粒度のアプローチが必要です。しかし、多くの場合、最後に一括してチェックするだけで十分です。

反復的なエラー処理コードを避けるための1つのテクニックを見てきました。errWriterbufio.Writerの使用がエラー処理を簡素化する唯一の方法ではないこと、そしてこのアプローチがすべての状況に適しているわけではないことを心に留めておいてください。しかし、重要な教訓は、エラーは値であり、それらを処理するためにGoプログラミング言語のすべての力を利用できるということです。

エラー処理を簡素化するために言語を使用しましょう。

しかし覚えておいてください:何をしても、常にエラーをチェックしてください!

最後に、@jxck_とのやり取りの全容については、彼が録画した短いビデオを含め、彼のブログをご覧ください。

次の記事: パッケージ名
前の記事: GothamGo: 大都会のGopherたち
ブログインデックス