GOPATH 汚染問題

【注意 2018-09-26】 この問題はバージョン 1.11 からサポートされる「モジュール」機能によって解消可能です。 もはやこの記事の内容は古いものであり「こんな時代もあったね」と生暖かい気持ちで読んでいただければ幸いです。

(初出: そろそろ真面目に Golang 開発環境について考える — GOPATH 汚染問題 - Qiita,そろそろ真面目に Golang 開発環境について考える — Internal Packages と Vendoring - Qiita)

go get コマンドはとても強力な機能で,私のように Windows と UNIX 系環境の間を渡り歩いてる身としては, make などの tool chain に大きく依存することなく, go get コマンドだけで repository の fetch からビルド・インストールまで出来てしまうのは非常にありがたい1。

しかし, go get コマンドは外部パッケージの revision 等をコントロールできず,常に repository の最新コードを取ってこようとする。 ひとつの環境でひとつのプロジェクトを管理していくのならこれでも何とかならないこともないが, GOPATH 内に複数のプロジェクトが同居している場合は同じ外部パッケージでもプロジェクトごとに異なるリビジョンを要求する可能性があり,管理が煩雑になってしまう。

しかも困ったことに GOPATH 環境変数は複数のプロジェクト管理を想定していないため,全てのパッケージをひとつのフォルダに入れようとする2 3。

【対策1】 プロジェクトごとに GOPATH を設定し直す

この問題に対する一番安直な答えは「プロジェクトごとに GOPATH を設定し直す」である。例えば前回紹介した gb をビルドする場合は以下のようにする。

C:>mkdir C:\workspace\gb

C:>SET GOPATH=C:\workspace\gb

C:>go get -v github.com/constabulary/gb/...
github.com/constabulary/gb (download)
github.com/constabulary/gb/log
github.com/constabulary/gb
github.com/constabulary/gb/vendor
github.com/constabulary/gb/cmd
github.com/constabulary/gb/cmd/gb
github.com/constabulary/gb/cmd/gb-vendor

あとは GOPATH 直下の bin フォルダにパスを通すか,パスの通ってるフォルダに実行ファイルをコピーすればよい。 実行履歴はバッチファイル(UNIX 系なら shell スクリプト)に保存しておけばいつでも復元できる。

毎回環境をセットアップしないといけないのは面倒だが,プロジェクト管理のためのツールも必要なく, Go コンパイラの標準機能のみで管理できる。 標準機能のみで管理できるというのは結構重要で,たとえば CI ツールを使っている場合は,設定を単純にできるので管理しやすいといえる。

UNIX 系の環境であれば direnv を使う手もある4。 direnv は cd をフックし,ディレクトリごとに環境変数を書き換えることができる。 この機能を使ってプロジェクト・フォルダごとに GOPATH を設定できる。

【対策2】 プロジェクト・ベースの管理ツールを使う

もうひとつは gb のようなプロジェクト・ベースでコード管理のできるツールを使う方法である。 gb については前回紹介したので,そちらを参照のこと。

gb で作った開発環境はフォルダ構成を丸ごと開発メンバに配布・同期することが可能になるため,複数人で環境を合わせることが容易になる。

【対策3】 Go 1.5 の Vendoring 機能を使う

Go 言語のバージョン 1.5 から Vendoring 機能が使えるようになった。

Vendoring 機能を使うと,外部パッケージを GOPATH とは独立に管理できるようになる。 この機能を使うには環境変数 GO15VENDOREXPERIMENT に 1 をセットする。

(追記 当初の予告通り Vendoring 機能は 1.6 から既定の機能になった。環境変数 GO15VENDOREXPERIMENT をセットしなくても有効になる)

Vendoring 機能が有効な状態では vendor フォルダが特別な意味を持つ。 たとえば mypackage パッケージに対して mypackage/vendor/vpackage と配置した場合, import "vpackage" と記述すれば mypackage/vendor フォルダ以下の vpackage も探してくれる。

では,前回作ったコードを流用して確かめてみる。

C:\workspace\vdemo>SET GOPATH=C:\workspace\vdemo

C:\workspace\vdemo>SET GO15VENDOREXPERIMENT=1

C:\workspace\vdemo>tree /f .
C:\WORKSPACE\VDEMO
└─src
    └─julian-day
            julian-day.go

C:\workspace\vdemo>go build ./...
src\julian-day\julian-day.go:10:2: cannot find package "github.com/spiegel-im-spiegel/astrocalc/modjulian" in any of:
        C:\Go\src\github.com\spiegel-im-spiegel\astrocalc\modjulian (from $GOROOT)
        C:\workspace\vdemo\src\github.com\spiegel-im-spiegel\astrocalc\modjulian (from $GOPATH)

C:\workspace\vdemo>mkdir src\julian-day\vendor

C:\workspace\vdemo>tree /f .
C:\WORKSPACE\VDEMO
└─src
    └─julian-day
        │  julian-day.go
        │
        └─vendor


