Goのmonorepoとworkspace mode

僕は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"
```

サンプルリポジトリは以下になります。

https://github.com/takashabe/workspace-sandbox

## single-module構成

single-moduleは単純でルートに1つだけ `go.mod` がある状態です。身の回りのmonorepo実践者はこの構成にしていることが多く割とメジャーなのかなと思います。
サンプルとして2つのパッケージを用意します。shopパッケージが依存される側でmainパッケージが依存する側です。

```sh:ファイルツリーとgo.mod
$ tree
.
├── go.mod
├── shop
│   └── shop.go
└── main
    └── main.go

$ cat go.mod
module github.com/takashabe/workspace-sandbox/single

go 1.18
```

```go:shop/shop.go
package shop

type Shop struct {
        Name string
}
```

```go:main/main.go
package main

import (
        "fmt"

        "github.com/takashabe/workspace-sandbox/single/shop"
)

func main() {
        s := &shop.Shop{
                Name: "single",
        }
        fmt.Println(s)
}
```

この状態でmainパッケージでgo runするとこんな感じで、もちろん問題なく実行できます。

```sh
$ go run ./main/
&shop.Shop{Name:"single"}⏎
```

どちらのパッケージも同じモジュールに入っているのでshopパッケージを編集したとしても当然変更はすぐ反映されます。

```sh:shop/shop.goを編集してrun
$ 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` が複数ある状態です。ファイル構成はこんな感じになります。

```sh:ファイルツリーと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すると当然実行できます。

```sh
$ cd main
$ go run .
&shop.Shop{Name:"multi", Age:0}
```

multi-moduleではそれぞれが異なるモジュールとなっているのでshopモジュール側で何か編集してもmainモジュール側では明示的に新しいバージョンをgo getするか、go.modのreplaceディレクティブを使う必要があります。
replaceを使う場合は以下のような形になります。

```sh
$ 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` サブコマンドによっても操作が可能です。

```sh
$ 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を編集することなく複数モジュールにまたがる修正が行いやすくなります。

```sh
$ 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` ファイルはリポジトリに含めるべきはないと書かれています。

https://go.googlesource.com/proposal/+/master/design/45713-workspace.md#preventing-files-from-being-checked-in-to-repositories

これはあくまでもmulti-moduleの開発をサポートするものであり、手元で一通り開発が終わったらCIには各モジュールの `go.mod` で依存解決が出来る状態にしておく必要があるということでしょう。

### workspace modeのユースケース

ここまででworkspaceを使うことでmulti-module構成であってもreplace無しで開発が出来ることを見てきました。
この他にいくつかユースケースが示されています。

#### gopls

従来gopls(module mode)は作業ディレクトリ = モジュールディレクトリ(`$GOMOD`)で動くようになっていたため、multi-moduleとの相性がよくありませんでした。vscodeのworkspaceやgoplsのexperimentalWorkspaceModuleオプションで対応出来ていましたが、それがworkspace modeでも動く未来が来そうな気配があります。

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

#### モジュールセットの切り替え

複数のgo.workファイルを用意した上で、それぞれ異なるモジュールをuseすることで簡単にビルドされるアプリケーションを切り替えられるというものです。個人的にはあまりピンと来ていませんが、goコマンドの `-workfile` オプションでgo.workファイルを指定、あるいは使わない(off)ことが選択出来るのでgo.modとreplaceを使ってゴニョるよりは楽かもしれません。

先ほどのshopモジュールを使った例だと以下のような感じです。

```sh
$ 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` と似た感じになっているような気がしますね。

```sh
$ 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
```