morikuni blog

技術系のなにか

Go 2 Draft DesignsのError Handlingについて

はじめに

gocon.connpass.com

Go 2 Draft Designs フィードバック会に参加してきたのですが、その中のエラーハンドリングの話について思ったことを書きます。 本家のGo 2 Draft Designsでは、handlecheckによって次のようにエラーハンドリングができるようになると書かれています。

func process(user string, files chan string) (n int, err error) {
    handle err { return 0, fmt.Errorf("process: %v", err)  }      // handler A
    for i := 0; i < 3; i++ {
        handle err { err = fmt.Errorf("attempt %d: %v", i, err) } // handler B
        handle err { err = moreWrapping(err) }                    // handler C

        check do(something())  // check 1: handler chain C, B, A
    }
    check do(somethingElse())  // check 2: handler chain A
}

個人的にエラーハンドリングを簡単にかけるようにすることは賛成ですが、このやり方はよくないと思ったので別の方法を考えました。

handle and checkのつらいところ

上のコードに書かれたコメントにあるようにhandleにはスコープが存在し、1つの関数内に複数のhandleが書けるようになっています。 さらにhandlereturnを書かなければerrを上書きできるようになっており、check 1のコメントがある部分ではC, B, Aのhandleが実行されます。 ただしcheck 2のコメントの部分ではAのhandleのみが実行されるようになっており、エラーが一体どのhandleで処理されるのかを意識しながらコーディングしなくてはなりません。

考えた方法

スコープを考えるのがつらいのであれば、関数スコープ外でhandleを定義できればよいのです。 なのでhandlerというものを定義できるようにすることを考えました。

// handlerと書くことでエラーハンドラーを定義できる
handler ParseError(err error) error { return errors.Wrap(err, “parse error“) }

// クロージャーと同じようにhandlerを返すfuncを定義できる
func WithMessage(message string) handler(err error) {
    return handler(err error) error { returns errors.Wrap(err, message) }
}

// 常にnilを返すことでエラーを無視できる
handler Ignore(_ error) error { return nil }

type MultiError struct {
    Errors []error
}

// structのメソッドとしても定義できる
handler (me *MultiError) Append(err error) error {
    me.Errors = append(me.Errors, err)
    return nil
}

func (me *MultiError) Error() string {
    return "multi error"
}

func doX() error {
    // checkはhandlerを受け取り、関数の返り値のerrでhandlerを呼び出す。handlerの返り値がnilなら処理を継続、そうでなければその場でreturnする。
    i1 := check ParseError strconv.ParseInt(“123”, 10, 64)
    i2 := check WithMessage(“parse error”) strconv.ParseInt(“123”, 10, 64)
    i3 := check Ignore strconv.ParseInt(“123”, 10, 64)

    // structに生やすことで複数のエラーを簡単にまとめることもできる。
    me := &MultiError{}
    i4 := check me.Append strconv.ParseInt(“123”, 10, 64)
    i5 := check me.Append strconv.ParseInt(“456”, 10, 64)
    return me
}

公式のDraftと大きな違いはありません。 handlerというfuncのようなものを定義できるようにするだけです。 handlerfunc(err error) errorという形で固定です。 checkではhandlerを受け取り、関数の返り値のerrがerr != nilの場合のみhandlerを呼び出します。 呼び出したhandlerの返り値がerr != nilの場合は、返ってきたerrをその場でreturnします。 nilだった場合はなにもせずにそのまま処理を継続します。

考え方としては、特別なinterfaceであるerrorと同じように特別なfuncであるhandlerを定義可能にしようというイメージです。 なお、このhandlerを採用する場合、handler型が書かれているところではエラーハンドリングが行われることを理解できるので、checkという構文自体を消して次のように書くことも可能になると思います。

i1 := ParseError strconv.ParseInt(“123”, 10, 64)

checkを入れるかどうかは見やすさや、コンパイラの実装がやりやすいほうに倒せばいいかなと思います。

追記

"Use functions as an error handler, Add syntactic sugar to remove duplicated if statement"というタイトルで Go2ErrorHandlingFeedback · golang/go Wiki · GitHub にFeedbackを書いておいた。 Go Fridayで話したらhandler構文なくしてもいいんじゃないかということを言われたので、提案したproposalではhandlerをfuncにしてcheckシンタックスシュガーのみを採用することにした。