> Source: https://www.ymotongpoo.com/works/goblog-ja/v2-go-modules/


# Goモジュール：v2とその先

[Go Modules: v2 and Beyond](https://go.dev/blog/v2-go-modules) by Jean Barkhuysen and Tyler Bui-Palsulich

## はじめに

この記事はシリーズの第4回です。

* 第1回：[Goモジュールを使う](https://go.dev/blog/using-go-modules)
* 第2回：[Goモジュールへの移行](https://go.dev/blog/migrating-to-go-modules)
* 第3回：[Goモジュールの公開](https://go.dev/blog/publishing-go-modules)
* **第4回：Goモジュール：v2とその先**（本記事）
* 第5回：[モジュールの互換性を保つ](https://go.dev/blog/module-compatibility)

なお、モジュールの開発に関するドキュメントについては [Developing and publishing modules](https://go.dev/doc/modules/developing) を参照してください。

成功したプロジェクトが成熟し、新たな要求が加わってくると、過去の機能や設計上の決定が
意味をなさなくなることがあります。開発者はそこから学んだ教訓を踏まえて、非推奨になった
関数を削除したり、型の名前を変えたり、複雑になったパッケージを扱いやすい単位に分割したり
したくなるかもしれません。しかしこの種の変更は、下流の利用者に新しいAPIへの移行の手間を
強いることになるため、その利益がコストを上回ると慎重に判断できない限り、行うべきではありません。

まだ実験的な段階にあり、メジャーバージョンが `v0` であるプロジェクトについては、
利用者もときおり破壊的変更が入ることを見込んでいます。一方、安定版として宣言された
メジャーバージョン `v1` 以降のプロジェクトでは、破壊的変更は必ず新しいメジャーバージョンとして
行わなければなりません。本記事では、メジャーバージョンの意味論、新しいメジャーバージョンの
作成と公開の方法、そして複数のメジャーバージョンを保守する方法について解説します。

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

モジュールは、Goにおける重要な原則である
[インポート互換性ルール](https://research.swtch.com/vgo-import)を明文化しました。

```
古いパッケージと新しいパッケージが同じインポートパスを持つならば、
新しいパッケージは古いパッケージと後方互換性を持たなければならない。
```

定義上、パッケージの新しいメジャーバージョンは、以前のバージョンと後方互換性を持ちません。
つまり、モジュールの新しいメジャーバージョンは、以前のバージョンとは異なるモジュールパスを
持たなければならないということです。 `v2` 以降では、メジャーバージョンは（`go.mod` ファイル内の
`module` 宣言で示される）モジュールパスの末尾に付与しなければなりません。たとえば、
モジュール `github.com/googleapis/gax-go` の作者たちが `v2` を開発したとき、彼らは
`github.com/googleapis/gax-go/v2` という新しいモジュールパスを使いました。 `v2` を使いたい
利用者は、パッケージのインポートとモジュールの要求を `github.com/googleapis/gax-go/v2` に
変更する必要がありました。

このメジャーバージョンのサフィックスが必要になるという点は、Goモジュールが他の多くの
依存関係管理システムと異なる点の一つです。このサフィックスは、
[ダイヤモンド依存関係問題](https://research.swtch.com/vgo-import#dependency_story)を
解決するために必要とされています。Goモジュールが登場する以前、
[gopkg.in](http://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` を使います。

```shell-session
$ 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/` ディレクトリを
作成し、パッケージをその中にコピーします。

```shell-session
$ 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` ファイルを作成します。

```shell-session
$ 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` を使えます。

```shell-session
$ 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` のプレリリースバージョンを公開できます。

```shell-session
$ git tag v2.0.0-alpha.1
$ git push origin v2.0.0-alpha.1
$
```

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

```shell-session
$ 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

