go module のバージョニング


はじめに

自身で作った go package を公開し、別のプロジェクトで利用されるというケースで、バージョンがどのように扱われるのか調べました。

バージョン指定なし

githubにhelloというリポジトリを作り、そこにパッケージを作成します。

go.mod

module github.com/username/hello

go 1.15
hello.go

package hello

import "fmt"

func Hello() {
    fmt.Println("this is version v0.0.0")
}

パッケージを呼び出す

パッケージを呼び出すためのクライアントを用意します。

$ go mod init hello_client
$ go get -u github.com/username/hello

以下のようなgo.modができます。バージョン(v0.0.0)と参照しているコミット(40fe1c06b3a8)がわかります。

go.mod
module hello_client

go 1.15

require github.com/username/hello v0.0.0-20210123030850-40fe1c06b3a8 // indirect
main.go
package main

import (
    m "github.com/username/hello"
)

func main() {
    m.Hello()
}
$ go run main.go 
this is version v0.0.0

意図通りに実行できました。

特にバージョンについて考慮しないと、masterブランチを参照することがわかりました。例えばこのあとmasterに更新がありその最新版を取得したい場合は、再びgo get -u github.com/username/helloを実行することで最新のコミットを参照するようにgo.modが更新されます。

補足: masterブランチの参照

masterブランチを参照する」というのは多少の語弊があって、正しくは「デフォルトブランチを参照」します。例えばmasterブランチを起点にmainブランチを作り、mainブランチをデフォルトブランチにするように設定が変更された場合、go get -u github.com/username/hellomainブランチを参照します。

v1

バージョンを指定しない場合の問題点は、go get -uを実行したタイミングによって参照される資材が異なってしまう点です。そこで常に同じ資材を参照できるようにバージョン管理をします。

hello.goに変更を加えてプッシュしたら、タグを作成します。

hello.go
package hello

import "fmt"

func Hello() {
    fmt.Println("this is version v1.0.0")
}
$ git tag -a v1.0.0 -m "Version 1.0.0"
$ git push --tags
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 167 bytes | 167.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To https://github.com/username/hello.git
 * [new tag]         v1.0.0 -> v1.0.0

パッケージを呼び出す

先ほど作成したクライアントで、パッケージをアップデートします。

$ go get -u github.com/username/hello

go.modは以下のようになります。

go.mod
module hello_client

go 1.15

require github.com/username/hello v1.0.0

実行してみます。

$ go run main.go 
this is version v1.0.0

タグを打つことで、masterブランチではなくv1.0.0タグを参照するようになりました。
これでmasterブランチで開発を続けても、利用者は常にv1.0.0を参照することができるようになります。

v1.1

マイナーバージョンを一つ上げてタグを作成してみます。手順は、v1の時と同様ですので省きます。

パッケージを呼び出す

クライアントも同じようにパッケージをアップデートして実行してみます。

$ go get -u github.com/username/hello
$ go run main.go 
this is version v1.1.0

参照先がv1.1.0に切り替わったことがわかります。

このようにマイナーアップデートやパッチアップデートが行われたら、それらが最新版として認識されることがわかりました。

// プレフィックスとしてのvは必須のようです。例えば1.2.0のようなタグの切り方はv1の最新版として認識されませんでした。

v1.0.0を参照したいときは明示的にバージョンを指定します。

go get github.com/username/[email protected]

v2

セマンティックバージョニングに従うと、メジャーバージョンのアップデートは互換性のない変更が行われた時に発生します。つまり、v1とv2は自動的にアップデートされるべきでもないし、別物として扱うべきです。

そのため、v1の時と同じようなバージョンのアップデートの仕方をしてもうまくいきません。
(v0 / v1v2 ~ で扱いが変わります。詳しくは、公式ブログを参照:https://blog.golang.org/v2-go-modules)

パッケージ側

hello.go
package hello

import "fmt"

func Hello() {
    fmt.Println("this is version v2.0.0")
}
$ git tag -a v2.0.0 -m "Version 2.0.0"

クライアント側

$ go get -u github.com/username/hello
$ go run main.go 
this is version v1.1.0

ここで -u オプションについて再確認します。メジャーアップデートはされないことがわかります。

The -u flag instructs get to update modules providing dependencies of packages named on the command line to use newer minor or patch releases when available.

また、@2.0.0とサフィックスを指定してもエラーになります。

$ go get github.com/username/[email protected]
go get github.com/username/[email protected]: github.com/username/[email protected]: invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v2

ということで、v2以降のバージョンを認識させるためにパッケージのgo.modを修正して、v2.0.1を作成してみます。

go.mod
module github.com/username/hello/v2

go 1.15
$ git tag -a v2.0.1 -m "Version 2.0.1"

パッケージを呼び出す

パスが変わりましたので、クライアントも呼び出し方を合わせて変更します。

go get -u github.com/username/hello/v2

go.modをみてみると別々のパッケージのように扱われていることがわかります。

go.mod
module hello_client

go 1.15

require (
    github.com/username/hello v1.1.0
    github.com/username/hello/v2 v2.0.1
)
main.go
package main

import (
    m1 "github.com/username/hello"
    m2 "github.com/username/hello/v2"
)

func main() {
    m1.Hello()
    m2.Hello()
}

実行してみると以下のようになります。

$ go run main.go 
this is version v1.1.0
this is version v2.0.1

このようにメジャーバージョンが上がるとパスが別々になり、それぞれのバージョンごとにメンテナンスができるようになります。