Edited at

GoでNewなんとかのような関数を不必要に提供しないほうが良い理由

More than 5 years have passed since last update.

構造体を定義して、それに対してメソッドを定義して、最後にその構造体をアロケートして初期化するNewなんとかという関数を用意する、というのを何の疑問も持たずに行っているならちょっと考えものだ。途中まではよいが、Newなんとかみたいなのは別に必須ではない。


メソッドとゼロ値

Goでは新しい値は「ゼロ値」で自動的に初期化される。ゼロ値は型ごとに違うが、数値なら0、文字列なら空文字列、ポインタやインターフェイスならnil、といった具合の値だ。構造体ならそれぞれのフィールドがゼロ値で初期化される。

メソッドはそのレシーバーの値のゼロ値に対して問題なく動くように書くほうがよい。構造体Tを割り当てて初期化する関数としてNewTみたいな関数を用意するのは、本当に初期化が必要なとき以外はやらないほうがよい。

なぜか、というといくつか理由がある。


  1. NewTの代わりにnew(T)を使うようにすると、エクスポートされる関数が一つ減る。APIがそのぶん多少簡単になる。

  2. var x Tや&T{}のような書き方でも値を割り当てられるようになる。

  3. Tを別の構造体Uのフィールドとして使った時に、Tがゼロ値で動くなら、Uも単純にnew(U)などで割り当てられるようになる。NewTが必須だとしたら、Tではなく*Tをフィールドとして使わないといけなくなるし、NewTを内部で呼ぶNewUも用意しないといけなくなる。アロケーションが2回になってしまう。

Newを必要としない型の好例の一つはsync.Mutexで、そのゼロ値はロックされていないミューテックスとして正当な値ということになっているので、他の構造体のフィールドにするのが簡単になっている。

bytes.Bufferも初期化を必要としない。bytes.NewBufferという関数も用意されているけど、これはバッファの長さの初期値を与えたうえで初期化するというオプショナルな機能で、使うのは必須ではない。


どうやって初期化を不要にするのか

大体の値はゼロ値がそれなりに適切なデフォルト値になっていると思う。その時には別になにもしなくてよい。

そうでない場合はメソッドの中で最初に値が初期化されているか調べて、初期化されていなければ初期化するというやり方がある。たとえばポインタがnilかどうか調べて、nilならポインタを初期化するというやり方である。

構造体のフィールドに代入するためのconstをいくつか定義しているなら、0がデフォルトになるように定数を定義するとよい。そうすると構造体がゼロ値で動くようになる。