Edited at

Go Modules

普段あまり Go を使わない人が仕事で使ってるソースが突然 Go Modules になって困って調べたメモ。


問題の背景

Go では GOPATH という特殊な概念があって、Go のコードはライブラリも含めてすべて $GOPATH/src 以下に置くという約束になっている。そうするとプロジェクトごとにライブラリのバージョンを変えたい場合に困るので、プロジェクト固有のライブラリは $GOPATH/src/プロジェクト/vendor の下に入れても良いことになっている。そうすると go コマンドは vendor 以下を優先して見てくれる。面白い事に、vendor ディレクトリの管理は govendor 等色々なサードパーティツールを使う。

将来の Go 1.12 からこのやり方を改め、$GOPATH/srcvendor は廃止となり Go Modules という物を採用する事になった。現在の Go 1.11 は移行期だが、環境変数 GO111MODULE により Go Modules を試す事が出来る。Go 1.11 で Go Module mode にするには次のようにする。

export GO111MODULE=on 


Go Modules の使い方

https://github.com/golang/go/wiki/Modules#quick-start-example のサンプルを試す。

GOPATH を指定しない状態で空のディレクトリを作り、中で

go mod init example.com/hoge/hello

を実行すると go.mod ファイルが出来る。続けて以下のようなファイル hello.go を作る。rsc.io/quote は Go Modules の説明のために作られたパッケージのようです。

package main

import (
"fmt"
"rsc.io/quote"
)

func main() {
fmt.Println(quote.Opt())
}

go build

でビルドすると、なんと勝手にライブラリ "rsc.io/quote" とその依存ファイルをダウンロードしてビルドしてくれる! ライブラリは ~/go/pkg/mod/ に保存されてました。試しに GOPATH を別の場所に設定すると $GOPATH/pkg/mod/ に保存されてました。また、go install するとやはり $GOPATH/bin にバイナリがコピーされました。ということは次の事が言えます。


Go Modules ではプロジェクトを GOPATH の外に置けるが、ライブラリのキャッシュや go install のコピー先として GOPATH (デフォルトで ~/go) が使われる。


知ってる人にとっては当たり前なんでしょうが、ドキュメントを読んでも GOPATH を使わないという事ばかり強調されてるので理解してませんでした。


デフォルトでインストールするライブラリのバージョンの決め方

自動生成された go.mod の内容はこんな感じ。依存ライブラリのバージョンが勝手に指定されている。

module example.com/hoge/hello

require rsc.io/quote v1.5.2

go build コマンドはどうやって使うバージョンを v1.5.2 と決めたのだろうか?

まず Remote import paths のルールに従って import path からレポジトリの位置を特定する。この例では https://rsc.io/quote にアクセスして rcs.io/quote のありかを meta タグで探す。

% curl 'https://rsc.io/quote'

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="rsc.io/quote git https://github.com/rsc/quote">
<meta http-equiv="refresh" content="0; url=https://godoc.org/rsc.io/quote">
</head>
<body>
Redirecting to docs at <a href="https://godoc.org/rsc.io/quote">godoc.org/rsc.io/quote</a>...
</body>
</html>

ここで https://github.com/rsc/quote が見つかった。

このレポジトリにはいくつかの tag があるが、同一メジャーバージョンの中でいちばん高いバージョンが選ばれる。この例 rsc.io/quote ではバージョン指定が無いので v1 が指定されたと見なす。v2 以上を指定したい時は import "rsc.io/quote/v2" のように明示的に指定する必要がある。

package main

import (
"fmt"
"rsc.io/quote/v2"
)

func main() {
fmt.Println(quote.HelloV2())
}

別の言い方をすると、メジャーバージョンが違うモジュールは import name が異なるので全然別のモジュールとして扱われる。


明示的にインストールするライブラリのバージョンの決め方

一旦必要なバージョンが go.mod に記載されると、それ以降は go コマンドは勝手にライブラリのバージョンを上げない。これでバイナリ作成時の依存バージョンを特定出来る。

一方で、2つのライブラリが共通のライブラリを利用していて、マイナーバージョンやパッチバージョンが違う場合は高いバージョンが選択される。なのでライブラリ作成者の観点からすると、呼び出し元がより高いバージョンのライブラリを使ってリンクする事もあるという事になる(本当かな。。。。?)


Go get

go get -u

で go.mod を更新し、最新のマイナーバージョン、パッチバージョンをキャッシュにダウンロードする。

今までと同様 go get でパッケージのビルドとインストールを行う事も出来る。$GOPATH/bin にインストールされる事も同じ。$GOPATH/src は使われない。

go get -u github.com/gogo/protobuf/protoc-gen-gogoslick

go get で明示的にパッケージをインストールすると go.mod に使わないライブラリのカスが貯まるので、以下のコマンドで使うやつだけに整理する。

go mod tidy


デフォルト動作

GO111MODULE が未定義の時は次のような動作になる。



  • GOPATH の中にいる時: 昔ながらの動作になる。GO111MODULE=off の動作



    • go get した物は $GOPATH/src に最新版が保存される。




  • GOPATH の外かつ go.mod が存在するツリーの中にいる時: Go Modules が有効になる。GO111MODULE=on の動作



    • GOPATH が未定義の時は $HOME/goGOPATH になる。


    • go get した物は $GOPATH/pkg にバージョンごとに保存される。




protobuf で使う時の落とし穴

Go Modules が有効だと $GOPATH/src にソースコードが置かれないので、protobuf からインクルード出来ない。protobuf 用には GO111MODULE=off go get なんとか のようにする必要がある。(参考: README.md: fix install instructions when used with modules)


用語


  • package とは、ソースコード内に package hoge のように指定する。だいたいディレクトリの事。

  • module とは、go.mod ファイル内に module example.com/user/hoge のように指定する。複数の package を含むもの。