Goのworkspaceを使ったモノレポ構成について考えたメモ
Goのモノレポについては以下の記事にあるようにsingle module
構成とmonorepo module
があるよう。
以下記事内記載の構成を拝借。
single module
.
├── go.mod
├── shop
│ └── shop.go
└── main
└── main.go
上記の例のような感じだとモノレポというか綺麗にモジュール分割されたモノリスというかモジュラーモノリスに近いんじゃないかなという印象。
multi module
.
├── main
│ ├── go.mod
│ ├── go.sum
│ └── main.go
└── shop
├── go.mod
└── shop.go
これはgo.mod
がモジュールごとに用意されていてモジュール同士が独立している。この構成でモジュールごとに別々のサーバーインスタンスにデプロイできると最高なのだがそんなにうまくいかない。
なぜなら、あるモジュールが別のモジュールに依存している場合single module
のように依存関係を解決できないから。そうなるとローカルの開発で困ってしまうのでgo.mod
のreplace
ディレクティブが使われる。
replace ディレクティブ
.
├── moduleA
│ ├── go.mod
│ └── hello.go
└── moduleB
├── go.mod
└── hello.go
モジュールA
module github.com/yamanaka-junichi/workspace-demo/moduleA
go 1.22.3
package modulea
import "fmt"
func HelloA() {
fmt.Println("moduleA")
}
モジュールB
module github.com/yamanaka-junichi/workspace-demo/moduleB
go 1.22.3
replace github.com/yamanaka-junichi/workspace-demo/moduleA => ../moduleA
require github.com/yamanaka-junichi/workspace-demo/moduleA v0.0.0-00010101000000-000000000000
package main
import modulea "github.com/yamanaka-junichi/workspace-demo/moduleA"
func main() {
modulea.HelloA()
}
モジュールBがモジュールAに依存しているが普通にimport文を書いてもそんなモジュールは公開されていないのでインポート文でエラーになる。
そこで、モジュールBのgo.mod
にreplaceディレクティブを書くことでローカルのモジュールを参照することができインポートエラーが消せる。
replaceディレクティブを書いた状態でgo getするとreuireディレクティブにv0.0.0-00010101000000-000000000000
のような形式のバージョンで依存関係が追加される。
このreplaceディレクティブで追加された依存モジュールのバージョンについてあんまり詳しく書いてあるサイトなどを見つけられなかったのだが、v0.0.0-20211209130015-a62dd127ac8a
のようなバージョン記載をしているのをよく見かけた。
これは特定のコミットに紐づくバージョン指定でコミットのタイムスタンプとコミットのハッシュ値が続いている。このバージョン指定は自動でされるのかと思っていたが調べても出てこないので手動でコミットに紐づくバージョン指定をしているのかな?たぶん
なんにせよrequireディレクティブに固定のバージョンの依存モジュールを記載する必要がある。
コミットに紐づく依存関係の追加
これが知りたかった
特定のコミットに紐づく依存関係を追加したければ
- go get <module>@<コミットのハッシュ値>
- go get <module>@<特定のブランチ>
とすればいいらしい。特定のブランチを指定した場合はそのブランチの最新のコミットを取ってくるらしい。
GitHubにpushされていないと依存関係に追加できないので注意。
ここまででGoのマルチモジュールとreplaceディレクティブを使ったモジュールの参照、コミットやタグに紐づいた依存関係の追加についてなんとなくわかった。
- 依存元のモジュールは開発が終わったら安定したブランチ(mainとか)にマージする
- 依存先のモジュールは依存モジュールをバージョン指定して追加する
- ローカルの開発の中で依存先のモジュールを変更する
- 依存先の変更は安定バージョンに含まれていないので変更が反映されない
- なのでタグを適切に打ってGitHubにpushしてgo getしなおさないといけなかった
- それをreplaceディレクティブを使うことでわざわざ変更反映のためにGitHubまでpushしなくても良くなった
- しかし、replaceディレクティブは
go.mod
の変更であり、そのままコミットしてしまうと依存解決に適切なバージョンが使われない - なのでコミット前にreplaceディレクティブは削除することが推奨されている
- ここで疑問なのがreplaceディレクティブを削除した後に依存モジュールをどのバージョンタグで追加すればいいのかということ
- 依存元のモジュールに変更を加えているのでその変更が安定ブランチにマージされるまでその変更を含むバージョンを指定できない気がする
- 安定ブランチではなくフィーチャーブランチやdevelopブランチの最新コミットで依存関係に追加するなども考えられるが安定ブランチで公開されているバージョン以外の依存モジュールを追加した状態で安定ブランチにマージするのはどうなんだろうか
- 結局依存元に変更が伴うようであればその変更が安定ブランチまでマージされて公開されるまで待つしかなくて、それまでのつなぎでreplaceディレクティブ使うみたいな感じなんだろうか
- こういったモジュールのバージョン管理をしっかりやるつもりないならreplaceディレクティブ書いたままpushでいいんじゃなかろうか
- でも、それやるならsingle module構成でええやんという話か
workspace
replaceディレクティブはgo.mod
に記載する必要があり、一時的な記載でpushしないように管理するのが面倒。それを解決するのがworkspace。
go 1.22.3
use (
./moduleA
./moduleB
)
このようなgo.work
を配置することでreplace
ディレクティブがなくともローカルにあるモジュールをimportで参照できるようになる。
簡単すぎてこれで全てを解決してくれるように思っていたがそうでもない。replaceディレクティブの代わりに使うのであればそれはあくまで代わりなのでgo.work
はpushしてはいけない。
go.work
をpushしてしまうとそれはreplaceディレクティブ書いたままでpushするのと同じなのでわざわざworkspaceを使う意味がない気がする。
なので、最終的にpushする前には安定ブランチからリリースされている依存元のモジュールをgo getして依存関係に追加する必要がある。
go.work
を使うメリットはgo.mod
をいじらないので誤ったコミットをすることがなくなったことなど。
ここまでのまとめ
- いまからマルチモジュールを使うならworkspaceを使うと簡単
- replaceディレクティブを使うにしろworkspaceを使うにしろバージョン管理には含めてはいけない
- 依存モジュールが使いたければ依存元のモジュールをmainブランチなどの安定ブランチまでマージして公開するのが先。バージョンタグまで打てるとなお良い
- 依存先モジュールは公開された依存元モジュールをgo getで追加してからpushする
モジュールのバージョン管理
上記でまとめたようにGoにおけるマルチモジュール構成の肝はモジュールのバージョン管理。replaceディレクティブやworkspaceはローカル開発をやりやすくするもので最終的にはバージョン管理されたモジュールを適切に依存関係に追加して開発するようなフローや仕組み、ルールが大事な気がした。
ちなみに、マルチモジュールの構成でtagを打つ時moduleA/v1.0.0
のような形式でタグを打てばいいらしい。ということといい感じにタグを打つ開発体制について以下の記事が参考になる。
プライベートリポジトリの参照
プライベートリポジトリを参照しにくいという問題があるらしい。確かに検証している中以下のようなエラーになった。
go get github.com/yamanaka-junichi/workspace-demo/moduleA@c298635
go: github.com/yamanaka-junichi/workspace-demo/moduleA@c298635: invalid version: git ls-remote -q origin in /Users/work/go/pkg/mod/cache/vcs/6032bbf12bdecbb2c994a4c5c8c9bf755af76f9c06a6364230de536f9c1591d8: exit status 128:
fatal: could not read Username for 'https://github.com': terminal prompts disabled
Confirm the import path was entered correctly.
If this is a private repository, see https://golang.org/doc/faq#git_https for additional information.
.gitconfig
に以下を追加で解決した。
[url "ssh://git@github.com/"]
insteadOf = https://github.com/
でどうするか
複数チームでモノレポなりマルチモジュールなGoリポジトリを開発していくならちゃんとバージョン管理するのがいい気がする。
開発初期でガンガン修正入ったり、そもそもモジュールがまだなかったりする中でこういったバージョン管理ちゃんとするのも難しい気がした。複数チームで開発する予定もないのであればバージョン管理はこの際やらないという選択をとってreplaceディレクティブ書いたままやgo.work
をpushでもいい気がした。でもそれやるならモノレポのメリットであるモジュール間の疎結合が薄れる気がしたのでモノリスかモジュラーモノリスとかのがいんじゃなかろうか。
参考
go mod tidy
workspaceにおける以下のようなissuneがまだオープンのまま。なんとなくバージョン管理を当たり前のようにやっているGoogle側とバージョン未公開のモジュールを含んだworkspaceでgo mod tidy
するとGitHubに取りにいっちゃうのなんとかしてよっていう人たちで認識の齟齬が生まれているような印象を受けた。
追記
バージョン管理頑張らないならworkspace使わないで各モジュールのgo.mod
にreplaceディレクティブ書くのがいい気がする。上のissueにもあるけどgo mod tidy
で公開していないモジュールに依存していると失敗してしまう。