はじめに

この投稿はシリーズの第4部です。

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

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

まだ実験段階にあるプロジェクト — メジャーバージョン v0 — では、ユーザーは時折の破壊的変更を期待しています。安定していると宣言されたプロジェクト — メジャーバージョン v1 以上 — では、破壊的変更は新しいメジャーバージョンで行う必要があります。この投稿では、メジャーバージョンのセマンティクス、新しいメジャーバージョンの作成と公開、モジュールの複数のメジャーバージョンを維持する方法について探ります。

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

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

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

定義によれば、パッケージの新しいメジャーバージョンは前のバージョンと後方互換性がありません。これは、モジュールの新しいメジャーバージョンは前のバージョンとは異なるモジュールパスを持たなければならないことを意味します。v2から、メジャーバージョンはモジュールパスの末尾に表示される必要があります(moduleステートメントでgo.modファイルに宣言されます)。たとえば、モジュール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コマンドは.v2gopkg.in/yaml.v2で有効なメジャーバージョンサフィックスとして受け入れます。これはgopkg.inとの互換性のための特別なケースです: 他のドメインでホストされているモジュールは、/v2のようなスラッシュサフィックスが必要です。

メジャーバージョン戦略

推奨される戦略は、v2+モジュールをメジャーバージョンサフィックスにちなんで名付けられたディレクトリで開発することです。

  1. github.com/googleapis/gax-go @ master branch
  2. /go.mod module github.com/googleapis/gax-go
  3. /v2/go.mod module github.com/googleapis/gax-go/v2

このアプローチは、モジュールを認識していないツールと互換性があります: リポジトリ内のファイルパスは、go getGOPATHモードで期待されるパスと一致します。この戦略により、すべてのメジャーバージョンを異なるディレクトリで一緒に開発することができます。

他の戦略では、メジャーバージョンを別のブランチに保持することがあります。しかし、v2+のソースコードがリポジトリのデフォルトブランチ(通常はmaster)にある場合、バージョンを認識していないツール — goコマンドをGOPATHモードで含む — はメジャーバージョンを区別できないかもしれません。

この投稿の例は、メジャーバージョンサブディレクトリ戦略に従います。これは最も互換性が高いためです。モジュールの著者は、GOPATHモードで開発しているユーザーがいる限り、この戦略に従うことをお勧めします。

v2以降の公開

この投稿ではgithub.com/googleapis/gax-goを例として使用します:

  1. $ pwd
  2. /tmp/gax-go
  3. $ ls
  4. CODE_OF_CONDUCT.md call_option.go internal
  5. CONTRIBUTING.md gax.go invoke.go
  6. LICENSE go.mod tools.go
  7. README.md go.sum RELEASING.md
  8. header.go
  9. $ cat go.mod
  10. module github.com/googleapis/gax-go
  11. go 1.9
  12. require (
  13. github.com/golang/protobuf v1.3.1
  14. golang.org/x/exp v0.0.0-20190221220918-438050ddec5e
  15. golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3
  16. golang.org/x/tools v0.0.0-20190114222345-bf090417da8b
  17. google.golang.org/grpc v1.19.0
  18. honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099
  19. )
  20. $
  1. ``````bash
  2. $ mkdir v2
  3. $ cp -v *.go v2
  4. 'call_option.go' -> 'v2/call_option.go'
  5. 'gax.go' -> 'v2/gax.go'
  6. 'header.go' -> 'v2/header.go'
  7. 'invoke.go' -> 'v2/invoke.go'
  8. $
  9. `

次に、現在のgo.modファイルをコピーし、モジュールパスに/v2サフィックスを追加してv2 go.modファイルを作成します:

  1. $ cp go.mod v2/go.mod
  2. $ go mod edit -module github.com/googleapis/gax-go/v2 v2/go.mod
  3. $

v2バージョンはv0 / v1バージョンとは別のモジュールとして扱われることに注意してください: 両方が同じビルド内で共存することができます。したがって、v2+モジュールに複数のパッケージがある場合は、それらを新しい/v2インポートパスを使用するように更新する必要があります: さもなければ、v2+モジュールはv0 / v1モジュールに依存することになります。たとえば、すべてのgithub.com/my/project参照をgithub.com/my/project/v2に更新するには、findsedを使用できます:

  1. $ find . -type f \
  2. -name '*.go' \
  3. -exec sed -i -e 's,github.com/my/project,github.com/my/project/v2,g' {} \;
  4. $

これでv2モジュールができましたが、リリースを公開する前に実験して変更を加えたいと思います。v2.0.0(またはプレリリースサフィックスのない任意のバージョン)をリリースするまで、私たちは新しいAPIに関して決定する際に開発し、破壊的変更を行うことができます。新しいAPIを正式に安定させる前に、ユーザーが新しいAPIを試すことができるようにしたい場合は、v2プレリリースバージョンを公開できます:

  1. $ git tag v2.0.0-alpha.1
  2. $ git push origin v2.0.0-alpha.1
  3. $

v2 APIに満足し、他の破壊的変更が必要ないことが確信できたら、v2.0.0をタグ付けできます:

  1. $ git tag v2.0.0
  2. $ git push origin v2.0.0
  3. $

その時点で、維持すべきメジャーバージョンが2つあります。後方互換性のある変更やバグ修正は、新しいマイナーおよびパッチリリースにつながります(たとえば、v1.1.0v2.0.1など)。

結論

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

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