Goモジュールの公開

Publishing Go Modules by Tyler Bui-Palsulich

はじめに

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

注: モジュールの開発に関するドキュメントは Developing and publishing modules を参照してください。

この記事では、他のモジュールが依存できるようなモジュールをどのように書き、公開するかを解説します。

なお、この記事が扱うのは v1 までの開発の話です。 v2 に興味がある方は Goモジュール:v2とその先 を参照してください。

この記事の例では Git を使用しています。 MercurialBazaar などもサポートされています。

プロジェクトのセットアップ

この記事のために、例として使う既存のプロジェクトが必要です。そこで、 Goモジュールを使う の記事の最後に登場したファイル群から始めましょう。

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote/v3 v3.1.0

$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

$ cat hello.go
package hello

import "rsc.io/quote/v3"

func Hello() string {
    return quote.HelloV3()
}

func Proverb() string {
    return quote.Concurrency()
}

$ cat hello_test.go
package hello

import (
    "testing"
)

func TestHello(t *testing.T) {
    want := "Hello, world."
    if got := Hello(); got != want {
        t.Errorf("Hello() = %q, want %q", got, want)
    }
}

func TestProverb(t *testing.T) {
    want := "Concurrency is not parallelism."
    if got := Proverb(); got != want {
        t.Errorf("Proverb() = %q, want %q", got, want)
    }
}

$

次に、新しく git リポジトリを作成し、最初のコミットを追加します。もし自分自身のプロジェクトを 公開するのであれば、忘れずに LICENSE ファイルを含めてください。 go.mod があるディレクトリに 移動して、リポジトリを作成します。

$ git init
$ git add LICENSE go.mod go.sum hello.go hello_test.go
$ git commit -m "hello: initial commit"
$

セマンティックバージョンとモジュール

go.mod に記載された必須モジュールにはそれぞれ セマンティックバージョン があり、これはそのモジュールをビルドするために使用する依存関係の最小バージョンを表します。

セマンティックバージョンは vMAJOR.MINOR.PATCH という形式を取ります。

  • モジュールの公開APIに後方互換性のない変更を加えたときは、 MAJOR バージョンを増やします。これは本当にやむを得ないときにのみ行うべきです。
  • 依存関係の変更や、新しい関数、メソッド、構造体のフィールド、型の追加のように、 APIに後方互換性のある変更を加えたときは、 MINOR バージョンを増やします。
  • バグ修正のように、モジュールの公開APIや依存関係に影響を与えないささいな変更を加えたあとは、 PATCH バージョンを増やします。

ハイフンとドット区切りの識別子を付け加えることで(たとえば v1.0.1-alphav2.2.2-beta.2 のように) プレリリースバージョンを指定できます。 go コマンドはプレリリースバージョンよりも通常のリリースを 優先するため、モジュールに通常のリリースが存在する場合、利用者はプレリリースバージョンを明示的に 指定する必要があります(たとえば go get example.com/hello@v1.0.1-alpha のように)。

v0 のメジャーバージョンとプレリリースバージョンは後方互換性を保証しません。これらは、 利用者に対して安定性を約束する前にAPIを洗練させるためのものです。一方で v1 以降のメジャー バージョンでは、そのメジャーバージョン内での後方互換性が要求されます。

go.mod で参照されるバージョンは、リポジトリに明示的にタグ付けされたリリース(たとえば v1.5.2)であることも、特定のコミットに基づいた疑似バージョン (たとえば v0.0.0-20170915032832-14c0d48ead0c)であることもあります。疑似バージョンは プレリリースバージョンの特殊な一種です。疑似バージョンは、セマンティックバージョンのタグを 一つも公開していないプロジェクトに依存する必要がある場合や、まだタグ付けされていないコミットに 対して開発する場合に便利ですが、利用者は疑似バージョンが安定した、十分にテストされたAPIを 提供しているとは思わないようにすべきです。明示的なバージョンでモジュールにタグを付けることは、 特定のバージョンが十分にテストされ利用可能な状態であることを利用者に伝える意味があります。

一度リポジトリにバージョンのタグを付け始めたら、モジュールを開発するにつれて新しいリリースに タグを付け続けることが重要です。利用者が( go get -ugo get example.com/hello によって) モジュールの新しいバージョンを要求すると、 go コマンドは、そのバージョンが数年前のもので 主開発ブランチから多くの変更分だけ遅れていたとしても、利用可能な最大のセマンティックリリース バージョンを選択します。新しいリリースにタグを付け続けることで、継続的な改善を利用者に届けられます。

リポジトリからバージョンのタグを削除してはいけません。あるバージョンにバグやセキュリティ上の 問題を見つけたら、新しいバージョンをリリースしてください。もし削除したバージョンに依存している 人がいれば、そのビルドは失敗するかもしれません。同様に、一度あるバージョンをリリースしたら、 それを変更したり上書きしたりしないでください。モジュールミラーとチェックサムデータベース は、モジュールとそのバージョン、署名済みの暗号学的ハッシュ値を保存しており、あるバージョンの ビルドが時間が経っても再現可能であることを保証しています。

