Go モジュールを雰囲気で使っている人のためのFAQ【Go 1.23.6時点】
つい雰囲気でモジュールを使いがちなので、モジュール関連で迷ったり気になることがあったら自分で見返せるようにと作ってみた。
FAQ形式でまとめているので、知ってるところは飛ばして気になるところだけ見てください。
モジュールの基本
Q. モジュールって何?
関連するGoパッケージをひとまとめにして、1つの単位としてバージョン管理・依存管理する仕組み(あるいはGoパッケージのかたまりそのもの)のこと。
モジュールは、プロジェクトのルートに配置された go.mod ファイルによって定義され、どのパッケージがそのモジュールに属しているか、また外部のモジュールに対する依存関係やバージョン情報が記述されている。
つまり、go.modが配置されたディレクトリとそのサブディレクトリにあるgoのコードが全てモジュールとなる。
Go 1.11で導入され、Go 1.13以降は標準で採用されているので、今新しくプロジェクトを始める場合はモジュールを利用することになる。
Q. パッケージって何?
同一ディレクトリにあるソースファイルのまとまり。
ソースファイルの先頭にpackage abc
と宣言されていたらそのソースコードはパッケージabcに属しているということ。
Q. go.modファイルって何?
go mod initで作成されるモジュールの各種情報が記載されたテキストファイル。
サンプルはこんな感じ。
module mygo
go 1.22.2
require (
github.com/livekit/server-sdk-go/v2 v2.1.3
golang.org/x/time v0.9.0
)
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gammazero/deque v0.2.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
~中略~
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
各行の先頭キーワード(module, go, require)から始まる行をディレクティブと呼ぶ。
代表的なディレクティブを説明する。
- module: このモジュールのモジュールパスを表すディレクティブ。モジュールパスについてはどうやってモジュールを使うの?で解説
- go: このモジュールの使用に最低限必要なGoのバージョンを設定する。ローカルのGoのバージョンが1.21以降かどうかで挙動が変わってくる。詳しくはこっち
- require: このモジュールが依存しているパッケージとそのバージョンを表す。indirectというコメントがついている場合はあなたのソースで直接インポートされていないことを表す。
- replace: インポートしている特定のバージョンあるいはすべてのバージョンのモジュールを別のモジュールに文字通り置き換えるディレクティブ。矢印の左側に書かれたモジュールがソースコードでインポートされた場合、実際には右側のモジュールからインポートされる
他にも色々ある。詳細は公式で
Q. go.sumファイルって何?
Goモジュールの整合性を保証するためのチェックサム情報を記録するファイル。
go.modに記載された依存モジュールが正しく取得され、改ざんされていないことを検証するために使用される。
Goコマンドは、モジュールをダウンロードする際にgo.sumの内容と比較し、ハッシュ値が一致するかを確認する。
具体的に何が嬉しいのか。
例えばAさんがあるモジュールを開発したとする。そのモジュールはmyatomicというモジュールをインポートしており、go.sumにはインポート時に生成された当該モジュールの暗号化ハッシュが記載されている。
次に、BさんがAさんのモジュールを使用したいとする。
この時、Bさんの環境ではmyatomicをインポートしたことがないのでモジュールをインポートすることになるが、悪意あるモジュールプロキシを経由して取得してしまい、モジュールが改ざんされてしまった。
しかし、go.sumに記載された正規のチェックサムと、Bさんがインポートした悪意あるモジュールのチェックサムが合わないことから改ざんを検知することができる。
このように、もしgo.sumによるチェックサム検証の機構が用意されていなければ、go mod tidyやgo get時に中間者攻撃でインポートしたいモジュールが改ざんされるリスクが発生してしまう。
ここからは少し深掘って解説するので興味がある人だけお付き合いを。
チェックサム検証にあたっては、モジュールプロキシと、Goが用意してくれているチェックサムデータベースというものが絡んでくる。
まずモジュールプロキシだが、これはモジュールをGitなどのVCSから取得するのを代わりにやってくれるサーバーである。
プロキシにはモジュールがキャッシュされるので高速な配信が可能となる。また、プロキシの仕様に則っていれば、自分たちで独自のプロキシを運用することもできる。
次にチェックサムデータベースだが、これはsum.golang.org
でホスティングされており、公開されている全てのモジュールのバージョンのチェクサムを保持する。先の例で言えばこのDBにmyatomicモジュールのチェックサムが記録されており、それにより悪意あるモジュールプロキシによる改ざんを防ぐことができた。
色々なドキュメントを読んでまとめてみたけど、ドキュメントの各所から情報を集めて集約したからどこか流れに間違いあるかも。
参考程度に。
参考
モジュールの使い方
Q. どうやってモジュールを使うの?
モジュールを使用して新規プロジェクトを開始する際には、モジュールパスを使用してモジュールを初期化する。
$ mkdir go-mod-sample
$ cd go-mod-sample
# モジュールパスを指定してモジュールを初期化
$ go mod init go-mod-sample
# go.modを確認
$ cat go.mod
module go-mod-sample
go 1.23.4
上記の例ではgo mod initに続いているgo-mod-sample
の部分がモジュールパス。
そして、go.modのmoduleディレクティブにモジュールパスが記載される。
ただ、一般的にはGitHubやGitLabなどのホスティングサービスを使用して開発するし、モジュールを外部からインポートできるようにしたいはず。
その場合、モジュールパスを下記のようにする。
$ git clone https://github.com/your-account-id/go-mod-sample.git
$ cd go-mod-sample
# 外部からインポートできるようにリポジトリのパスを使用してモジュールを初期化
$ go mod init github.com/your-account-id/go-mod-sample
$ cat go.mod
module github.com/your-account-id/go-mod-sample
go 1.23.4
モジュールパスがgithub.com/your-account-id/go-mod-sample
になっている。
これにより、他のモジュールからgo getやgo mod tidyなどでモジュールをインポートする際に、あなたのアカウントにあるgo-mod-sampleリポジトリをバージョン管理システム(この例ではGitHub)から探し出すことができる。
というわけで、モジュールを外部から使わせたいのであればリポジトリの完全なパス(github.com/your-account-id/go-mod-sampleなど)をモジュールパスに設定すると良い。
ちなみに、メジャーバージョンを上げるときはモジュールパスの末尾にバージョン情報を追記する必要がある(詳細はここで解説)。
Q. gitリポジトリのルート以外にもモジュールを作りたい
作れる。
ローカルディレクトリgo-mod-sample
があるとすると、つぎのようにサブディレクトリにモジュールを作成可能。
$ cd go-mod-sample
# subdirectory作成
$ mkdir sub
$ cd sub
# subdirectoryを末尾に含めてモジュールパスを指定
$ go mod init github.com/your-repo-name/go-mod-sample/sub
$ cat go.mod
module github.com/your-repo-name/go-mod-sample/sub
go 1.23.4
この時、下記公式説明で述べられているように、モジュールパスはリポジトリの完全なパス(github.com/your-repo-name/go-mod-sample)の後に続けてサブディレクトリ(ここではsub)の名前を付与する必要がある。
モジュールはリポジトリのサブディレクトリで定義されることもあります。
これは通常、独立してリリースされバージョン管理される必要のある複数のコンポーネントを持つ大規模なリポジトリに対して行われます。
このようなモジュールは、リポジトリのルートパスの後にあるモジュールのパスの一部と一致するサブディレクトリで見つかることが期待されます。
例えば、example.com/monorepo/foo/bar というモジュールがルートパス example.com/monorepo のリポジトリにあるとします。
そのgo.modファイルはfoo/barサブディレクトリになければなりません。
サブディレクトリを複数作成して一つのリポジトリに複数のモジュールを作成することもできるが、それらのモジュール間でインポートが発生する場合は注意が必要(詳細はここで解説)。
Q. どうやってモジュールをバージョニングするの?
Gitなどのバージョン管理システムを利用し、タグをつけることでバージョニングする。
すると、go get some-module@v1.2.3
のようにタグv1.2.3
を指定してモジュールをインポートすることができる。
バージョニングの書き方は以下のようにセマンティックバージョニングに従わなければならない。
バージョンタグは"v"から始まり、Major, Minor, Patchバージョンと任意のPre-release識別子を含む。
それぞれの意味は下記の通り。
Version | 説明 |
---|---|
Major |
互換性を壊す変更(APIの変更、パッケージの削除など)がある場合に増加する。 |
Minor |
後方互換性を保ちつつ新機能を追加した場合に増加する。 |
Patch |
バグ修正やセキュリティパッチなど、互換性を維持したまま改善した場合に増加する。 |
Pre-release suffix |
プレリリースバージョンを示す接尾辞(例: -alpha , -beta , -rc など)。付与は任意。 |
build metadata suffix |
ビルドメタデータ(記載順はPatchまたはプレリリースの後)。通常、バージョン比較には影響しない(例: +commit-hash , +incompatible など)。 |
バージョンにはrelease versionとpre-release versionの2種類ある。
Pre-release suffixがつくと、そのバージョンはプレリリースバージョンとみなされる。
それ以外は通常のリリースバージョンである。
上記の表に従って、モジュールの更新をする際に開発者がバージョン番号をアップデートしたタグ付けを行なっていく必要がある。
なお、メジャーバージョンをv2以上に上げる際はタグを付けるだけではダメなので注意が必要(こちらで解説)。
Q. 最初のバージョンはいくつにすべき?
セマンティックバージョニングのFAQにも記載があるが、まず開発版を0.1.0で始めることが推奨。
その後、本番運用するタイミングでv1.0.0とする。
メジャー バージョンが 0 であるか、プレリリース サフィックスが付いている場合、そのバージョンは不安定であると見なされる。
不安定なバージョンには互換性要件は適用されない。
Q. バージョニングしていないモジュールは外部からインポートできないの?
そんなことはない。
バージョニングしていないモジュールも外部からインポート可能だが、その場合 擬似バージョン(pseudo-version) が自動的に割り当てられる。
つまり、モジュールAからバージョニングされていないモジュールBをインポートしようとすると、モジュールBは擬似バージョンを使用してインポートされる。
擬似バージョンは vX.0.0-YYYYMMDDHHMMSS-<commit-hash>
の形式で、X
は通常 0
(メジャーバージョンが確定していない場合)や 1
以上の既存のメジャーバージョン、YYYYMMDDHHMMSS
はそのコミットのタイムスタンプ、commit-hash
はそのコミットの短縮ハッシュを示す。
go.modのrequiredディレクティブでv0.0.0-20191109021931-daa7c04131f5
のような記載を見かけたことがあるかと思うがそれが擬似バージョンである。
擬似バージョンはあくまでも正式なバージョンタグ(例:v1.2.3)がない場合の補助的なものであり、開発ブランチなどでバージョンタグを作成する前にコミットをテストするために使用する。
擬似バージョンはタグのない特定のコミットを参照するための手段として有用だが、明示的なバージョン管理を行うためには適切なタグ付け(v1.0.0
など)を行うことが推奨される。
Q. プレリリースバージョンの使い所は?
新規機能やサービスをリリースする際、まずはベータ版などで提供してフィードバックを得ることが往々にしてある。
そのようなときに v1.0.0-beta.1
などとすると良い。
セマンティックバージョニングにおいて、プレリリースバージョンは安定版 (v1.0.0 など) よりも前の段階のバージョンとして扱われる(つまり、v1.0.0-beta.1 < v1.0.0となる)。
そのため、プレリリースバージョンは正式リリースと比べて不安定とみなされ、外部の依存関係に対して互換性を保証するものではない。
この性質を活かし、外部に対してまだ正式版ではなく、API変更の可能性があることを明確に示すことができる。
Q. サブディレクトリに作ったサブモジュールのバージョニングはどうやるの?
gitリポジトリのルート以外にもモジュールを作りたいで紹介したサブモジュールに対してバージョンを切るには、タグの手前にサブモジュールのパスを付与する。
go-mod-sample/sub
というディレクトリにサブモジュールを作成しているなら、タグはsub/vx.x.x
となるように付ける。
実際の例として、aws-sdk-go-v2のタグを見てみる。
ご覧のように、このリポジトリには多くのサブモジュールがあり、それらに対してタグをつけてバージョニングしている。
例えば、AWS SSM(System Manager)を操作するモジュールservice/ssm/v1.57.1
があるが、これは実際にサブモジュールが配置されているパスaws-sdk-go-v2/service/ssm/
のルートパスを除いた部分に対応している。
これは公式で明言されている従わねばならないルールである。
If a module is defined in a subdirectory within the repository, that is, the module subdirectory portion of the module path is not empty, then each tag name must be prefixed with the module subdirectory, followed by a slash. For example, the module golang.org/x/tools/gopls is defined in the gopls subdirectory of the repository with root path golang.org/x/tools. The version v0.4.0 of that module must have the tag named gopls/v0.4.0 in that repository.
モジュールがリポジトリ内のサブディレクトリで定義されている場合、つまりモジュールパスのモジュールサブディレクトリ部分が空でない場合、各タグ名の前にモジュールサブディレクトリを付け、その後にスラッシュを付けなければなりません。例えば、golang.org/x/tools/goplsモジュールは、ルートパスがgolang.org/x/toolsのリポジトリのgoplsサブディレクトリで定義されています。そのモジュールのバージョンv0.4.0は、そのリポジトリにgopls/v0.4.0というタグがなければなりません。
Q. モジュールのメジャーバージョンをアップデートしてタグを付けたのにインポートできなくなった
例えばgithub.com/go-mod-sample
というモジュールのメジャーバージョンをv2に上げる場合、v2.0.0
のようにタグをつけるだけでは以下のようなエラーが出る。
require github.com/aaa/bbb: version "v2.0.0" invalid: should be v0 or v1, not v2
先に結論を伝えると、go.modのmodule
ディレクティブをmodule github.com/go-mod-sample/v2
とすれば動作する。
何故タグを付けるだけではダメなのか?
Russ Cox氏によるSemantic Import Versioningから読み解く。
仮にモジュールパスがgithub.com/go-mod-sample
のままでタグをv2.0.0
としただけの場合、インポートする側は複数バージョンのgo-mod-sample
をインポートできない。
例えば、go-mod-sample
v1を必要とするモジュールAとv2を必要とするモジュールBがあり、A,Bを使用するCがある場合、Cは間接的にv1とv2の両方を必要とすることになるが、どちらかしかインポートできずに困ってしまう。
また、モジュールパスがv1でもv2でも変わらないということはインポートパスimport "github.com/go-mod-sample"
も変わらないので、不注意にモジュール更新をしてしまった時に破壊的な変更が不本意に発生する事にもなり得る。
そういった事態を避けるべく、Goはメジャーバージョンを上げる際、モジュールパスの接尾辞(v2, v3など)を付けることを言語としてルール化している。
If an old package and a new package have the same import path,
the new package must be backwards compatible with the old package.
古いパッケージと新しいパッケージのインポート パスが同じである場合、
新しいパッケージは古いパッケージと下位互換性がある必要があります。
こうすれば、import "github.com/go-mod-sample"
はv0かv1であることは保証されるので勝手にv2に上げられることは無いし、v1とv2の両方が必要になったとしてもgo.modのrequireにgithub.com/go-mod-sample
とgithub.com/go-mod-sample/v2
が記載されるだけである。
余談だが、Russ Cox氏は以下のようにも述べている。
I certainly agree that “incompatible changes should not be introduced lightly.” Where I think semver falls short is the idea that “having to bump major versions” is a step that will make you “think through the impact of your changes and evaluate the cost/benefit ratio involved.” Quite the opposite: it’s far too easy to read semver as implying that as long as you increment the major version when you make an incompatible change, everything else will work out.
確かに、「互換性のない変更を軽々しく導入すべきではない 」という点には同意する。私がsemverが不十分だと思うのは、「メジャーバージョンを押し上げなければならない」ことが、「module変更の影響をよく考え、それに伴うコストと利益の比率を評価する」ステップになるという考えだ。互換性のない変更をするときにメジャーバージョンをインクリメントしさえすれば、他のことはすべてうまくいくと暗に言っているようにsemverを読むのは、あまりにも簡単すぎる。
確かに、みんながメジャーバージョンを上げることによるコストと利益をきちんと考えたうえで上げられるなら良いが、メジャーバージョンを上げればいいやと考えて簡単に互換性の無い変更をしてしまう何てことはありそうな話ではある。
Q. モジュールのインストールの流れはどうなってるの?
図で示すとこんな感じ。
まず、ローカルにはモジュールキャッシュと呼ばれるディレクトリが作成されており、ここには過去にダウンロードしたモジュールが格納されている。
Goコマンドはまずここを確認する。
キャッシュモジュールが存在しなければ、GOPROXY
とGONOPROXY
の設定に従ってモジュールプロキシかリポジトリへモジュールをダウンロードしに行く(設定詳細はこちら)。
モジュールプロキシとは特定のGETリクエストに応答できるHTTPサーバーのこと。
特定のリクエストとは、例えばhttps://example.com/mod/golang.org/x/text/@v/v0.3.2
のような、モジュールとそのバージョンをエンドポイントとして指定してGETできるようなものを指す。
ダウンロードしたら、Goコマンドはモジュールキャッシュにモジュールを格納しておく。
なお、モジュールファイルはzipファイルで扱われる。モジュールキャッシュにダウンロードされたときにモジュールファイルは展開されて保存される。
Q. GOPROXY環境変数の設定例を教えて
GOPROXYは指定したプロキシサーバーやVCSからモジュールを取得する仕組みを制御する。
GOPROXYにはいくつかの設定値があり、設定値をリスト形式で指定することもできる。
様々な挙動を示すため、ここではいくつか設定例を紹介する。
特定のプロキシサーバーを指定する
GOPROXY=https://my.custom.proxy
このようにすると、my.custom.proxy
というプロキシサーバーに対してhttpsでモジュールを取得しに行く。
URIスキームにはfile
, http
を用いることもできる。
直接VCSからインストール
GOPROXY=direct
この設定では、プロキシサーバーを使用せずgitなどのVCSから直接モジュールを取得する。
なお、一般的にはプロキシサーバーから取得する方が高速ではあるが、非公開リポジトリを使用していてプロキシサーバーがアクセスできない場合などは認証情報を付与して直接リポジトリから取得することもある。
そのようなケースについてはprivateなリポジトリからどうやったらモジュールをインストールできる?を参照。
モジュールをインストールしない
GOPROXY=off
この設定ではモジュールのダウンロードは禁止される。
複数のプロキシサーバーを指定して順にインストールを試す
GOPROXY=https://proxy.golang.org,https://my.custom.proxy
カンマ区切りまたはパイプ(|)区切りで値をリスト形式にすることができる。
この場合、リストの左端から順にインストールが試行されるため、https://proxy.golang.org
, https://my.custom.proxy
の順にインストールが試される。
なお、カンマで区切るかパイプで区切るかでエラーが出た時の挙動が若干異なる。
カンマの場合は、HTTP404もしくは410のステータスコードであればインストールを継続して次のリスト値で試す。
パイプの場合は、タイムアウトのような非HTTPエラーを含め、どのようなエラーの後でも次のリスト値で試す。
プロキシサーバーからインストールできなかったらVCSからインストール
GOPROXY=https://my.custom.proxy,direct
プロキシサーバーhttps://my.custom.proxy
からインストールを試みた後、失敗したらVCSへ直接インストールを試みる。
GOPROXYを設定しない場合
GOPROXYのデフォルトはhttps://proxy.golang.org,direct
となっている。
この設定では、GoコマンドはまずGoogleが運営するGoモジュールのミラーに接続し、ミラーにモジュールがない場合は直接接続にフォールバックする。
Q. privateなリポジトリからどうやったらモジュールをインストールできる?
可能。
プライベートリポジトリから直接モジュールを取得するか、プライベートリポジトリにアクセスできるプライベートプロキシがあればそちらを利用することもできる。
プライベートプロキシサーバーが用意されている
プライベートリポジトリのモジュールを提供するプライベートプロキシが使用できる場合、GOPROXY
環境変数にサーバーのURLを指定してやれば良い。
GOPROXY="https://my.private.proxy.com"
この時、プライベートリポジトリのモジュールのチェックサムはGoのチェックサムデータベースにはないので、モジュールパスを指定して検証を外すのを忘れずに。
GONOSUMDB="my.private.com"
次に、プライベートプロキシ>パブリックプロキシ>リポジトリ直 の順に取得しにいく例も載せておく。
この場合も同様にプライベートリポジトリのモジュールのパスはチェックサムの検証から外すこと。
GOPROXY=https://my.private.proxy.com,https://proxy.golang.org,direct
プライベートプロキシへの認証情報の受け渡しは、下記のように.netrcを用いることができる。
machine <url> login <username> password <token>
- <url>:GitLabのURL、例えばgitlab.com
- <username>:ユーザー名
- <token>:アクセストークン
.netrc
ファイルの場所はNETRC
環境変数で設定できる。
NETRC
が設定されていない場合、GoコマンドはUNIXライクなプラットフォームでは$HOME/.netrc
を、Windowsでは%USERPROFILE%_netrc
を読み込む。
プライベートリポジトリに直接取得しにいく
プライベートリポジトリから直接モジュールをダウンロードする場合はGOPRIVATE
環境変数を利用できる。
自社でプライベートプロキシを運用してるとかでないとこちらになるかと。
GOPRIVATE=mycustom.module.com,example.com
このようにすると、mycustom.module.com
とexample.com
で始まるモジュールのプロキシサーバーに接続しないようgoコマンドに指示できる。
こちらの場合でも認証には.netrcは使える(筆者が普段使うGitLabではいけた)。
普通にgitに対して.netrcを使ってるだけっぽいのでgoの特別な挙動というわけではなさそう。
他にも、gitconfigを書き換えて設定する方法なんかもある。
余談だが、GOPRIVATE
はGONOPROXY
とGONOSUMDB
のデフォルト値でしかない。
GONOPROXY
に設定された値と合致するモジュールパスをインポートする際は、プロキシを使用せず直接ダウンロードする。
また、GONOSUMDB
に設定された値と合致するモジュールパスをインポートする際は、Goの公開チェックサムデータベースを使用しない。
GOPRIVATE
を使うことでこれらを一度に同じ値でセットできる。
Q. 複数のモジュールを1つのリポジトリで管理したいんだけどどうできる?
各モジュールをサブモジュールとすれば管理できる。
こんな感じ。
$ pwd
/multi-modules-sample
$ ll
〜
drwxr-xr-x 6 user1 staff 192B Jan 23 13:59 myhttp # custom http client module
drwxr-xr-x 6 user1 staff 192B Mar 5 15:09 mylogger # custom logger module
drwxr-xr-x 6 user1 staff 192B Feb 18 15:18 myatomic # custom atomic module
上記ではmyhttp
, mylogger
, myatomic
の3つのモジュールを一つのリポジトリで管理している。
サブモジュールについては、「Q. gitリポジトリのルート以外にもモジュールを作りたい」と「Q. サブディレクトリに作ったサブモジュールのバージョニングはどうやるの?」を参照。
もし各モジュール間に全く依存関係がない場合はこれでOK。
しかし、myhttp
でmylogger
を使いたい場合はこのままでは困りごとが出る。
それは、開発環境のモジュール間において、開発中のモジュールをインポートできないと言うことである。
例えば、myhttp
でmylogger
をインポートしたくてもインポートできない。
正確に言うと、バージョンタグづけされて公開されているmylogger
でないとmyhttp
はインポートできない。
モジュールのインストールの流れはどうなってるの?で解説した通り、Goコマンドはimport文のパスに従ってモジュールプロキシかリポジトリ(あるいはローカルキャッシュ)から取得されるので、そのままではローカルの/multi-modules-sample/mylogger
ディレクトリを参照してくれない。
これを解消する方法は3つある。
まず1つは、mylogger
にバージョンタグをつけて公開することである。
そうすれば、myhttp
のgo.modのrequire example.com/mylogger v1.0.1
のように、インポートしたいタグを指定するだけである。
ただ、このやり方だとmylogger
を修正するたびにリリースしてバージョンタグをつけないと開発できないのでやり辛いし検証に困る。
そもそも、バージョンタグを変更するのはリリースタイミングである。
開発中は動かして修正してを繰り返すので、この方法は開発には基本使わないだろう。
次に、go.modのreplace
ディレクティブを使用する方法がある。
module example.com/myhttp
go 1.22.2
require example.com/mylogger v1.0.0
replace example.com/mylogger => ../mylogger
このように書くと、example.com/mylogger
モジュールは公開済みのv1.0.0ではなく、代わりにローカルのmylogger
ディレクトリのソースコードでのモジュールが読み込まれる。
ただ、このreplace
ディレクティブは開発用なのでリポジトリにプッシュする際には削除しておかなくてはならない。
開発した結果mylogger
も修正が発生するならv1.0.1のタグをつけてリリースし、myhttp
はgo.modを修正してそれを取り込む必要がある。
最後に紹介するのはreplace
に変わる機能としてGo1.18.0で導入されたワークスペースである。
このケースではこちらが最も推奨される。
$ pwd
/multi-modules-sample
# go.workファイルをリポジトリルートに作成
$ go work init
# 全てのモジュールをgo.workに追加
$ go work use ./myhttp
$ go work use ./mylogger
$ go work use ./myatomic
このようにすると、myhttp
は/multi-modules-sample/mylogger
を参照するようになる。
workspaceモードの詳細については以下のドキュメントを読むと良い。
- Tutorial: Getting started with multi-module workspaces
- Proposal: Multi-Module Workspaces in cmd/go
- Workspaces
ちなみに、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.
・チェックインされたgo.workファイルは、親ディレクトリにある開発者自身のgo.workファイルを上書きするかもしれません。
・チェックインされたgo.workファイルは、継続的インテグレーション(CI)システムに、モジュールの依存関係の間違ったバージョンを選択させ、テストさせるかもしれません。CIシステムは、一般的にgo.workファイルの使用を許可されるべきではなく、モジュール内のgo.workファイルが影響を及ぼさない、他のモジュールから要求されたときに使用されるようなモジュールの動作をテストすることができます。
ただし、一部例外も認めている。
That said, there are some cases where committing a go.work file makes sense. For example, when the modules in a repository are developed exclusively with each other but not together with external modules, there may not be a reason the developer would want to use a different combination of modules in a workspace. In that case, the module author should ensure the individual modules are tested and released properly.
とはいえ、go.work ファイルをコミットすることが理にかなっている場合もあります。たとえば、リポジトリ内のモジュールが、外部モジュールとは一緒に開発されずに、互いに排他的に開発されている場合、開発者がワークスペースで異なるモジュールの組み合わせを使いたい理由がないかもしれません。その場合、モジュールの作者は、個々のモジュールが適切にテストされ、リリースされていることを確認する必要があります。
モジュール操作コマンド
Q. go get, go install, go mod download, go mod tidyの使い分けは?
まとめるとこんな感じ。
コマンド | 用途 | 影響 | 具体的な使用例 |
---|---|---|---|
go get |
モジュールの追加・更新・削除 |
go.mod , go.sum を変更 |
go get example.com/module@latest (最新バージョンに更新) |
go install |
バイナリの取得・ビルド($GOBIN に配置) |
go.mod , go.sum に影響なし |
go install example.com/cmd@latest (特定バージョンのバイナリを取得してコマンドとして使用できるようにする) |
go mod download |
依存モジュールのダウンロード |
go.mod , go.sum に影響なし |
go mod download all (go.mod に記載されたすべてのモジュールをダウンロード) |
go mod tidy |
不要なモジュールを削除し、必要なモジュールを追加 |
go.mod , go.sum を整理 |
go mod tidy (未使用のモジュールを削除し、必要なものを追加) |
go get
- 指定したモジュールの取得・更新・削除を行う
-
go.mod
とgo.sum
に変更を加える
使用例
# `example.com/module` を追加(最新の互換バージョンを取得)
go get example.com/module
# モジュールを最新の互換バージョンに更新
go get -u example.com/module
# メジャーバージョンも含めて最新に更新
go get -u=latest example.com/module
# モジュールを削除
go get example.com/module@none
go install
使用例
# `example.com/cmd` の最新バージョンのバイナリをインストール
go install example.com/cmd@latest
# 特定バージョンのバイナリをインストール
go install example.com/cmd@v1.2.3
go mod download
-
go.mod
に記載された依存モジュールをモジュールキャッシュ (GOMODCACHE) にダウンロード -
go.mod
,go.sum
に影響なし -
go mod tidy
みたいにソースコードのimport文を調べて自動でインポートしたりはしない
使用例
# `go.mod` に記載されているすべてのモジュールをダウンロード
go mod download all
# 特定のモジュールのみダウンロード
go mod download example.com/module
go mod tidy
-
go.mod
がモジュールのソースコードと一致することを保証する - ソースコードのimport文を調べ、
go.mod
への記載が漏れていたら追記してモジュールをインポート - ソースで使われないが
go.mod
に記載があるモジュールはgo.mod
から削除 -
go.mod
の更新に合わせてgo.sum
を更新
使用例
# プロジェクトの依存関係を整理
go mod tidy
なお、go get
やgo mod tidy
を実行してgo.modのgoディレクティブやtoolchainディレクティブが更新されることがある場合はToolchainって何?を参照。
Q. go mod tidyをするとどのバージョンのモジュールがインポートされる?
go.modのrequiredにバージョンの記載があればそれがインポートされる。
go.modに記載がない場合はMinimal Version Selectionによって必要最低限のバージョンが用いられる。
この時、どのバージョンが用いられるにせよ、メジャーバージョンは変わらない。
ちなみに、インポート元モジュールのgo.modのgoディレクティブより高いバージョンを要求するモジュールがインポート対象に含まれる場合、Toolchainがautoまたはpathになっていれば必要なGoバージョンにgo.modを書き換えてモジュールをインポートする。
詳細はこちらのブログ「苦しんで覚えたGo Toolchainを詳しく解説する」でまとめている。
Q. Goがインポートしようとしている依存モジュールのバージョンはどう確認する?
go list -m -json all
このコマンドでMVSによって選択されるモジュールのバージョンや、そのモジュールが必要とする最低限のGoのバージョンなどを確認できる。
# jqにパイプして出力量を絞る
$ go list -m -json all | jq '. | {Path, Version, GoVersion}'
{
"Path": "google.golang.org/genproto/googleapis/rpc",
"Version": "v0.0.0-20241202173237-19429a94021a",
"GoVersion": "1.21"
}
{
"Path": "google.golang.org/grpc",
"Version": "v1.70.0",
"GoVersion": "1.22"
}
{
"Path": "google.golang.org/protobuf",
"Version": "v1.36.5",
"GoVersion": "1.21"
}
{
"Path": "gopkg.in/check.v1",
"Version": "v1.0.0-20190902080502-41f04d3bba15",
"GoVersion": null
}
{
"Path": "gopkg.in/yaml.v3",
"Version": "v3.0.1",
"GoVersion": null
}
...
Toolchain
Q. go.modのgoディレクティブを変えてもビルド時のバージョンが変わらないんだけど..
結論から言うと、ローカルにインストールされているGoが1.20以前の場合はgoディレクティブに記載されているバージョンを変えてもビルド時のバージョンは変わらない。
Go 1.20以前では、go.mod内のgoディレクティブにどのバージョンが書かれていても、ビルド時に使用されるのはローカルマシンにインストールされているGo。
'ローカルマシンのGoのバージョン'が'goディレクティブで指定されたバージョン'より高いか低いかでビルド時の挙動が変わるだけ。
'ローカルGo'>'goディレクティブ'であれば、goディレクティブのバージョンのセマンティクスでビルドを試みる(例.ローカルが1.19でgoディレクティブが1.18であれば1.19で登場した標準パッケージは使用できない)。
'ローカルGo'<'goディレクティブ'であれば、ローカルGoでビルドを試みてビルドエラーが出なければビルドは可能(ソースコード内でローカルGoより後のバージョンの機能が使われていなければビルドは通る)。
Go1.21以降はこの辺りのgoディレクティブの挙動の分かりづらさをなんとかするためにGo Toolchainという機能が導入されているのでまた話が変わってくる。
Q. Toolchainって何?
go.mod, go.workのgoディレクティブの挙動を分かりやすくするとともに、複数プロジェクト間で異なるバージョンのGoを利用しやすくするための仕組み。
Go 1.21から導入された。
GOTOOLCHAINという環境変数にモジュールに適用したいGoのバージョンを指定することで、そのバージョンでモジュールを動かせる(かもしれない)。
GOTOOLCHAINの設定値や実行するコマンドに応じて挙動が色々分かれるので割と複雑。
説明すると長くなるのでこちらのブログ「苦しんで覚えたGo Toolchainを詳しく解説する」でまとめている。
おわりに
他にも気になることが出たら追記するかも。
ここに載ってなくて困ったことがあったら、公式のこのドキュメントを読もう。
ではまた。
Discussion