The Go Blog

Go Modules: v2 以降

Jean Barkhuysen と Tyler Bui-Palsulich
2019年11月7日

はじめに

この記事はシリーズのパート4です。

注: モジュールの開発に関するドキュメントについては、モジュールの開発と公開をご覧ください。

成功したプロジェクトが成熟し、新しい要件が追加されると、過去の機能や設計上の決定が意味をなさなくなることがあります。開発者は、非推奨の関数を削除したり、型名を変更したり、複雑なパッケージを管理しやすい部分に分割したりして、学んだ教訓を統合したいと考えるかもしれません。これらの種類の変更は、ダウンストリームユーザーがコードを新しいAPIに移行するために労力を必要とするため、利点がコストを上回ることを慎重に検討せずに変更すべきではありません。

実験段階にあるプロジェクト (メジャーバージョン v0) の場合、ユーザーは時折、互換性を破る変更を予想します。安定していると宣言されたプロジェクト (メジャーバージョン v1 以上) の場合、互換性を破る変更は新しいメジャーバージョンで行う必要があります。この記事では、メジャーバージョンのセマンティクス、新しいメジャーバージョンを作成および公開する方法、モジュールの複数のメジャーバージョンを維持する方法について説明します。

メジャーバージョンとモジュールパス

モジュールは、Go の重要な原則であるimport compatibility rule (インポート互換性ルール)を形式化しました。

If an old package and a new package have the same import path,
the new package must be backwards compatible with the old package.

定義上、パッケージの新しいメジャーバージョンは以前のバージョンと下位互換性がありません。これは、モジュールの新しいメジャーバージョンは、以前のバージョンとは異なるモジュールパスを持つ必要があることを意味します。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-gov2 の開発を開始するには、新しい 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 に更新するには、findsed を使用できます。

$ 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 など) につながります。

まとめ

メジャーバージョンの変更は、開発および保守のオーバーヘッドをもたらし、ダウンストリームユーザーが移行するために投資を必要とします。プロジェクトが大きくなればなるほど、これらのオーバーヘッドは大きくなる傾向があります。メジャーバージョンの変更は、説得力のある理由が特定された後にのみ行われるべきです。互換性を破る変更に対して説得力のある理由が特定されたら、より広範な既存ツールと互換性があるため、マスターブランチで複数のメジャーバージョンを開発することをお勧めします。

v1+ モジュールに対する互換性を破る変更は、常に新しい vN+1 モジュールで行われるべきです。新しいモジュールがリリースされると、それはメンテナーと、新しいパッケージに移行する必要があるユーザーにとって追加の作業を意味します。したがって、メンテナーは安定版をリリースする前に API を検証し、v1 を超える互換性を破る変更が本当に必要かどうかを慎重に検討すべきです。

次の記事: Go は10周年
前の記事: Go 1.13 でのエラーの扱い
ブログインデックス