Carpe Diem

備忘録

Go modulesで依存モジュールのメジャーバージョンがv2以上の時の対応

背景

依存するモジュールのメジャーバージョンがv2以上の場合に、以下のようにバージョン指定すると

$ go get github.com/xxx/yyy@v2.0.1

次のように怒られます。

require github.com/xxx/yyy: version "v2.0.1" invalid: should be v0 or v1, not v2

今回はこの対応方法について説明します。

環境

  • go 1.16.3

version "vX.X.X" invalid: should be v0 or v1, not v2

原因

go modulesは複数の依存モジュールA, Bが同じ依存モジュールCに依存している場合、ABのgo.modでより新しいバージョンのCを使おうとします。

f:id:quoll00:20210501231353p:plain

メジャーバージョンアップのように後方互換性を持たない場合はどうなるでしょう? そうなると古いメジャーバージョンに依存していたモジュールは期待する動作ができなくなります。

f:id:quoll00:20210501231414p:plain

これの対応方法として、go modulesはimport pathにメジャーバージョンを記入する方針を取っています。

互換性のルールとして

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

ref: research!rsc: Semantic Import Versioning (Go & Versioning, Part 3)

とあるように、同じimport pathなら互換性を保つ。互換性を保たないなら別のimport pathに、ということです。

f:id:quoll00:20210501231518p:plain

しかし現状これがあまり周知されておらず、メジャーバージョンがv2以上にも関わらずimport pathがそのままのモジュールが多数あります。

今回のケースもそれに当たります。

対応方法

これを解決する方法としては以下の3通りがあります。

  1. モジュール側を正しいgo.modに直し、使用側のimport pathを修正する(正攻法)
  2. +incompatibleをつける
  3. 依存モジュールのgo.modを消す

a. モジュール側を正しいgo.modに直し、使用側のimport pathを修正する(正攻法)

モジュール側

go modulesの仕様の通り、モジュール側のgo.modにバージョンをつけます。

v2であれば

module github.com/xxx/yyy

go 1.16

module github.com/xxx/yyy/v2

go 1.16

にします。

使用側

また使用する側もimport pathにv2をつけます。

package main

import "github.com/xxx/yyy"

だったのを

package main

import "github.com/xxx/yyy/v2"

にします。

b. +incompatibleをつける

モジュール側が自分たちの管理でなく、イジれない場合は使用側のみで解決する必要があります。

go.modを以下のようにし

require github.com/xxx/yyy v2.2.0+incompatible

Checksumデータベースの検証時でもコケないよう、GOPRIVATEをつけてgo mod tidyします。

GOPRIVATE=github.com/xxx/yyy go mod tidy

c. 依存モジュールのgo.modを消す

go.modが無い状態であれば、メジャーバージョン指定がv2以上でも勝手に+incompatibleが付きます。
なのでgo.mod未対応なリポジトリであれば気にせず利用できます。GOPRIVATEも不要です。

具体的なコードで検証

依存モジュールが以下のケースの時にどういった状態になるかを一通り確認します。

  1. リリースタグがない
  2. リリースタグがあるがマイナーバージョンしかない
  3. リリースタグがあり、v1.0.0以上v2.0.0未満
  4. リリースタグがあり、v2.0.0以上(go.modなし)
  5. リリースタグがあり、v2.0.0以上(path変更なし)
  6. リリースタグがあり、v2.0.0以上(正攻法)

↓を依存モジュールとして利用します。

github.com

リリースタグがない

モジュール側

masterにコミットをマージしておきます。

使用側

リリースタグがないのでmasterブランチやコミットハッシュを使って指定します。

$ go get github.com/jun06t/go-modules-test@master

以下のようなgo.modファイルになります。

module github.com/jun06t/go-sample/modules

go 1.16

require github.com/jun06t/go-modules-test v0.0.0-20210430152744-b043f1aff7e1

リリースタグがあるがマイナーバージョンしかない

モジュール側

v0.1.0のtagを切ってリリースします。

使用側

リリースタグがあるのでそれを指定します。

$ go get github.com/jun06t/go-modules-test@v0.1.0

以下のようなgo.modファイルになります。

module github.com/jun06t/go-sample/modules

go 1.16

require github.com/jun06t/go-modules-test v0.1.0

リリースタグがあり、v1.0.0以上v2.0.0未満

モジュール側

v1.2.3のtagを切ってリリースします。

使用側

リリースタグがあるのでそれを指定します。

$ go get github.com/jun06t/go-modules-test@v1.2.3

以下のようなgo.modファイルになります。

module github.com/jun06t/go-sample/modules

go 1.16

require github.com/jun06t/go-modules-test v1.2.3

リリースタグがあり、v2.0.0以上(go.modなし)

モジュール側

v2.0.1のtagを切ってリリースします。

使用側

$ go get github.com/jun06t/go-modules-test@v2.0.1

で勝手に+incompatibleが付きます。

module github.com/jun06t/go-sample/modules

go 1.16

require github.com/jun06t/go-modules-test v2.0.1+incompatible

リリースタグがあり、v2.0.0以上(path変更なし)

モジュール側

v2.0.2のtagを切ってリリースします。

使用側

$ go get github.com/jun06t/go-modules-test@v2.0.2

では

invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v2

とエラーが出てできないので、go.modを以下のように直接いじります。

module github.com/jun06t/go-sample/modules

go 1.16

require github.com/jun06t/go-modules-test v2.0.2+incompatible

次にgo mod tidyしますが、そのまま実行するとChecksumデータベースによる検証時に

verifying go.mod: github.com/jun06t/go-modules-test@v2.0.2+incompatible/go.mod: reading https://sum.golang.org/lookup/github.com/jun06t/go-modules-test@v2.0.2+incompatible: 410 Gone
        server response: not found: github.com/jun06t/go-modules-test@v2.0.2+incompatible: invalid version: +incompatible suffix not allowed: module contains a go.mod file, so semantic import versioning is required

とエラーが出るので、Checksum検証の対象外にするためにGOPRIVATEをつけます。

$ GOPRIVATE=github.com/jun06t go mod tidy
go: downloading github.com/jun06t/go-modules-test v2.0.2+incompatible

これで成功です。

リリースタグがあり、v2.0.0以上(正攻法)

モジュール側

go.modに以下のpathを追加します。 ※モジュール側でパッケージ内依存があれば、各パッケージでもimport pathに/v2を付ける必要があります

module github.com/jun06t/go-modules-test/v2

go 1.16

v2.1.0のtagを切ってリリースします。

使用側

import pathに/v2をつけます。

package main

import (
        echo "github.com/jun06t/go-modules-test/v2"
)

func main() {
        echo.Call()
}

go get時もv2をつけます。

$ go get github.com/jun06t/go-modules-test/v2
go: downloading github.com/jun06t/go-modules-test/v2 v2.1.0
go get: added github.com/jun06t/go-modules-test/v2 v2.1.0

以下のようなgo.modファイルになります。

module github.com/jun06t/go-sample/modules

go 1.16

require github.com/jun06t/go-modules-test/v2 v2.1.0

まとめ

v2以上のtagをつけているリポジトリではgo.modやimport pathもそのメジャーバージョンに合わせるようにしましょう。

自分が↑の修正をできないリポジトリであれば、+incompatibleで回避するようにしましょう。

参考