Goモジュール:v2とその先
Go Modules: v2 and Beyond by Jean Barkhuysen and Tyler Bui-Palsulich
はじめに
この記事はシリーズの第4回です。
- 第1回:Goモジュールを使う
- 第2回:Goモジュールへの移行
- 第3回:Goモジュールの公開
- 第4回:Goモジュール:v2とその先(本記事)
- 第5回:モジュールの互換性を保つ
なお、モジュールの開発に関するドキュメントについては Developing and publishing modules を参照してください。
成功したプロジェクトが成熟し、新たな要求が加わってくると、過去の機能や設計上の決定が 意味をなさなくなることがあります。開発者はそこから学んだ教訓を踏まえて、非推奨になった 関数を削除したり、型の名前を変えたり、複雑になったパッケージを扱いやすい単位に分割したり したくなるかもしれません。しかしこの種の変更は、下流の利用者に新しいAPIへの移行の手間を 強いることになるため、その利益がコストを上回ると慎重に判断できない限り、行うべきではありません。
まだ実験的な段階にあり、メジャーバージョンが v0 であるプロジェクトについては、
利用者もときおり破壊的変更が入ることを見込んでいます。一方、安定版として宣言された
メジャーバージョン v1 以降のプロジェクトでは、破壊的変更は必ず新しいメジャーバージョンとして
行わなければなりません。本記事では、メジャーバージョンの意味論、新しいメジャーバージョンの
作成と公開の方法、そして複数のメジャーバージョンを保守する方法について解説します。
メジャーバージョンとモジュールパス
モジュールは、Goにおける重要な原則である インポート互換性ルールを明文化しました。
古いパッケージと新しいパッケージが同じインポートパスを持つならば、
新しいパッケージは古いパッケージと後方互換性を持たなければならない。
定義上、パッケージの新しいメジャーバージョンは、以前のバージョンと後方互換性を持ちません。
つまり、モジュールの新しいメジャーバージョンは、以前のバージョンとは異なるモジュールパスを
持たなければならないということです。 v2 以降では、メジャーバージョンは(go.mod ファイル内の
module 宣言で示される)モジュールパスの末尾に付与しなければなりません。たとえば、
モジュール github.com/googleapis/gax-go の作者たちが v2 を開発したとき、彼らは
github.com/googleapis/gax-go/v2 という新しいモジュールパスを使いました。 v2 を使いたい
利用者は、パッケージのインポートとモジュールの要求を github.com/googleapis/gax-go/v2 に
変更する必要がありました。
このメジャーバージョンのサフィックスが必要になるという点は、Goモジュールが他の多くの
依存関係管理システムと異なる点の一つです。このサフィックスは、
ダイヤモンド依存関係問題を
解決するために必要とされています。Goモジュールが登場する以前、
gopkg.in は、パッケージの管理者が現在私たちがインポート互換性ルールと
呼んでいるものに従うことを可能にしていました。gopkg.inでは、 gopkg.in/yaml.v1 を
インポートするパッケージと gopkg.in/yaml.v2 をインポートする別のパッケージの両方に
依存していても、この2つの yaml パッケージは異なるインポートパスを持つため、衝突は
起こりません。これはGoモジュールと同様に、バージョンのサフィックスを使っているからです。
gopkg.inはGoモジュールと同じバージョンサフィックスの方式を採用しているため、goコマンドは
gopkg.in/yaml.v2 の中の .v2 を有効なメジャーバージョンサフィックスとして受け入れます。
これはgopkg.inとの互換性のための特別扱いであり、それ以外のドメインでホストされている
モジュールは /v2 のようなスラッシュ付きのサフィックスを必要とします。
メジャーバージョンの戦略
推奨される戦略は、メジャーバージョンのサフィックスにちなんだ名前のディレクトリの中で
v2 以降のモジュールを開発することです。
github.com/googleapis/gax-go @ master branch
/go.mod → module github.com/googleapis/gax-go
/v2/go.mod → module github.com/googleapis/gax-go/v2
このアプローチは、モジュールを認識しないツールとも互換性があります。リポジトリ内の
ファイルパスが、GOPATHモードでの go get が期待するパスと一致するからです。この戦略なら、
すべてのメジャーバージョンを異なるディレクトリで一緒に開発することもできます。
他の戦略として、メジャーバージョンごとに別々のブランチで管理する方法もあります。ただし、
v2 以降のソースコードがリポジトリのデフォルトブランチ(通常は master)にある場合、
GOPATHモードの go コマンドを含む、バージョンを認識しないツールはメジャーバージョン同士を
区別できないことがあります。
本記事の例では、最も互換性の高いメジャーバージョンのサブディレクトリ戦略に従います。 GOPATHモードで開発する利用者を抱えている間は、モジュールの作者はこの戦略に従うことを おすすめします。
v2以降を公開する
本記事では例として github.com/googleapis/gax-go を使います。
$ pwd
/tmp/gax-go
$ ls
CODE_OF_CONDUCT.md call_option.go internal
CONTRIBUTING.md gax.go invoke.go
LICENSE go.mod tools.go
README.md go.sum RELEASING.md
header.go
$ cat go.mod
module github.com/googleapis/gax-go
go 1.9
require (
github.com/golang/protobuf v1.3.1
golang.org/x/exp v0.0.0-20190221220918-438050ddec5e
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b
google.golang.org/grpc v1.19.0
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099
)
$
github.com/googleapis/gax-go の v2 の開発を始めるために、新しい v2/ ディレクトリを
作成し、パッケージをその中にコピーします。
$ mkdir v2
$ cp -v *.go v2
'call_option.go' -> 'v2/call_option.go'
'gax.go' -> 'v2/gax.go'
'header.go' -> 'v2/header.go'
'invoke.go' -> 'v2/invoke.go'
$
次に、現在の go.mod ファイルをコピーし、モジュールパスに /v2 サフィックスを追加して
v2の go.mod ファイルを作成します。
$ cp go.mod v2/go.mod
$ go mod edit -module github.com/googleapis/gax-go/v2 v2/go.mod
$
v2 バージョンは v0 / v1 バージョンとは別のモジュールとして扱われることに注意してください。
両者は同じビルドの中で共存できます。そのため、 v2 以降のモジュールが複数の
パッケージを持つ場合は、それらを新しい /v2 インポートパスを使うように更新すべきです。
そうしなければ、その v2 以降のモジュールは v0 / v1 モジュールに依存したままに
なってしまいます。たとえば、 github.com/my/project へのすべての参照を
github.com/my/project/v2 に更新するには、 find と sed を使えます。
$ find . -type f \
-name '*.go' \
-exec sed -i -e 's,github.com/my/project,github.com/my/project/v2,g' {} \;
$
これで v2 モジュールができましたが、公開する前に実験を重ね、変更を加えたいはずです。
v2.0.0(あるいはプレリリースのサフィックスが付かないバージョン)をリリースするまでは、
新しいAPIを決めていく過程で自由に破壊的変更を加えて開発できます。正式に安定版とする前に
利用者に新しいAPIを試してもらいたい場合は、 v2 のプレリリースバージョンを公開できます。
$ git tag v2.0.0-alpha.1
$ git push origin v2.0.0-alpha.1
$
v2 のAPIに満足し、他に破壊的変更が必要ないと確信できたら、 v2.0.0 にタグを打てます。
$ git tag v2.0.0
$ git push origin v2.0.0
$
この時点で、保守すべきメジャーバージョンが2つになりました。後方互換性のある変更や
バグ修正は、新しいマイナーバージョンやパッチバージョンのリリース(たとえば v1.1.0 や
v2.0.1 など)につながっていきます。
まとめ
メジャーバージョンの変更は開発と保守のオーバーヘッドを生み、下流の利用者に移行のための 投資を強います。プロジェクトが大きいほど、こうしたオーバーヘッドは大きくなる傾向にあります。 メジャーバージョンの変更は、納得できる理由が見つかったときにのみ行うべきです。破壊的変更の 納得できる理由が見つかったら、私たちは複数のメジャーバージョンをmasterブランチの中で 開発することをおすすめします。これはより幅広い既存のツールと互換性があるからです。
v1 以降のモジュールへの破壊的変更は、常に新しい vN+1 モジュールの中で行うべきです。
新しいモジュールがリリースされるということは、保守担当者と、新しいパッケージへの移行が
必要になる利用者の双方にとって、追加の作業が発生するということです。したがって保守担当者は、
安定版としてリリースする前にAPIを十分に検証し、 v1 を超えて破壊的変更が本当に必要かどうかを
慎重に検討すべきです。
By Jean Barkhuysen and Tyler Bui-Palsulich