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