Goが未使用のパッケージインポートをエラーとする理由
Goが未使用のパッケージインポートの記述をエラーにするという挙動は多くの非難を浴びました。
「コンパイルエラーでなく警告にすべき」
または
「コンパイルオプションで選べる様にして欲しい」
というような意見はたくさん寄せられましたがGo開発コアメンバーはこれらの要望に応えることはありませんでした。その理由を解説します。
Goの依存解決のアプローチ
Goの依存ツリーはコードの中に記述します。これはメジャーなパッケージマネージャとは異なる手法です。
多くの処理系では依存解決に必要な情報を言語仕様には含めず、別途ファイルに依存情報を列挙して記述しておき、それらの情報を辿ることで依存ツリーを構築しそのツリーに基づいて依存を解決するというアプローチを採用しているものがほとんどです。
Goは依存解決に必要な情報をコードの中に記述するというレアなアプローチを採りました。
ESモジュールのアイディアに近い考え方です。
ESモジュールのインポート例
HTTPのGetによりリソースを読み込みます。
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.module.js';
プロトコルがgitになっているだけで考え方は同じ。
必要なタイミングでインポートパスに該当するVCSリポジトリから内容を引き込みます。
多くは「git clone https://<インポートパス>」
import hoge "github.com/nobonobo/hoge"
npmやpipなどメジャーなパッケージマネージャが採用している方式はコード中に依存情報は記述せずに、依存解決に必要な情報を別ファイルに記述しておいてその情報だけををもとに依存解決を行います。
Goモジュールも一見go.modファイルに依存情報が列挙されていて同じ方式に見えますが依存情報の主たる記述はコード中に書いたものになっています。go.modはどちらかというと依存モジュールの「バージョン情報」を記述するためのファイルになっています。
ソースコードに記述された依存が増えるとビルド時にはその時点の最新バージョンへの依存情報が自動的にgo.modファイルに追記されます。
go mod tidyの挙動
「go mod tidy」を行う時、どの外部リポジトリのモジュールを引くかどうかはソースコードにインポートされているかどうかで決まります。go.modファイルに列挙されたものを全て引き込むことはしません。引き込むことが決まったモジュールに関する記述がgo.modにあればgo.modに指定されたバージョンを引き込むだけです。その後go.sumがあればチェックサムをベリファイするという挙動です。
ソースコードに依存記述があり、go.modに無いもしくはgo.modが無い場合は改めてその依存の最新バージョンを引き込みます。
つまり、go.modファイルの記述内容で依存解決を行うわけでは無いというところがユニークな挙動です。go.modファイルはどちらかというと依存情報(URLとバージョン)の可視化とバージョン指定がメインの役割なのです。
Goの依存解決の良さ
go-getの挙動を要約すると以下の挙動を採ります。
- ソースコード上で参照されているモジュールやパッケージだけをダウンロードする
- ソースコード上で参照されている「サブパッケージ」だけをビルド対象とする
これはダウンロードする分量とコンパイル対象のコードそれぞれが最小に保たれるという恩恵につながっています。この良さを知ってしまうと、従来型のパッケージマネージャが自己申告型であるがゆえにずいぶんと蛇足な依存を引き込み必要のないモジュールをビルドしてしまう挙動がみられ、開発マシンやネットのリソースを不必要に浪費してしまうことがもったいなく感じてしまいます。(例えばhttp負荷テストツールをビルドする時、HTTPサーバーフレームワークやその依存モジュール群までビルドされてしまうなど)
さらにGoは持ち前のコンパイルの速さがこの良さをさらに後押ししています。
この良さを担保するのに一役買っているのが
「Goが未使用のパッケージインポートの記述をエラーにする」
という挙動なのです。
また、この文書でお伝えしたいこととは別件ですが、
Goの依存バージョン指定は指定メジャーバージョンが同じ範囲に保とうとする挙動も面白いです。
V1を指定している場合、V2が公開済みであっても依存のアップデートを行った時にV1の最新バージョンになるだけという挙動をとります。
モジュール公開の推奨ポリシーに
「モジュールのインターフェースの互換が取れない場合はメジャーバージョンをあげましょう」
というものがあります。つまり、逆にいうと「同じメジャーバージョンのうちは呼び出しインターフェースの互換を維持しましょう」ということです。この場合、依存バージョンのアップデートを行っても動作するということが期待できるという素晴らしい仕掛けです。この良さはこれから効果がじわじわ広がっていくと思います。
まとめ
「Goが未使用のパッケージインポートの記述をエラーにする」理由は、
「Goのエコシステムが素早く最適な依存解決ができるようにする」ためなのです。
Discussion
ちなみにこの依存解決の基本挙動はGo-Moduleだからこうなったわけでなく、僕の確認しているかぎりGoの1.2から何も変わっていません。Go1.0からこうなっていたんじゃないでしょうか。
私もその認識ですね
もちろん、人は怠惰なので、オプション化してエラーをワーニングに変えられるようにしたとき、配布物を公開する時にちゃんとオプションを戻してエラーがないことを確認してからリリースをちゃんと忘れずに実行し続けるのは無理で、ちゃんとCIをセットアップしてCIをパスした時だけリリースされるように作りこむことが求められるんだけど、人は怠惰なのでやはりそういうセットアップをもれなくすることも無理なのです。