go moduleのreplaceでハマったこと
初めに
仕事でGoを使ってマイクロサービスシステムを作っていて、次のようにgo.mod
をプロジェクトごと分けるようにしています。
プロジェクト共通で使用するような機能、たとえばロガーなども同様に分けていますが、この際は通常にimport
しているのではなくreplace
を使うようにしています。
.
├── logger
│ └── go.mod
├── utils
│ └── go.mod
├── service1
│ ├── go.mod
│ └── ...
├── service2
│ ├── go.mod
│ └── ...
└── ...
このようにreplace
を使って開発を進めていきましたが、ちょっとした問題に遭遇してしまいました。
この記事はその問題の解決方法の紹介と、もっとよいやり方ないかについて考察していきます。
問題
複数のプロジェクトでreplace
を使っていくと、気付かないうちにreplace
先のプロジェクトもreplace
をしていたことがありました。
たとえばs3 => s2 => s1
というように、s3
がs2
をreplace
していて、s2
がs1
をreplace
している、というケースです。
この場合、s3
では次のようにビルドエラーになります。
[I] s3 ) go run main.go
go: github.com/skanehira/test/mono/s2@v0.0.0 requires
github.com/skanehira/test/mono/s1@v0.0.0: missing go.sum entry; to add it:
go mod download github.com/skanehira/test/mono/s1
s1
がないよ、と怒られています。
replace
はメインモジュール(この場合はs3
のgo.mod)のみ適用されるため、依存先(この場合s2
)のreplace
は適用されず、ローカルにあるs1
が見つからなかったためです。
この仕様はこちらに記載されています。
replace directives only apply in the main module's go.mod file and are ignored in other modules.
対策
このような構成になっている場合は、s3
でもs1
のreplace
を書いてあげることでエラーを回避できます。
// s3のgo.mod
replace github.com/skanehira/test/mono/s1 => ../s1
[I] s3 ) go run main.go
var1
go moduleのしくみ
go moduleはMVS(Minimal version selection)というアルゴリズムを使って、依存モジュールのバージョンを決定しています。
このMVSは、ビルドする上で最小バージョンを選択するようになっています。
以下、公式の例をもとに簡単に説明します。
Main
が依存しているA1.2
とB1.2
はそれぞれC1.3
とC1.4
を依存していますが、選択されるのはC1.4
になります。
バージョンが高い方(C1.4
)が選ばれているのは互換性のことを考慮しているからだと思われます。B1.2
はC1.3
だと動かないけどB1.4
は確実に動く、A1.2
はC1.3
or C1.4
のどちらでも動くはず、という感じかと思います。
Goはセマンティック バージョニングを採用しているので、
ご存じがない方は一度読んでみると上記の説明につながると思います。
replace
の場合は、単純に指定されたモジュールを使うようになります。
以下、公式の例ではR
を選択することになります。
考察
replace
のみ適用可能なのか
なぜメインモジュールのgo moduleはさきほど説明したとおり、モジュールのバージョンを選択するアルゴリズムになっています。
しかしreplace
を使うとバージョンが無視されるため、replace
先のreplace
も許容してしまうとバージョンを決定できないケースが出てきます。決定できる/できない場合をそれぞれ次のケースがあると思います。
- 決定できるケース
A => B => C
- 決定できないケース
A => B, C => D
決定できないケースはreplace
先のB
とC
が並列でD
をreplace
していた場合、どちらのD
を選択すればよいのか決定できない、という問題が置きます。本来ならバージョンを見て決定できるがreplace
はバージョンを無視するのでこのような問題が置きます。
逆に並列ではなく直列でreplace
している場合は決定できます。
このように、次の理由でメインモジュールのみreplace
を適用という仕様になっていると思われます。
- 決定できないケースがある
- 特定ケースのみ
replace
可能は混乱を招きやすい
replace
を使う場面
今回の問題を振り返り、そもそもreplace
先がreplace
しているという構成自体が良くないかなと思います。
ですので、replace
されるもしくは可能性があるモジュールはreplace
を使わないようにする構成を検討する、というのが根本的な対策かなと思っています。
具体的にどのような構成がよいかは答えがないので、この場では考察はしないんですが、普通に開発していたらreplace
先がreplace
していることはまず起きないと思います。
ただ、replace
を使わないと行けない場面もあるかと思います。ぱっと思い付くのは次のケースです。
- 非公開モジュールを使う
- forkして改修したモジュールを使う
1はGitHubに載せているprivateなリポジトリを使う場合ですが、これはgit
の設定を変えればある程度解消されると思います。
詳細はGitHubのprivate repositoryを含んだ場合のGo Modules管理を参照してください。
しかし、モジュールのキャッシュがすぐに更新されるわけではないので、開発が激しい場合はキャッシュを更新するしくみを取り入れる必要があるかと思います。
ちなみに、問題が起きたプロジェクトでreplace
を使ったのはキャッシュの更新と設定が面倒という理由が大きかったです。
2はプロジェクトで必要だけど、修正がなかなか公式に取り込まれない場合、使うことがあるかと思います。
この場合、だいたいライブラリで使われる側なのでreplace
しても今回のような問題が起きることは基本的にないと思います。
最後に
replace
先がreplace
しているという変な構成にしてしまったことで、今回のような問題が起きましたがおかげさまでいろいろ学ぶことができました。
気軽にreplace
を使ってよい場合と、考慮しないといけない場合はあるのでみなさんも使う際は意識してみるとよいかもしれません。
Discussion