4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-09-05

本記事の背景と目的

 go言語のWorkspace modeを導入した際に生成されるgo.workファイルは,リポジトリにコミットすべきでないとの指摘がある[1][2].この点についてProposalでも盛んに議論されており,公式リファレンスではgo.workをリポジトリにコミットしないよう推奨されている.
 本記事では,go.workをリポジトリにコミットすると困りうるパターンを検証し,go.workをコミットすべきでない理由を明らかにする.

Workspace modeとは

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

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や先述のProposalで議論を呼んでいる.

go.workファイルをコミットすべきでない理由

 ここで,公式リファレンスで示されている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 ファイルが影響を及ぼさない、他のモジュールから要求されたときに使われるようなモジュールの挙動をテストすることができます。

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

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

検証内容と結果

 例えば下記のようなmonorepo構成を想定する.monorepo-hogeにはmodule-hugamodule-piyoの2つのモジュールが含まれているが,これらに明確な依存関係はない.

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は下記のようになる.

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
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に記述する.(参考元

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の文脈で語られることがあるが,詳細はこちらのページで図解入り解説があるため参考にされたい.
 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ファイルのベターな運用方法

 こちらの記事に述べられていることが基本であると考えられる.要約すると下記のようになる.

  • go.workファイルは原則コミットすべきでなく,.gitignoreに入れる
  • Workspace modeの利用は開発者で閉じたテスト用途にとどめ,開発が完了したら通常どおりgo.modファイルを使って依存モジュールを取り込む
  • (どうしてもgo.workファイルをリポジトリで共有したい場合や,そうでない場合も)意図しないgo.workの混入に備えて,CI/CD環境やビルドに使う環境では,GOWORK=offを設定する

【余談】モジュールの公開直後にgo.modで古いバージョンのモジュールが取得される場合

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

まとめ

 本記事では,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を設定することが望ましい.

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?