Closed12

Goのworkspaceを使ったモノレポ構成について考えたメモ

ぱんだぱんだ

Goのモノレポについては以下の記事にあるようにsingle module構成とmonorepo moduleがあるよう。

https://qiita.com/takashabe/items/c0c8dd8bfdc13fc743aa

以下記事内記載の構成を拝借。

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.modreplaceディレクティブが使われる。

ぱんだぱんだ

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ディレクティブに固定のバージョンの依存モジュールを記載する必要がある。

ぱんだぱんだ

コミットに紐づく依存関係の追加

これが知りたかった

https://stackoverflow.com/questions/53682247/how-to-point-go-module-dependency-in-go-mod-to-a-latest-commit-in-a-repo

特定のコミットに紐づく依存関係を追加したければ

  • 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のような形式でタグを打てばいいらしい。ということといい感じにタグを打つ開発体制について以下の記事が参考になる。

https://qiita.com/takashabe/items/5ef6193a3f92411bf2c5

ぱんだぱんだ

プライベートリポジトリの参照

プライベートリポジトリを参照しにくいという問題があるらしい。確かに検証している中以下のようなエラーになった。

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/

https://go.dev/doc/faq#git_https

ぱんだぱんだ

でどうするか

複数チームでモノレポなりマルチモジュールなGoリポジトリを開発していくならちゃんとバージョン管理するのがいい気がする。

開発初期でガンガン修正入ったり、そもそもモジュールがまだなかったりする中でこういったバージョン管理ちゃんとするのも難しい気がした。複数チームで開発する予定もないのであればバージョン管理はこの際やらないという選択をとってreplaceディレクティブ書いたままやgo.workをpushでもいい気がした。でもそれやるならモノレポのメリットであるモジュール間の疎結合が薄れる気がしたのでモノリスかモジュラーモノリスとかのがいんじゃなかろうか。

ぱんだぱんだ

workspaceにおけるgo mod tidy

以下のようなissuneがまだオープンのまま。なんとなくバージョン管理を当たり前のようにやっているGoogle側とバージョン未公開のモジュールを含んだworkspaceでgo mod tidyするとGitHubに取りにいっちゃうのなんとかしてよっていう人たちで認識の齟齬が生まれているような印象を受けた。

https://github.com/golang/go/issues/50750

ぱんだぱんだ

追記

バージョン管理頑張らないならworkspace使わないで各モジュールのgo.modにreplaceディレクティブ書くのがいい気がする。上のissueにもあるけどgo mod tidyで公開していないモジュールに依存していると失敗してしまう。

このスクラップは4ヶ月前にクローズされました