Goでゼロ値の構造体を生成することを防ぐべきかという話

社内slackで以下の記事と同じようなことを悩んでいるというメッセージがあった。 2018年の記事ではあるが、自分もGoを利用し始めた頃に考えたことがあるので、この記事に書かれている内容をベースに改めて自分の意見をまとめておく。 ここに書く内容が正しいかどうかは自己判断して欲しい。

tyru.hatenablog.com

なぜ golint でエラーになるのか?

上記の記事には "golint でエラーが出る" という内容があった。 なので "そもそもなぜ golint でエラーが出るのか?" を考える必要がある。 エラーになる理由は以下である。

github.com

パッと読んだ感じ以下の2つになると思う。

1. クライアントコードで非公開になっている構造体を参照できない。

https://github.com/golang/lint/issues/210#issuecomment-219888594

2.非公開の構造体はGoDocに出力されないのに利用者がそれを利用することができてしまう。

https://github.com/golang/lint/issues/210#issuecomment-245724135

人によって感じ方は違うかもいれないが、個人的には「なるほどなー」という感じで納得感がある。 特に1番に関しては別パッケージの関数の引数や構造体に非公開になっている構造体を指定することができないので、 利用者からすると非常に使いづらい(利用できる場所が限定される)構造体になってしまうだろう。

元の記事で "ライブラリとして提供する場合" について言及されているが、 不特定多数の利用者が想定されるライブラリではライブラリが提供する構造体をどう扱うのかは利用者次第である。 構造体に持たせたり、関数の引数に指定したり・・・利用者の実装によって扱い方は異なってくるだろう。 "ライブラリとして提供するので正しい構造体の生成を強制する" というメリットによって "不特定多数にとって使いづらい構造体になる" というデメリットが発生してしまってはライブラリの実装としては本末転倒な気がする。

元の記事の "Interface を定義することを考える" という解決策について

元の記事では最終的に "Interface を定義することを考える" という結論になっている。 たしかに Interface を利用することで、この問題を解決することはできるかもしれない。 ただ、Interface は抽象を表すための機能なので "Interface を定義することで解決する" というのは用途として違うかなと思う。 "どの構造体に Interface をもたせるか?" というのは程度問題になるが、 極端に言うと "すべての構造体に Interface を持たせる" という方針が正当化されてしまう。 避けた方がいいだろう。

現実的に起こりうるリスクを考える

意図しない構造体の生成を防ぐためのアプローチとしては、 構造体のフィールドを private にするというのが一般的だろう。

type Editor struct {
    name string
}

しかし、この方法では以下のようなゼロ値の構造体生成は防げない。

editor = &Editor{}

なので、この問題でポイントになるのは "ゼロ値の構造体生成を防ぐ必要があるのか?" という点である。 仮に何かしらの前提があって利用者がゼロ値の構造体を生成する可能性が 0% であれば、 この問題はフィールドを private で定義するだけで解決するはずである。

では、実際に "ゼロ値の構造体を生成する可能性があるのか?" というと、 個人的に可能性としては低いかなと思っている。

例えば、使ったことのない新しいライブラリを利用する場合、当然ドキュメントは読むし、example のコードも確認するだろう。 仮にそれらがない場合でも、Goの利用者であれば NewXxx() という構造体を生成するための関数があるかどうかは確認するだろう。 仮に NewXxx() を見落として、ゼロ値の構造体を利用してしまったとしても、 実動作を確認したり、テストを書いたり、リリース前のQAはするだろう。 いずれにしても、どこかのタイミングで気づくはずである。 仮にこれらすべての工程を突破して本番環境で問題になった場合、実装の問題というよりも開発体制であったり、システムの仕様上に問題がある気がする。

ということで、"ゼロ値の構造体を生成する可能性" についてはあまり気にしなくていいかなと思っている。

まとめ

Goは構造体のオブジェクトを直接生成できますが、Java, PHP は Go のようにクラスのオブジェクトを直接生成できないんですよね。 なので、その世界観を Go に持ち込むと、自分みたいに悩んでしまうことがあるのかなーと思いました。 言語仕様によって "正しい姿" は変わってくるので、いい意味で言語に合わせた妥協をすることで、結果的にその言語の思想に沿った良い実装になるのではないかと思っています。

宣伝

DMMでチーム作りました。 "マイクロサービス x 認証基盤 x ゼロトラストネットワーク x DDD" あたりに興味ある方は是非話を聞きに来てください。

bosyu.me