Goブログ

Goモジュール:v2とその先

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

はじめに

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

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

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

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

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

モジュールは、Goにおける重要な原則である、インポート互換性ルールを形式化しました。

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モジュールが他のほとんどの依存関係管理システムと異なる点の1つです。サフィックスは、ダイヤモンド依存性の問題を解決するために必要です。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に更新するには、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.0v2.0.1など)。

結論

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

v1+モジュールへの破壊的な変更は、常に新しいvN+1モジュールで行う必要があります。新しいモジュールがリリースされるということは、メンテナーと新しいパッケージに移行する必要があるユーザーにとって、追加の作業が必要になることを意味します。したがって、メンテナーは安定版リリースを行う前にAPIを検証し、v1以降で破壊的な変更が本当に必要かどうかを慎重に検討する必要があります。

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