Go Conference 2014 Autumnレポート

鵜飼文敏氏「Goに入ってはGoに従え」可読性のあるコードにするために~Go Conference 2014 Autumn基調講演2人目

この記事を読むのに必要な時間:およそ 10 分

エラー

Must-を使った初期化

Go言語では,戻り値の最後にエラーを取ることが一般的で,そのエラーは必ず受け取る必要があります。しかし,_で受け取ることで,エラー処理を行わなくて済むようになります。同氏が行ったレビューにおいても,このようなコードを見かけることがあるそうで,エラーは必ずチェックする必要があると指摘していました。たとえば,正規表現をコンパイルするregexp.Compile関数も第2戻り値にエラーを返しますが,以下のように_で受け取ることでエラー処理を回避できます。

var whitespaceRegex, _ = regexp.Compile("\\s+")

同氏は,上のコードは以下のコードのように修正すべきだと述べていました。

var whitespaceRegex = regexp.MustCompile(`\s+`)

同氏は,パッケージ変数をvarinit()関数で初期化する場合などは,エラー処理を回避するのではなく,regexp.MustCompileなどを使用するべきだと主張していました。一方,関数やメソッドの内では戻り値のerrorを受け取って適切に処理すべきだと述べていました。

このように,標準パッケージが提供する関数で最後の戻り値にエラーを返すものの一部に対して,Mustで始まる関数が用意されている場合があります。Mustで始まる関数は,内部で対応するerrorを返す関数を呼び出し,戻り値のerrornilではない場合は,panicを起こすように定義されています。

また,上記の例では,文字列のエスケープで正規表現が読みにくいため,raw string literalを使うべきだと指摘されていました。

deferにおけるエラー処理

以下のように,deferを使ってファイルをCloseする場合があると思います。deferを使えば,関数からリターンするときにCloseを呼び出すことができます。

func run() error {
    in, err := os.Open(*input)
    if err != nil {
        return err
    }
    defer in.Close()

    out, err := os.Create(*output)
    if err != nil {
        return err
    }
    defer out.Close()
    // some code
}

このとき,読み込むために開いたファイルの場合はそのままdefer in.Close()のように閉じてしまっても問題ないですが,書き込みのために開いたファイルの場合はそのまま閉じると,うまく書き込めなかった場合などにエラーが適切に処理できません。そのため,同氏は以下のように修正できると述べていました。

func run() (err error) {
    in, err := os.Open(*input)
    if err != nil {
        return err
    }
    defer in.Close()

    out, err := os.Create(*output)
    if err != nil {
        return err
    }
    defer func() {
        if cerr := out.Close(); err == nil {
            err = cerr
        }
    }()
    // some code
}

deferの中で戻り値であるerrnilである場合に,Closeで発生したエラーを代入することで,そのエラーを戻り値として返すことができます。そして,some code以下でエラーがあった場合は,戻り値のerr代入することでそのエラーを返すことができます。しかし,deferを使うと処理が複雑になることがあるため必ずしもdeferを使えば良いというわけではないとのことでした。上記の例では,some code以下がシンプルな場合はdeferを使わず,関数の最後でCloseを呼び出してエラーをチェックするほうがシンプルになると同氏は述べていました。

値とエラーを混ぜない

値としてエラーを使用している場合の例として以下のコードが挙げられていました。

func proc(it Iterator) (ret time.Duration) {
    d := it.DurationAt()
    if d == duration.Unterminated {
        ret = -1
    } else {
        ret = d
    }
    // some code
}
// duration.Unterminated = -1 * time.Second

func (it Iterator) DurationAt() time.Duration {
    // some code
    switch durationUsec := m.GetDurationUsec(); durationUsec {
    case -1:
        return duration.Unterminated
    case -2:
        return -2
    default:
        return time.Duration(durationUsec) * time.Microsecond
    }
    return -3
}

このコードでは,C言語やC++のコードのように特別な値をエラーとして扱っています。具体的には,time.Durationは期間を表す型なので,マイナスの値をエラーとして扱っています。Go言語では複数の戻り値を返すことができます。そのため同氏は,エラーを値に混ぜて返すのではなく,最後の戻り値としてerror型で返すべきだと主張していました。

エラーの設計

エラー処理の方法によって,エラーの設計方法が分けられるそうです。エラー処理の際に,エラーの区別が必要なく,単にerr != nilでエラーの有無をチェックする場合は以下のようにfmt.Errorferrors.Newを使用すると良いそうです。

fmt.Errorf("error in %s", val) もしくは errors.New("error msg")

また,エラーにはいくつか種類があり,それを区別する必要がある場合は以下のように変数に代入しておいて使用するほうが良いそうです。なお,このときの変数名はErr-が一般的なようです。

var (
  ErrInternal   = errors.New("foo: inetrnal error")
  ErrBadRequest = errors.New("foo: bad request")
)

error型はErrorメソッドを持つインターフェースです。そのため同氏は,エラーにいろいろな情報を含めたい場合は,構造体でそれらの情報を定義し,Errorメソッドを実装することでerrorとして使用することができると述べていました。なお,このときのエラー型の名前は-Errorという名前を使用するのが一般的らしいです。

type FooError struct { /* エラー情報のフィールド */ }
func (e *FooError) Error() string { return エラーメッセージ }

&FooError{ /* エラー情報 */ }

同氏は,Errorメソッドのレシーバをポインタにした場合,nilとの比較に注意すべきだと指摘していました。インターフェースは値と型情報を持ち,その両方がnilの場合にnilとなります。そのため,以下のコードはif err != nil {trueとなります(参考:FAQ: Why is my nil error value not equal to nil?⁠。

import "log"
type FooError struct{}
func (e *FooError) Error() string { return "foo error" }

func foo() error {
    var ferr *FooError  // ferr == nil
    return ferr
}
func main() {
    err := foo()
    if err != nil {
        log.Fatal(err)
    }
}

また同氏は,panicは極力使わないようにすべきだと主張していました。どうしてもpanicを使いたい場合は,recoverを使ってパッケージ外に出るときはerrorにすべきだと述べていました。

著者プロフィール

上田拓也(うえだたくや)

KLab(株)所属。Go Conference 2014 Autumnスタッフ。

業務ではJavaScript,Luaなどを扱っている。

大学時代にGo言語に出会い,それ以来Go言語にのめり込む。

時間があると,Go言語の勉強会に参加している。

Go言語のマスコットのGopherの絵を描くのも好き。

Twitter:@tenntenn