強い思想: Go を Web 開発に採用する上で
Go は Web 開発に向いているか?
最も向いている領域は「CLI ツール」「ミドルウェア」「マイクロサービス」だと思っている。なぜならそれらはコードベースを比較的小さく抑えることを前提としているからだ。 Go は大きなコードベースを抱えやすい設計の言語になっていない。
- ミドルウェアとマイクロサービスに関しては小さく作ることが正義。
- CLI ツールに関しては単一責務なツールであれば小さくなるが,複数を束ねるツールであっても Web サービス開発に比べれば考えることは少なくて済む。
Web 業界における「一般的な Web 開発」,すなわちモノリスを基本とした中規模以上の開発にははっきりと 向いていない と言うべきだろう。
フラットパッケージは正義か?
私が SNS で何度か言及した以下の記事がある。
フラットパッケージ戦略は,確かに Go の文化圏においては一定の支持を集めている。Go の公式リポジトリや有名ライブラリなんかも,Java などの言語に比べたらずっとパッケージ階層が浅く,ネストしていないものが多いと思う。
しかし,それも 「コードベースを小さく保つ」 を大前提としていることを忘れてはならない。 DDD やクリーンアーキテクチャといった言葉が飛び交うぐらいの規模であれば,パッケージを切ることに関して後ろめたさを感じる必要はない。 むしろ,大きなコードベースが誕生することが開発初期から簡単に予見できるような状況で, YAGNI という言葉に甘んじて設計を放棄するのは極めて悪手であると私は断言する。身を以て失敗を経験した私の口から伝えたい。
Go はそのパッケージ構成上,名前の衝突を容易に起こす。
- PHP や Java は循環参照も許されているし,カジュアルにネームスペースを切っていい文化がある。ゆえにクラス名に関して衝突が起こりにくいい。また処理のほぼ全てがクラスのメソッドとして載るため,メソッド名も衝突を起こしにくい。
- TypeScript/JavaScript(ESM) や Rust はクラスにメソッドとして載せるというよりは,普通に関数として並べることが多い。しかしファイル単位でスコープが形成され,明示的にエクスポートしていないものは公開もされない。
- Go はディレクトリ単位でパッケージスコープを構成する。そのため,フラットにその中に大量のファイルが並ぶと,それらの中ではシンボルの衝突が起こりやすい。
フラットに並べても何とかなる範囲のものしか 1 パッケージに押し込んではならないのは明らかである。
技術的関心でパッケージを切ってしまうのもよくない。以下にアンチパターンの例を挙げる。
controllers
というパッケージを切り,その中にロジックの中核ごと埋め込んでしまう。
- 処理を切り出す際に作る,ちょっとしたユーティリティ関数同士が衝突を起こしやすい。TypeScript/JavaScript であればエクスポートされていない関数, PHP/Java であれば private メソッドとしてカジュアルに切り出せる部分が, Go だと切り出しづらくなる。
- 切り出してある程度横並びになること自体は本来そこまで悪くないはずだが,ドメイン的に関連性がないものが横並びになる のは由々しき事態である。
controllers
自体はあってもよいが,その中身は極力シンプルにし,責務は 「各パッケージに処理を委譲すること」 程度で済ませておかなければならない。
db
というパッケージを切り,その中にデータベース接続や使用する SQL の実装などをすべて埋め込んでしまう。
- これも同様で,ドメイン的無関係な SQL がたくさん横並びになると非常に可視性・一覧性が悪くなる。
- 私が担当したプロジェクトでは
sqlc
というものを使用したが,このコード生成ツールを見出しのようなディレクトリ構成に適用してしまった結果, 1 パッケージの 1 インタフェース配下にクエリが 200 個以上並べられるという地獄が生まれてしまった。- 使い方次第で多少は工夫できる面もあるがこの際,
sqlc
は SQL の数が多いプロジェクトには向いていない と言い切ってしまってもいいだろう。
- 使い方次第で多少は工夫できる面もあるがこの際,
個人的に思う正解を述べる。 データベースコネクション周りに関する知識は db
に入れてよいが,具体的なクエリの知識は各ドメインのパッケージが持つべきである。 クエリはデータベースコネクションのインタフェースを利用し,具象に依存せずにコードを書くことができる。
データベース周りは Bun を使いましょう。コード生成なんかせずとも,生 SQL とほぼ同じ自由度で書ける非常に強力なクエリビルダーが提供されています。
panic
は厳禁か?
全くそんなことはない。Go は標準パッケージでも,使い方を誤ったときは容赦なく panic が発生する設計になっているものも多い。
…にも関わらず, 「panic
は使ってはいけないんだ」 と思考停止したように、自分が書くコードの中では完全回避する人が多いのには頭を抱えている。
上の記事で述べているように,明らかに使い方が間違ったことで発生するものに関しては,容赦なく panic
を書いてよい。そうすることで無駄なエラー分岐がなくなり,本当に運用中に発生しうる分岐だけに注力することができる。コードの可読性も大きく上がるだろう。
PHP の基準で目安を説明しよう。
PHP におけるもの | Go でどうすべきか |
---|---|
Error |
panic |
LogicException |
アプリケーションであれば panic ライブラリであれば状況次第 |
RuntimeException その他の Exception
|
丁寧にエラー分岐 |
Go のテストライブラリは標準のもので十分か?
諸派あるだろうが,これも真っ向から否定させていただく。
要するに、「Assert は便利だけど、頼り過ぎてエラーのレポートが適当になる。エラーレポート重要だからきちんと書こう。議論の余地はあるけど、新しい試みとしてやってみる。」とのこと。
オープンソースライブラリ,あるいは小さく作られたソフトウェアについてはこの姿勢は間違っていないと考えてもよいと思う。しかし,一般業務におけるモノリス Web アプリケーション開発においては適合しない場面が多いと私は考えている。
我々の業務でのアプリケーション開発に対しては,理想が通用しないことが非常に多い。オープンソースライブラリは比較的コード品質が高く保たれる傾向にあるため,「冗長に美しく書く」 のがうまくいくケースもあるだろう。
一方で業務では,そんなに意識が高くならないことも多い。きれいに書けるほどスキルのない人も多いし,実力のある人でも適度に手を抜いたりする。そんな状況に対して 「意図的に不便なデザイン」 を突き付けたらどうなってしまうだろうか?それは想像に難くない。品質の低い,ただ冗長で読みにくく改修もしにくいテストが量産されてしまうだけである。その状況では 「コード量を賢く抑える」 アプローチ,即ちアサーションを簡易に記述するためのライブラリを導入したほうがうまくスケールするだろう。
これを入れましょう。
Go のエラーライブラリは標準のもので十分か?
これに関しても,アプリケーション開発という文脈においてはあまり同意はできない。
Go はスタックトレースを error
に積まないため,エラーが発生した場所を特定できるようにするには,上に伝播していくエラーに対して,各階層で fmt.Errorf("〇〇が失敗しました: %w", err)
のように特徴的なメッセージを連結していく必要がある 。これを徹底されるための Linter ルールがある。
しかし実際には,毎回ラップする際にエラーメッセージを都度全部書くのが煩雑に感じられることが多いだろう。これもアプリケーション開発かライブラリ開発かで温度感が変わってくる部分だと思う。その際, cockroachdb/errors.WithStack()
のような,スタックトレースを自動で作ってくれるもの があると開発効率の向上に寄与すると思う。パフォーマンスが悪いという噂があるが, IO がボトルネックになることが大半な Web アプリケーション開発でそんなことを気にするだけ無駄であることがほとんどである。オブザーバビリティのほうがもっと大事だ。
私は標準エラーライブラリだけで突き進んでしまい,「さすがに毎回ラップを強制するのはやりすぎか」と思って wrapcheck
ルールを外してしまった結果,悲劇を体験した。
ライブラリに依存しすぎるとメンテナンス性が云々論について
一度作り切ってしまったら放置するだけでよいアプリケーションに関しては,確かに一理ある。コードを凍結保存していると考えてよいようなケースである。
しかし,実際には運用保守していく中でコードは継ぎ接ぎされていくことがほとんどだ。コードは生きている。その際,ライブラリに依存しなければそれに相当するロジックを自分で泥臭く書くことになるが,ライブラリの依存関係を保守するのとどっちが楽だろうか?あなたはライブラリ作者よりも頭のいいコードが書けるのか?
はっきりと自信をもって YES と言えるケースでだけ,車輪の再発明をすればよいだろう。それ以外のケースでは,本当に必要であれば容赦なくライブラリを入れていってよい。なにせ,意図的に機能を削りまくってシンプルを追求した言語なのであるから,中規模以上のモノリスを作るには言語標準機能だけでは力不足になるのは最初から分かり切っていることなのだ。