なぜgo.workはリポジトリにコミットすべきではないのか

## 本記事の背景と目的
　go言語のWorkspace modeを導入した際に生成される`go.work`ファイルは，リポジトリにコミットすべきでないとの指摘がある[[1](https://poyo.hatenablog.jp/entry/2022/12/05/090000)][[2](https://future-architect.github.io/articles/20220216a/)]．この点について[Proposal](https://github.com/golang/go/issues/53502)でも盛んに議論されており，[公式リファレンス](https://go.dev/ref/mod#go-work-file)では`go.work`をリポジトリにコミットしないよう推奨されている．
 　本記事では，`go.work`をリポジトリにコミットすると困りうるパターンを検証し，`go.work`をコミットすべきでない理由を明らかにする．

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

https://go.dev/ref/mod#go-work-file

## Workspace modeとは
　Go言語のWorkspace modeとは，Go 1.18以降で利用可能なマルチモジュール開発を支援する機能である．開発するモジュールから外部のモジュールを取り込むとき，通常はGitHub等で既に公開されているモジュールを`go install`などを使って取り込む必要がある．
 　一方，複数モジュールを同時開発し，それらが依存関係にある場合，公開前でローカル上にある開発中モジュールを使ってテストしたい場合がある．このとき，`go.mod`のreplaceディレクティブでモジュールの向き先を一時的にローカルファイルに振り向けることができるが，`go.mod`ファイルを汚してしまう懸念がある．これに対してWorkspace modeを使うことで，`go.mod`を汚さずに同様のことが実現できる．このWorkspace modeを設定する際に生成されるのが`go.work`ファイルであるが，その設定方法はチュートリアルを参照されたい．

https://go.dev/doc/modules/gomod-ref#replace

https://go.dev/doc/tutorial/workspaces
 
## Workspace modeとmonorepo
　複数のモジュールやサービスを単一のリポジトリで管理するmonorepoを採用する場合，Workspace modeを利用するケースが考えられる．類似の仕組みとして，Yarn Workspacesがある．workspaceを用いることで，monorepo上の異なるモジュール同士がシームレスに行き来できる．
 　また，Go言語のLanguageServerであるgoplsを動かし，エディタで補完や定義のジャンプ機能を使いたい場合，プロジェクトのルート配下に`go.mod`ファイルが必要になる．monorepo構成の場合は，各サブディレクトリ内に`go.mod`が配置されるため，goplsが機能せず困る．一方で，Workspace modeを設定し，ルート配下に`go.work`が存在すればgoplsは機能する．（Workspace mode以外にも，VS CodeのMulti-root Workspacesを使う方法もある．）
　monorepoにおいてWorkspace modeはメリットがあり，`go.work`をリポジトリにコミットして開発者間で共用したくなるが，そうすべきか否かは[Issue](https://github.com/golang/go/issues/45713)や先述の[Proposal](https://github.com/golang/go/issues/53502)で議論を呼んでいる．

## `go.work`ファイルをコミットすべきでない理由
　ここで，[公式リファレンス](https://go.dev/ref/mod#go-work-file)で示されている`go.work`をコミットすべきでない理由を下記に引用する．

> - A checked-in go.work file might override a developer’s own go.work file from a parent directory, causing confusion when their use directives don’t apply.
> - A checked-in go.work file may cause a continuous integration (CI) system to select and thus test the wrong versions of a module’s dependencies. CI systems should generally not be allowed to use the go.work file so that they can test the behavior of the module as it would be used when required by other modules, where a go.work file within the module has no effect.

 DeepLによる翻訳も掲載する．
> - チェックインされた go.work ファイルは、親ディレクトリにある開発者自身の go.work ファイルを上書きするかもしれません。
> - チェックインされた go.work ファイルは、 継続的インテグレーション (CI) システムで、 モジュールの依存関係を間違ったバージョンでテストしてしまうかもしれません。CI システムは一般的に、go.work ファイルを使うことを許可されるべきではなく、モジュール内の go.work ファイルが影響を及ぼさない、他のモジュールから要求されたときに使われるようなモジュールの挙動をテストすることができます。 

　本記事では特に２点目について，具体的にどういったケースで困るのかを検証する．

## `go.work`ファイルにおけるモジュールの依存関係への影響

### 検証内容と結果
　例えば下記のようなmonorepo構成を想定する．`monorepo-hoge`には`module-huga`と`module-piyo`の２つのモジュールが含まれているが，これらに明確な依存関係はない．
 
``` sh
monorepo-hoge/
├── .git
├── go.work # checked-in go.work file
├── module-huga/
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── module-piyo/
    ├── go.mod
    ├── go.sum
    └── main.go
```
　`module-huga`は，インターネット上に公開されている或るモジュール（例えば github.com/samber/lo v1.**46**.0 ）を取り込み，`module-piyo`では同モジュールの別バージョン（github.com/samber/lo v1.**47**.0）を取り込むものとする．この場合に，それぞれの`go.mod`は下記のようになる．

 ``` go:module-huga/go.mod
 module monorepo-hoge/module-huga

go 1.23.0

require github.com/samber/lo v1.46.0

require golang.org/x/text v0.16.0 // indirect
 ```
 
 ``` go:module-piyo/go.mod
 module monorepo-hoge/module-piyo

go 1.23.0

require github.com/samber/lo v1.47.0

require golang.org/x/text v0.16.0 // indirect
 ```

　今回は検証用に，取り込んでいるモジュールのバージョンを出力するコードを各々の`main.go`に記述する．（[参考元](https://stackoverflow.com/questions/62009264/golang-how-to-display-modules-version-from-inside-of-code)）

 ```go:module-huga/main.goおよびgo:module-piyo/main.go
 package main

import (
	"fmt"
	"log"

	"runtime/debug"
	"github.com/samber/lo"
)

func main() {
	bi, ok := debug.ReadBuildInfo()
	if !ok {
		log.Printf("Failed to read build info")
		return
	}
	for _, dep := range bi.Deps {
		fmt.Printf("Dep: %+v\n", dep)
	}
}
 ```

 　これらを実行すると，いずれも下記のような実行結果が得られる．`module-huga`においては，`github.com/samber/lo`の古いバージョン（v1.**46**.0）を`go.mod`で指定しているにもかかわらず，新しい方（v1.**47**.0）の実行結果が得られる．もちろん，`go.work`ファイルを削除すれば，`go.mod`で指定した通りのバージョンの実行結果が得られる．

```
Dep: &{Path:github.com/samber/lo Version:v1.47.0 Sum:h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= Replace:<nil>}
Dep: &{Path:golang.org/x/text Version:v0.16.0 Sum:h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= Replace:<nil>}
```

　通常Go言語では，`go.mod`に記載される別々のモジュール同士が，間接的に同一のモジュールに依存するがそのバージョンが異なる場合，より最新のバージョンのモジュールが取り込まれる．これについてはMajor Version Suffixの文脈で語られることがあるが，詳細は[こちら](https://christina04.hatenablog.com/entry/go-modules-major-version#:~:text=v1%2C%20not%20v2-,%E5%8E%9F%E5%9B%A0,-go%20modules%E3%81%AF)のページで図解入り解説があるため参考にされたい．
　Workspace modeにおいても同様の考え方があるようで，workspace内の各モジュールが`go.mod`で同一モジュールを参照し，そのバージョンが異なる場合に新しい方のバージョンが採用されること分かる．

### `go.work`をコミットすることで生じうる懸念点
　上記のような状態で`go.work`をコミットして困るケースとして，リポジトリの一部だけcloneする**sparse-checkout**を使う場合が考えられる．巨大なmonorepoの開発においてリポジトリ全体をcloneするのは非効率な場合があり，sparse-checkoutを使いたくなる．上記で検証したように，`go.work`ファイルの有無によって依存モジュールのバージョンが変わってしまう状況で，リポジトリ全体を`go.work`を含めてcloneした場合と，sparse-checkoutを使って`go.work`を含めずcloneした場合で，モジュールの実行結果が変わってくる可能性がある．従って，開発・テスト・ビルドの各フェーズでclone範囲を一貫させる必要性が生じる．
 　また，リポジトリの或るモジュールで先行して`go.mod`のバージョンアップ対応を行った場合，workspace内の別のモジュールでも意図せずバージョンが上がってしまう可能性がある．monorepoにおけるテストの差分実行を取り入れている場合は，暗黙的な依存モジュールのバージョンアップがテストされないまま放置される懸念もある．何より，`go.mod`に記述されている依存モジュールのバージョンと，実際に使われるバージョンが異なる状況は混乱を生む．

## `go.work`ファイルのベターな運用方法
　[こちらの記事](https://dev.to/gophers/what-are-go-workspaces-and-how-do-i-use-them-1643#:~:text=Best%20practises%20and%20Warnings)に述べられていることが基本であると考えられる．要約すると下記のようになる．
 - `go.work`ファイルは原則コミットすべきでなく，`.gitignore`に入れる
 - Workspace modeの利用は開発者で閉じたテスト用途にとどめ，開発が完了したら通常どおり`go.mod`ファイルを使って依存モジュールを取り込む
 - （どうしても`go.work`ファイルをリポジトリで共有したい場合や，そうでない場合も）意図しない`go.work`の混入に備えて，CI/CD環境やビルドに使う環境では，`GOWORK=off`を設定する

https://dev.to/gophers/what-are-go-workspaces-and-how-do-i-use-them-1643#:~:text=Best%20practises%20and%20Warnings

## 【余談】モジュールの公開直後に`go.mod`で古いバージョンのモジュールが取得される場合
　上記のベターな運用方法にもとづき，`go.work`でローカル上のテストをした後，そのモジュールを公開し`go.mod`を使って本番用に依存モジュールを取り込む際に，`go install`でモジュール取得エラーとなる，或いは古いバージョンが取得される場合がある．この場合，キャッシュが悪さをしている可能性があるので，`go clean --modcache`コマンドを試すとよい．

 https://shinshin86.hateblo.jp/entry/2022/05/02/092519

## まとめ
　本記事では，Go言語の公式リファレンスに示される`go.work`ファイルをリポジトリにコミットしない推奨事項をもとに，`go.work`有無が`go.mod`にどう影響するかを検証し，`go.work`をコミットすることで生じうる懸念点を挙げた．
 　複数モジュールを管理するmonorepo構成においてWorkspace modeは有効で，`go.work`をリポジトリにコミットするユースケースも考えうるが，monorepo内の各`go.mod`同士が作用し，実際に記載されるバージョンと異なるモジュールが取り込まれる可能性がある．そのため，`go.work`は原則リポジトリへのコミットを避け，開発・テスト用途にとどめたい．
  　`go.work`ファイルを共有することが有効であると判断した場合においても，Workspace mode有効状態のコードが本番環境まで流れないよう，CI/CD環境下では`GOWORK=off`を設定することが望ましい．