僕はmonorepoが好きで、これまでGoのsingle-module構成とmulti-module構成どちらも実運用してきました。最近ではメルカリShopsがmonorepoを採用して話題になったりとにわかに盛り上がっていて嬉しくあります。
https://engineering.mercari.com/blog/entry/20210817-8f561697cc/
さて、来たるGo 1.18の目玉はやはりGenericsで影に隠れがちですが、multi-moduleの体験を向上させるworkspace modeという機能が入る予定です。
https://github.com/golang/go/issues/45713
この記事ではGoでmonorepoするときの一般的な構成であるsingle-moduleとmulti-module、そしてworkspace modeが入ったあとの世界について見ていきたいと思います。
※ workspace mode自体はまだ頻繁にCLが取り込まれていてリリースされるまでに変更が入る可能性が非常に高いです
また本記事での実行環境は以下の通りです。(GOMOD, GOWORKだけはディレクトリによって異なります)
$ go env
GO111MODULE="on"
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/takashabe/.cache/go-build"
GOENV="/home/takashabe/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/takashabe/dev/pkg/mod"
GONOPROXY="github.com/takashabe/*"
GONOSUMDB="github.com/takashabe/*"
GOOS="linux"
GOPATH="/home/takashabe/dev"
GOPRIVATE="github.com/takashabe/*"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/home/takashabe/dev/src/go.googlesource.com/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/home/takashabe/dev/src/go.googlesource.com/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="devel go1.18-d6c4583ad4 Wed Dec 8 23:38:20 2021 +0000"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/takashabe/dev/src/github.com/takashabe/workspace-sandbox/single/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3359729334=/tmp/go-build -gno-record-gcc-switches"
サンプルリポジトリは以下になります。
single-module構成
single-moduleは単純でルートに1つだけ go.mod
がある状態です。身の回りのmonorepo実践者はこの構成にしていることが多く割とメジャーなのかなと思います。
サンプルとして2つのパッケージを用意します。shopパッケージが依存される側でmainパッケージが依存する側です。
$ tree
.
├── go.mod
├── shop
│ └── shop.go
└── main
└── main.go
$ cat go.mod
module github.com/takashabe/workspace-sandbox/single
go 1.18
package shop
type Shop struct {
Name string
}
package main
import (
"fmt"
"github.com/takashabe/workspace-sandbox/single/shop"
)
func main() {
s := &shop.Shop{
Name: "single",
}
fmt.Println(s)
}
この状態でmainパッケージでgo runするとこんな感じで、もちろん問題なく実行できます。
$ go run ./main/
&shop.Shop{Name:"single"}⏎
どちらのパッケージも同じモジュールに入っているのでshopパッケージを編集したとしても当然変更はすぐ反映されます。
$ git --no-pager diff shop/shop.go
type Shop struct {
Name string
+ Age int
}
$ go run ./main/
&shop.Shop{Name:"single", Age:0}⏎
single-moduleは構成が簡単になりますが複数人で開発しているとconflictが頻繁に起きうる、意図しない影響を他パッケージに与えやすい(壊れやすい)のが欠点だと思います。
multi-module構成
multi-moduleはその名の通り1つのリポジトリ内に go.mod
が複数ある状態です。ファイル構成はこんな感じになります。
$ main tree
.
├── main
│ ├── go.mod
│ ├── go.sum
│ └── main.go
└── shop
├── go.mod
└── shop.go
$ cat shop/go.mod
module github.com/takashabe/workspace-sandbox/multi/shop
go 1.18
$ cat main/go.mod
module github.com/takashabe/workspace-sandbox/multi/main
go 1.18
require github.com/takashabe/workspace-sandbox/multi/shop v0.0.0-20211209130015-a62dd127ac8a
この状態でgo runすると当然実行できます。
$ cd main
$ go run .
&shop.Shop{Name:"multi", Age:0}
multi-moduleではそれぞれが異なるモジュールとなっているのでshopモジュール側で何か編集してもmainモジュール側では明示的に新しいバージョンをgo getするか、go.modのreplaceディレクティブを使う必要があります。
replaceを使う場合は以下のような形になります。
$ git --no-pager diff .
diff --git a/multi/main/go.mod b/multi/main/go.mod
index f31f08e..a52090c 100644
--- a/multi/main/go.mod
+++ b/multi/main/go.mod
@@ -3,3 +3,5 @@ module github.com/takashabe/workspace-sandbox/multi/main
go 1.18
require github.com/takashabe/workspace-sandbox/multi/shop v0.0.0-20211209130015-a62dd127ac8a
+
+replace github.com/takashabe/workspace-sandbox/multi/shop => ../shop
diff --git a/multi/shop/shop.go b/multi/shop/shop.go
index 998cf07..baeeae3 100644
--- a/multi/shop/shop.go
+++ b/multi/shop/shop.go
@@ -1,6 +1,7 @@
package shop
type Shop struct {
- Name string
- Age int
+ Name string
+ Age int
+ Address string
}
このようにmulti-moduleではモジュール間が粗結合になることで結果的に開発チームを分割しやすくなる特徴がありますが、複数モジュールにまたがった変更を行うのが単純に面倒くさくなります。
かといってreplaceを常時使うようにするとsingle-moduleで良いやんとなるので、手前味噌ですが実際の開発現場では以下のような仕組みを導入して少しでも粗結合の恩恵を受けつつバージョン管理しやすい形で開発を行っていました。
https://qiita.com/takashabe/items/5ef6193a3f92411bf2c5
workspace modeの登場
ここまでは従来のおさらいでやっとここから本題で未来の話です。workspace modeによってどんなことが可能になるのかサクッと見ていきましょう。
まずworkspaceは go.work
という専用のファイルによって管理されます。直接編集しても良いですが新しく追加される go work
サブコマンドによっても操作が可能です。
$ go work init
$ go work use -r .
$ cat go.work
go 1.18
use (
main
shop
)
$ tree
.
├── go.work
├── main
│ ├── go.mod
│ ├── go.sum
│ └── main.go
└── shop
├── go.mod
└── shop.go
workspace modeが有効になっていると、workspaceに含まれる複数のモジュールはそれぞれworkspace内のモジュールで依存解決が行われるようになります。つまり明示的に依存先をgo getしたりreplaceするといった手間が省略されます。(workspaceよりreplaceが優先されるといった挙動はある)
これにより次のようにgo.modを編集することなく複数モジュールにまたがる修正が行いやすくなります。
$ git --no-pager diff .
diff --git a/workspace/shop/shop.go b/workspace/shop/shop.go
index baeeae3..711963b 100644
--- a/workspace/shop/shop.go
+++ b/workspace/shop/shop.go
@@ -4,4 +4,5 @@ type Shop struct {
Name string
Age int
Address string
+ Tel string
}
$ go run ./main/
&shop.Shop{Name:"workspace", Age:0, Address:"", Tel:""}⏎
このようにreplaceを使うことなくshopモジュールの修正が反映されました。またmainモジュール内ではなくルートのworkspaceディレクトリで go run
出来ていることもポイントになると思います。
go.workファイルの扱い
proposalでは go.work
ファイルはリポジトリに含めるべきはないと書かれています。
これはあくまでもmulti-moduleの開発をサポートするものであり、手元で一通り開発が終わったらCIには各モジュールの go.mod
で依存解決が出来る状態にしておく必要があるということでしょう。
workspace modeのユースケース
ここまででworkspaceを使うことでmulti-module構成であってもreplace無しで開発が出来ることを見てきました。
この他にいくつかユースケースが示されています。
gopls
従来gopls(module mode)は作業ディレクトリ = モジュールディレクトリ($GOMOD
)で動くようになっていたため、multi-moduleとの相性がよくありませんでした。vscodeのworkspaceやgoplsのexperimentalWorkspaceModuleオプションで対応出来ていましたが、それがworkspace modeでも動く未来が来そうな気配があります。
モジュールセットの切り替え
複数のgo.workファイルを用意した上で、それぞれ異なるモジュールをuseすることで簡単にビルドされるアプリケーションを切り替えられるというものです。個人的にはあまりピンと来ていませんが、goコマンドの -workfile
オプションでgo.workファイルを指定、あるいは使わない(off)ことが選択出来るのでgo.modとreplaceを使ってゴニョるよりは楽かもしれません。
先ほどのshopモジュールを使った例だと以下のような感じです。
$ go run -workfile=off ./main/
go: cannot find main module, but found .git/config in /home/takashabe/dev/src/github.com/takashabe/workspace-sandbox
to create a module there, run:
cd .. && go mod init
$ go run -workfile=$PWD/go.work ./main/
&shop.Shop{Name:"workspace", Age:0, Address:"", Tel:""}⏎
最後に
Goでmonorepoする際の一般的な構成とGo 1.18で導入予定のworkspace modeについて見てきました。
workspace modeはproposalでも元々goplsについて言及されていたりとツールとの相性が良くなるように考えられていそうです。goplsだけでなく静的解析ツールを作るときにも面白いことが出来るようになっていくのかなと期待しています。
これまでGo moduleは完全に雰囲気で使ってきていた(いる)ので何か誤りがあれば編集リクエストを頂ければ幸いです!
余談ですがworkspace modeが有効になっているかは GOWORK
環境変数を見ると良さそうです。この辺は GOMOD
と似た感じになっているような気がしますね。
$ pwd
/home/takashabe/dev/src/github.com/takashabe/workspace-sandbox/workspace/main
$ go env GOWORK
/home/takashabe/dev/src/github.com/takashabe/workspace-sandbox/workspace/go.work