C:\workspace\vdemo>go build ./...
src\julian-day\julian-day.go:10:2: cannot find package "github.com/spiegel-im-spiegel/astrocalc/modjulian" in any of:
        C:\workspace\vdemo\src\julian-day\vendor\github.com\spiegel-im-spiegel\astrocalc\modjulian (vendor tree)
        C:\Go\src\github.com\spiegel-im-spiegel\astrocalc\modjulian (from $GOROOT)
        C:\workspace\vdemo\src\github.com\spiegel-im-spiegel\astrocalc\modjulian (from $GOPATH)

vendor フォルダを追加したことで Go コンパイラの挙動が変わったことがお分かりだろうか。 目的のパッケージを vendor tree → GOROOT → GOPATH の順で捜索している。

では vendor フォルダに外部パッケージを導入してビルドしてみよう。

C:\workspace\vdemo>pushd src\julian-day\vendor

C:\workspace\vdemo\src\julian-day\vendor>git clone https://github.com/spiegel-im-spiegel/astrocalc.git github.com/spiegel-im-spiegel/astrocalc
Cloning into 'github.com/spiegel-im-spiegel/astrocalc'...
remote: Counting objects: 43, done.
remote: Total 43 (delta 0), reused 0 (delta 0), pack-reused 43
Unpacking objects: 100% (43/43), done.
Checking connectivity... done.

C:\workspace\vdemo\src\julian-day\vendor>popd

C:\workspace\vdemo>tree /f .
C:\WORKSPACE\VDEMO
└─src
    └─julian-day
        │  julian-day.go
        │
        └─vendor
            └─github.com
                └─spiegel-im-spiegel
                    └─astrocalc
                        │  .editorconfig
                        │  .gitignore
                        │  .travis.yml
                        │  LICENSE
                        │  README.md
                        │
                        └─modjulian
                                example_test.go
                                LICENSE
                                modjulian.go
                                modjulian_test.go

C:\workspace\vdemo>go install -v ./...
julian-day/vendor/github.com/spiegel-im-spiegel/astrocalc/modjulian
julian-day

C:\workspace\vdemo>tree /f .
C:\WORKSPACE\VDEMO
├─bin
│      julian-day.exe
│
├─pkg
│  └─windows_amd64
│      └─julian-day
│          └─vendor
│              └─github.com
│                  └─spiegel-im-spiegel
│                      └─astrocalc
│                              modjulian.a
│
└─src
    └─julian-day
        │  julian-day.go
        │
        └─vendor
            └─github.com
                └─spiegel-im-spiegel
                    └─astrocalc
                        │  .editorconfig
                        │  .gitignore
                        │  .travis.yml
                        │  LICENSE
                        │  README.md
                        │
                        └─modjulian
                                example_test.go
                                LICENSE
                                modjulian.go
                                modjulian_test.go

C:\workspace\vdemo>bin\julian-day.exe 2015 1 1
2015-01-01 00:00:00 +0000 UTC
MJD = 57023日

vendor フォルダ以下にパッケージがフルパスで入ってしまうため階層が深くなりがちなのが「玉に瑕」だが,それ以外は特に問題はない。 あるいは vendor フォルダ以下のパッケージは go get の制約から外れているので,呼び出し側を

import (
    "flag"
    "fmt"
    "os"
    "strconv"
    "time"

    "astrocalc/modjulian"
)

として以下のフォルダ構成にする手もある5。

C:\workspace\vdemo>tree /f .
C:\WORKSPACE\VDEMO
└─src
    └─julian-day
        │  julian-day.go
        │
        └─vendor
            └─astrocalc
                │  .editorconfig
                │  .gitignore
                │  .travis.yml
                │  LICENSE
                │  README.md
                │
                └─modjulian
                        example_test.go
                        LICENSE
                        modjulian.go
                        modjulian_test.go


C:\workspace\vdemo>go install -v ./...
julian-day/vendor/astrocalc/modjulian
julian-day

C:\workspace\vdemo>bin\julian-day.exe 2015 1 1
2015-01-01 00:00:00 +0000 UTC
MJD = 57023日

注意が必要なのは, go get は git の submodule を上手く扱えないため, vendor フォルダ以下のパッケージを submodule として配置している場合はビルドに失敗することだ。 この場合は -d オプションで go get がビルドまで行わないようにし,手動で submodule の init と update を行う必要がある。

C:>go get -d project/...
C:>git submodule init
C:>git submodule update
C:>go install ./...

(「Glide で Vendoring」に続く)

ブックマーク


  1. それでも git などのコード管理ツールへの依存はどうしても残るのだけれど。 ↩︎

  2. 具体的には GOPATH で列挙されるパスのリストのうち先頭のパスにインストールされる。 ↩︎

  3. Go 言語の開発・管理主体は Google だが,こんな構成で Google は困らないのかと思ったのだが,実は Google は全てのコードを単一の repository で管理しているらしい。(参考: 20億行のコードを保存し、毎日4万5000回のコミットを発行しているGoogleが、単一のリポジトリで全社のソースコードを管理している理由) ↩︎

  4. direnv は Go 言語で組まれている。 ↩︎

  5. パッケージのパスが変わるとテストが通らなくなる場合があるので注意。 ↩︎