日記マン

モチベはGCP, MapReduce, MachineLearning, Docker, [Kotlin/Go/Python] です。アカデミックへの興味は情報幾何や計算論的神経科学など。

Go言語の例外を必ず扱うような言語機能のありがたさ

僕はGo言語が好きだ。そのGoがもたらす恩恵のひとつとして、例外周りのセマンティクスがある。
Goでは例外はerrorという型の値に抽象化され関数の返り値として記述することが多いというのはご存知だと思う。

func GetUser(id int) (*User, error) {
    // do some thing ...
    if err != nil {
        return nil, err
    }

    return user, nil
}

上記の例のように、複数の返り値を設定できる言語機能を利用して、
第一にその関数に期待する主要な出力のデータ、第二に関数内で発生した例外(error型の値)を伝播させるのが一般的な記述だ。

この if err != nil {return err} を毎回書くのが(たとえコピペでも)
「めんどくさい」、「冗長だ」という意見を持つプログラマもスクなくはない。
個人的には気持ちはわからなくもないが後述する理由と別にそこまで苦に思うような記述量じゃないと思っているが
TwitterのTLでも何度かこの批判的な声はよく界隈を賑わしている。

僕は、むしろ、このセマンティクスには肯定で、どちらかというとありがたい恩恵をもたらしていると思っている。
その恩恵というのは、感知すべきバグを埋もれさせてしまうことを防ぐ一助になっているからだ。
実際に僕はGoでアドサーバAPIを書き、運用を初めてから潜在したバグもなく稼働し続けることに成功している。

早期returnの記述が多くなりやすい

まず第一にGo言語は手続き的な記述に優れたシンタックス・セマンティクスを提供している。
それゆえ、手続きは上から下に流れていく。その上から流れる手続きのなかで、
外部関数を呼び出した際のerror検知は、基本的に早期returnの記述が多くなる。
上記の例でもあったように、

   if id < 0 {
        return nil, fmt.Errorf("invalid user id")
    }
    // ここから先はidが不正な値であるケースを
    // 考慮しなくてよくなる
    user := &User{ID: id}
    if err := db.Get(user); err := nil {
        return nil, err
    }
    // ここから先は外部呼び出しの例外(DBからGetできなかった)のケースを
    // 考慮しなくてよくなる
    // do something ...

上から下へと理路整然と記述される手続きにおいて、
if err != nil {return err} は例外のケースを取り除いていき、
手続きの内容がシンプルに洗練されていく。
これによりコーディングもコードリーディングも、考えうるケースというのを不必要に増大させない効果をもたらしている。
その習慣的なフレームワークにより、バグを生みにくいプログラムコードを書くことができると思う。

めんどくさいけどやらなきゃいけないことにシンプルなインターフェイスを提供している

ここからは個人的に他の言語にある try-catch による例外機構と比較していく。
意見のわかれるところであり、論争に発展しそうな話題である。

まず。例外処理はやらなきゃいけない。
でも僕個人はめんどくさがりで、 うまくいくことを記述することがなによりも楽しい。
そうしたときに、try-catchをサボりたくなる。
一度うまくいった場合しか想定しないコードを書いた後で、try-catchを書くのは結構めんどくさいと思ってしまう。
ネストしないといけないし、スコープのこととか、どの粒度が適切か、今一度リファクタリングの必要を与える。

そんなめんどくさい例外処理というのを、Go言語は、
if err != nil { return err } という極限までシンプルなインターフェイスによって
必ずしないといけない制約を設けている。
これは、もたらしてくれる恩恵にくらべれば、メンドくさがりやの僕でも、このルールくらい全然守れる。