v0:最初の不安定バージョン

モジュールに v0 のセマンティックバージョンでタグを付けてみましょう。 v0 バージョンは いかなる安定性も保証しないため、ほとんどすべてのプロジェクトは公開APIを洗練させていく間、 v0 から始めるべきです。

新しいバージョンにタグを付けるにはいくつかの手順があります。

  1. go mod tidy を実行し、モジュールに溜まってしまった不要な依存関係を取り除きます。
  2. 最後にもう一度 go test ./... を実行し、すべてが正しく動作することを確認します。
  3. git tag を使ってプロジェクトに新しいバージョンのタグを付けます。
  4. 新しいタグを元となるリポジトリにプッシュします。
$ go mod tidy
$ go test ./...
ok      example.com/hello       0.015s
$ git add go.mod go.sum hello.go hello_test.go
$ git commit -m "hello: changes for v0.1.0"
$ git tag v0.1.0
$ git push origin v0.1.0
$

これで他のプロジェクトが example.com/hellov0.1.0 に依存できるようになりました。 自分のモジュールについては、 go list -m example.com/hello@v0.1.0 を実行して最新バージョンが 利用可能になっているかを確認できます(この例のモジュールは実在しないため、利用可能な バージョンはありません)。もしすぐに最新バージョンが見えなかった場合、かつGoモジュール プロキシ(Go 1.13以降のデフォルト)を使っているのであれば、プロキシが新しいバージョンを 読み込むまで数分待ってから再度試してみてください。

公開APIに何かを追加した場合、 v0 モジュールに破壊的変更を加えた場合、あるいは依存関係の いずれかのマイナーバージョンやメジャーバージョンを上げた場合は、次のリリースで MINOR バージョンを増やします。たとえば、 v0.1.0 の次のリリースは v0.2.0 になります。

既存のバージョンのバグを修正した場合は、 PATCH バージョンを増やします。たとえば、 v0.1.0 の次のリリースは v0.1.1 になります。

v1:最初の安定バージョン

モジュールのAPIが安定していると確信できたら、 v1.0.0 をリリースできます。 v1 の メジャーバージョンは、モジュールのAPIに互換性のない変更が加えられることはないと 利用者に伝えるものです。利用者は新しい v1 のマイナーバージョンやパッチバージョンに アップグレードでき、そのコードが壊れることはないはずです。関数やメソッドのシグネチャは 変更されず、エクスポートされた型が削除されることもありません。APIに変更がある場合、 それは(たとえば構造体に新しいフィールドを追加するなど)後方互換性のあるものとなり、 新しいマイナーリリースに含まれます。バグ修正がある場合(たとえばセキュリティ修正など)は、 パッチリリースに(あるいはマイナーリリースの一部として)含まれます。

後方互換性を保つことが、ときにぎこちないAPIにつながることがあります。それでも構いません。 不完全なAPIのほうが、利用者の既存のコードを壊すよりもましだからです。

標準ライブラリの strings パッケージは、API全体の一貫性を犠牲にしてでも後方互換性を 保つことの好例です。

  • Split は文字列をセパレータで区切ったすべての 部分文字列に分割し、そのセパレータの間にある部分文字列のスライスを返します。
  • SplitN を使うと、返される部分文字列の数を 制御できます。

しかし Replace は( Split とは異なり)先頭から 数えて何個の文字列を置換するかという個数を引数に取ります。

SplitSplitN があるのだから、 ReplaceReplaceN のような関数を期待するかも しれません。しかし、私たちは既存の Replace を、呼び出し元を壊さないという約束を破らずに 変更することはできませんでした。そこでGo 1.12で新しい関数 ReplaceAll を追加しました。結果として、 SplitReplace の振る舞いが異なるという、いくぶん奇妙なAPIになりましたが、その 不整合は破壊的変更よりもましなものです。

example.com/hello のAPIに満足しており、最初の安定バージョンとして v1 をリリース したいとしましょう。

v1 へのタグ付けは v0 バージョンへのタグ付けと同じ手順です。 go mod tidygo test ./... を実行し、バージョンにタグを付け、そのタグを元となるリポジトリに プッシュします。

$ go mod tidy
$ go test ./...
ok      example.com/hello       0.015s
$ git add go.mod go.sum hello.go hello_test.go
$ git commit -m "hello: changes for v1.0.0"
$ git tag v1.0.0
$ git push origin v1.0.0
$

この時点で、 example.com/hellov1 のAPIは確定しました。これは私たちのAPIが 安定していて、誰もが安心して使ってよいということをすべての人に伝えるものです。

まとめ

この記事では、セマンティックバージョンでモジュールにタグを付ける手順と、いつ v1 を リリースすべきかを説明しました。今後の記事では v2 以降のモジュールをどのように 保守し、公開するかを扱う予定です。

Goにおける依存関係管理の将来をより良いものにするためのフィードバックをお寄せいただける方は、 バグ報告または経験談(experience report)を お送りください。

これまでいただいたフィードバックと、Goモジュールの改善に関するご協力に感謝します。

By Tyler Bui-Palsulich