The Go Blog
Goにおけるパッケージバージョニングの提案
はじめに
8年前、Goチームはgoinstall(後にgo getにつながる)と、Go開発者にとって今日おなじみの分散型URLライクなインポートパスを導入しました。goinstallをリリースした後、人々が最初に尋ねた質問の1つは、バージョン情報をどう組み込むかということでした。私たちは知らないと認めました。長い間、私たちはパッケージバージョニングの問題はアドオンツールによって最もよく解決されると信じ、人々がそれを作成することを奨励しました。Goコミュニティは様々なアプローチで多くのツールを作成しました。それぞれが私たち全員の問題理解を深めましたが、2016年中頃には、あまりにも多くの解決策があることが明らかになりました。私たちは単一の公式ツールを採用する必要がありました。
2016年7月のGopherConで始まり、秋まで続いたコミュニティでの議論の後、私たちは皆、RustのCargoに代表されるパッケージバージョニングのアプローチ、つまりタグ付けされたセマンティックバージョン、マニフェスト、ロックファイル、そして使用するバージョンを決定するためのSATソルバーに従うのが答えだと信じていました。Sam BoyerはDepを作成するチームを率い、この大まかな計画に従い、goコマンド統合のモデルとして機能させるつもりでした。しかし、Cargo/Depのアプローチの意味合いについてさらに学ぶにつれて、Goは特に後方互換性に関して、いくつかの詳細を変更することで利益を得られることが明らかになりました。
互換性の影響
Go 1の最も重要な新機能は、言語機能ではありませんでした。それはGo 1が後方互換性を重視したことでした。それまで私たちは、ほぼ毎月、大幅な非互換な変更を伴う安定版スナップショットをリリースしていました。Go 1のリリース直後から、関心と採用が著しく加速したのを観察しました。私たちは、互換性の約束が開発者にとってGoを本番環境で使用することにはるかに快適に感じさせ、Goが今日人気がある主要な理由であると信じています。2013年以来、Go FAQはパッケージ開発者に、自身のユーザーに対して同様の互換性の期待を提供することを奨励しています。私たちはこれをインポート互換性ルールと呼んでいます:「古いパッケージと新しいパッケージが同じインポートパスを持つ場合、新しいパッケージは古いパッケージと後方互換性がある必要があります。」
独立して、セマンティックバージョニングは、Goコミュニティを含む多くの言語コミュニティでソフトウェアバージョンを記述するためのデファクトスタンダードとなっています。セマンティックバージョニングを使用すると、後のバージョンは以前のバージョンと後方互換性があることが期待されますが、それは単一のメジャーバージョン内でのみです。v1.2.3はv1.2.1およびv1.1.5と互換性がある必要がありますが、v2.3.4はそれらのいずれとも互換性がある必要はありません。
ほとんどのGo開発者が期待するように、Goパッケージにセマンティックバージョニングを採用する場合、インポート互換性ルールは、異なるメジャーバージョンが異なるインポートパスを使用することを要求します。この観察により、v2.0.0から始まるバージョンがインポートパスにメジャーバージョンを含むセマンティックインポートバージョニング、つまりmy/thing/v2/sub/pkgが導き出されました。
1年前、私はバージョン番号をインポートパスに含めるかどうかは主に好みの問題であり、それらがあることが特にエレガントであるとは懐疑的でした。しかし、この決定は好みの問題ではなく、論理の問題であることが判明しました。インポート互換性とセマンティックバージョニングが組み合わさって、セマンティックインポートバージョニングを要求します。このことに気づいたとき、その論理的な必然性に私は驚きました。
私はまた、セマンティックインポートバージョニングへの第2の独立した論理的経路、すなわち段階的なコード修復または部分的なコードアップグレードがあることに気づいて驚きました。大規模なプログラムでは、プログラム内のすべてのパッケージが特定の依存関係のv1からv2に同時に更新されることを期待するのは非現実的です。代わりに、プログラムの一部がv1を使用し続けながら、他の部分がv2にアップグレードできる必要があります。しかし、その場合、プログラムのビルドと最終的なバイナリは、依存関係のv1とv2の両方を含まなければなりません。それらに同じインポートパスを与えることは混乱を招き、私たちがインポート一意性ルールと呼ぶものを侵害します。異なるパッケージは異なるインポートパスを持たなければなりません。部分的なコードアップグレード、インポート一意性、そしてセマンティックバージョニングをすべて実現する唯一の方法は、セマンティックインポートバージョニングも採用することです。
もちろん、セマンティックインポートバージョニングなしでセマンティックバージョニングを使用するシステムを構築することは可能ですが、それは部分的なコードアップグレードまたはインポートの一意性のいずれかを放棄することによってのみです。Cargoはインポートの一意性を放棄することによって部分的なコードアップグレードを可能にします。つまり、特定のインポートパスは、大規模なビルドの異なる部分で異なる意味を持つことができます。Depは部分的なコードアップグレードを放棄することによってインポートの一意性を保証します。つまり、大規模なビルドに関わるすべてのパッケージは、特定の依存関係の単一の合意されたバージョンを見つけなければならず、大規模なプログラムがビルド不能になる可能性が生じます。Cargoは大規模なソフトウェア開発にとって不可欠な部分的なコードアップグレードを主張するのが正しく、Depもインポートの一意性を主張するのが同様に正しいです。Goの現在のベンダーサポートの複雑な使用はインポートの一意性を侵害する可能性があります。そのような場合、結果として生じる問題は、開発者とツールにとって理解するのが非常に困難でした。部分的なコードアップグレードとインポートの一意性のどちらかを選択するには、どちらを放棄するのがより痛いかを予測する必要があります。セマンティックインポートバージョニングは、その選択を避け、両方を保持することを可能にします。
また、インポート互換性がバージョン選択をどれだけ簡素化するかを発見して驚きました。バージョン選択とは、特定のビルドで使用するパッケージバージョンを決定する問題です。CargoとDepの制約により、バージョン選択はブール充足可能性を解くことと同等になり、有効なバージョン構成が存在するかどうかを判断するのに非常にコストがかかる可能性があります。そして、多くの有効な構成が存在する可能性があり、その中で「最良の」ものを選択する明確な基準はありません。代わりにインポート互換性に依存することで、Goは常に存在する単一の最良の構成を見つけるための、些細な線形時間アルゴリズムを使用できます。私が最小バージョン選択と呼ぶこのアルゴリズムは、結果として、個別のロックファイルとマニフェストファイルの必要性を排除します。それらを、開発者とツールの両方によって直接編集される単一の短い構成ファイルに置き換え、再現可能なビルドを依然としてサポートします。
Depでの私たちの経験は、互換性の影響を示しています。Cargoや以前のシステムに倣い、私たちはセマンティックバージョニングを採用する一環として、インポート互換性を放棄するようにDepを設計しました。意図的にそう決定したとは思いません。ただ、他のシステムに倣っただけです。Depを実際に使用した経験は、互換性のないインポートパスを許可することによって、どれだけの複雑さが生じるかを正確に理解するのに役立ちました。セマンティックインポートバージョニングを導入することで、インポート互換性ルールを復活させることにより、その複雑さが排除され、はるかにシンプルなシステムにつながります。
進捗、プロトタイプ、そして提案
Depは2017年1月にリリースされました。その基本的なモデル、つまりセマンティックバージョンでタグ付けされたコードと依存関係の要件を指定する設定ファイルは、ほとんどのGoベンダーツールからの明確な進歩であり、Dep自体への収束も明確な進歩でした。私は、特に開発者が自身のコードと依存関係の両方でGoパッケージのバージョンについて考えることに慣れるのを助けるために、その採用を心から奨励しました。Depが明らかに私たちを正しい方向に導いている一方で、私は詳細における複雑さの悪魔について懸念を抱き続けていました。特に、大規模なプログラムでの段階的なコードアップグレードのサポートがDepに欠けていることについて懸念していました。2017年の間、私はSam Boyerやパッケージ管理ワーキンググループの他のメンバーを含む多くの人々と話しましたが、誰も複雑さを減らす明確な方法を見つけることができませんでした。(私はそれに加わる多くの方法を見つけました。)年末に近づいても、SATソルバーと満足できないビルドが最善の策であるように見えました。
11月中旬、Depが段階的なコードアップグレードをどのようにサポートできるかを再度検討しようとしたところ、インポート互換性に関する私たちの古いアドバイスがセマンティックインポートバージョニングを意味していることに気づきました。これは真のブレークスルーのように思えました。私は、私のセマンティックインポートバージョニングに関するブログ記事の初稿を書き、Depがその慣習を採用することを提案して締めくくりました。私はこの草稿を話していた人々に送ったところ、非常に強い反応がありました。誰もがそれを気に入るか、嫌うかでした。私は、このアイデアをさらに広める前に、セマンティックインポートバージョニングのより多くの意味合いを検討する必要があることに気づき、その作業に取り掛かりました。
12月中旬、インポート互換性とセマンティックインポートバージョニングが組み合わさることで、バージョン選択を最小バージョン選択に削減できることを発見しました。私はそれを理解していることを確認するために基本的な実装を書き、それがなぜそれほど単純なのかの理論を学ぶのにしばらく時間を費やし、それを説明する記事の草稿を書きました。それでも、このアプローチがDepのような実際のツールで実用的かどうかはまだ確信が持てませんでした。プロトタイプが必要であることは明らかでした。
1月、私はセマンティックインポートバージョニングと最小バージョン選択を実装するシンプルなgoコマンドラッパーの作業を開始しました。些細なテストはうまく機能しました。月末に近づくと、私のシンプルなラッパーは、多くのバージョン管理されたパッケージを使用する実際のプログラムであるDepをビルドできるようになりました。このラッパーにはまだコマンドラインインターフェイスがありませんでした。Depをビルドしているという事実は、いくつかの文字列定数にハードコードされていましたが、このアプローチは明らかに実行可能でした。
2月の最初の3週間は、ラッパーを完全なバージョン付きgoコマンドであるvgoに変換し、vgoを紹介するブログ記事シリーズの草稿を書き、Sam Boyer、パッケージ管理ワーキンググループ、Goチームとそれらを議論しました。そして、2月の最後の1週間で、ついにvgoとその背後にあるアイデアをGoコミュニティ全体と共有しました。
インポート互換性、セマンティックインポートバージョニング、最小バージョン選択という核心的なアイデアに加え、vgoプロトタイプは、goinstallとgo getでの8年間の経験に基づいて、いくつかの小規模ながら重要な変更を導入しています。これには、ユニットとしてバージョン管理されるパッケージの集合であるGoモジュールという新しい概念、検証可能で検証済みのビルド、そしてgoコマンド全体でのバージョン認識が含まれ、$GOPATH外での作業を可能にし、(ほとんどの)vendorディレクトリの廃止につながります。
これらすべての結果が、私が先週提出したGoの公式提案です。完全な実装のように見えるかもしれませんが、まだプロトタイプに過ぎず、私たち全員が協力して完成させる必要があります。vgoプロトタイプはgolang.org/x/vgoからダウンロードして試すことができ、Tour of Versioned Goを読んでvgoの使用感がどのようなものかを知ることができます。
今後の道筋
私が先週提出した提案は、まさにそれです。最初の提案です。Go開発者が私たちが知らない多くの巧妙な方法でGoを使用しているため、Goチームと私が見つけられない問題がそこにあることは承知しています。提案のフィードバックプロセスの目標は、私たち全員が協力して現在の提案の問題を特定し、対処することであり、将来のGoリリースで出荷される最終的な実装が可能な限り多くの開発者にとってうまく機能するようにすることです。問題は提案の議論イシューで指摘してください。フィードバックが届き次第、議論の要約とFAQを更新していきます。
この提案を成功させるには、Goエコシステム全体、特に今日の主要なGoプロジェクトが、インポート互換性ルールとセマンティックインポートバージョニングを採用する必要があります。それがスムーズに行われるように、新しいバージョニング提案をコードベースに組み込む方法について質問があるプロジェクト、またはその経験についてフィードバックがあるプロジェクトと、ビデオ会議によるユーザーフィードバックセッションも実施します。そのようなセッションへの参加に興味がある場合は、Steve Francia(spf@golang.org)までメールしてください。
Goコミュニティに、go getにパッケージバージョニングを組み込む方法に関する単一の公式な回答を(ついに!)提供できることを楽しみにしています。ここまで私たちを助けてくれたすべての人々、そして今後も私たちを助けてくれるすべての人々に感謝します。皆様の助けを借りて、Go開発者が気に入るようなものを出荷できることを願っています。
次の記事:Goの新しいブランド
前の記事:Go 2017年調査結果
ブログインデックス