Go 1.11 Modules でパッケージ管理を行う

これは Go4 Advent Calendar 2018 の 22 日目の記事です。

今年の 8 月に Go 1.11 がリリースされました。このバージョンでは Modules と呼ばれる新しいコンセプトが導入されています。これはバージョニングとパッケージ配布の仕組みで、 GOPATH 配下以外でのプロジェクト管理を可能にするものです。

これは Go 1.11 の時点では実験的な機能として提供されていて、環境変数 GO111MODULE の設定やプロジェクトが GOPATH 配下にあるかどうかに応じて有効化されるようになっています。このバージョンでフィードバックを取り込みながら Go 1.12 で正式な機能としてリリースされる予定ということです。今回は現時点での Go Modules を試してみたいと思います。

Go 1.11 Modules で依存解決を行う

私は Web API の作成に goa を良く使っているので、その example である goa-cellar に Go 1.11 Modules を適用できるか試してみたいと思います。

1. GOPATH 外にプロジェクトを用意してそのルートに移動する

GOPATH~/go なので、その配下ではない ~/goa-cellargit clone してプロジェクトルートに移動します。

$ git clone git@github.com:goadesign/goa-cellar.git ~/goa-cellar
$ cd ~/goa-cellar

2. Module 定義を初期化して go.mod ファイルを作成する

go コマンドには既にサブコマンドとして mod が組み込まれているのでそれを使って go.mod を作成します。

$ go mod init
go: creating new go.mod: module github.com/goadesign/goa-cellar

3. Module をビルドする

Module をビルドします。 Go Modules ではビルドやテストのタイミングで必要な依存解決が行われます。

$ go build -o cellar
go: finding github.com/goadesign/goa v1.4.0
go: finding github.com/satori/go.uuid v1.2.0
go: finding github.com/go-kit/kit v0.8.0
go: finding github.com/dimfeld/httptreemux v5.0.1+incompatible
go: finding github.com/hashicorp/go-immutable-radix v1.0.0
go: finding github.com/go-logfmt/logfmt v0.4.0
go: finding github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: finding golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: finding github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding github.com/hashicorp/go-uuid v1.0.0
go: finding github.com/hashicorp/golang-lru v0.5.0
go: downloading github.com/goadesign/goa v1.4.0
go: downloading github.com/go-kit/kit v0.8.0
go: downloading golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: downloading github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: downloading github.com/dimfeld/httptreemux v5.0.1+incompatible
go: downloading github.com/satori/go.uuid v1.2.0
go: downloading github.com/go-logfmt/logfmt v0.4.0
go: downloading github.com/hashicorp/go-immutable-radix v1.0.0
go: downloading github.com/hashicorp/golang-lru v0.5.0
# github.com/goadesign/goa/uuid
../go/pkg/mod/github.com/goadesign/goa@v1.4.0/uuid/uuid.go:18:23: not enough arguments in call to uuid.Must
        have (uuid.UUID)
        want (uuid.UUID, error)

おっと、エラーが出てしまいました。コードを見てみます。

https://github.com/goadesign/goa/blob/v1.4.0/uuid/uuid.go#L18

import "github.com/satori/go.uuid"

// FromString Wrapper around the real FromString
func FromString(input string) (UUID, error) {
    u, err := uuid.FromString(input)
    return UUID(u), err
}

// NewV4 Wrapper over the real NewV4 method
func NewV4() UUID {
    return UUID(uuid.Must(uuid.NewV4()))
}

これは https://github.com/satori/go.uuid の問題ですね。当初 uuid.Must()uuid.UUID だけを返す関数だったのですが、あるとき破壊的変更が入って error も返す形にシグネチャが変更されました。しかしその変更のあとに新たなバージョンのリリースは行われておらず master ブランチだけに変更が入っている状態です。

goa はこの master ブランチのコードを参照しているのですが、 Go Modules はデフォルトだとタグ付けされた最新のリリースバージョンを選択するため master ではなく v1.2.0 が使われることになります。これがエラーの原因でした。

$ cat go.mod | grep github.com/satori/go.uuid
        github.com/satori/go.uuid v1.2.0 // indirect

このデフォルトのバージョン選択は @version 接尾辞を用いた go get で変更することが出来るので、明示的に master を参照するよう依存を変更します。

$ go get github.com/satori/go.uuid@master
go: finding github.com/satori/go.uuid master
go: downloading github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b

再度ビルドしてみます。

$ go build -o cellar
go: finding github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b

今度は成功です。ではサーバを起動してリクエストを送信してみます。

$ ./cellar &
[1] 27478
lvl=info msg=mount ctrl=Account action=Create route="POST /cellar/accounts"
lvl=info msg=mount ctrl=Account action=Delete route="DELETE /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Account action=List route="GET /cellar/accounts"
lvl=info msg=mount ctrl=Account action=Show route="GET /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Account action=Update route="PUT /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Bottle action=Create route="POST /cellar/accounts/:accountID/bottles"
lvl=info msg=mount ctrl=Bottle action=Delete route="DELETE /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=List route="GET /cellar/accounts/:accountID/bottles"
lvl=info msg=mount ctrl=Bottle action=Rate route="PUT /cellar/accounts/:accountID/bottles/:bottleID/actions/rate"
lvl=info msg=mount ctrl=Bottle action=Show route="GET /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=Update route="PATCH /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=Watch route="GET /cellar/accounts/:accountID/bottles/:bottleID/watch"
lvl=info msg=mount ctrl=Public files=public/html/index.html route="GET /ui"
lvl=info msg=mount ctrl=Js files=public/js route="GET /js/*filepath"
lvl=info msg=mount ctrl=Js files=public/js/index.html route="GET /js/"
lvl=info msg=mount ctrl=Swagger files=public/swagger/swagger.json route="GET /swagger.json"
lvl=info msg=listen transport=http addr=:8081
$ http :8081/cellar/accounts
req_id=pTlK94BRv4-1 lvl=info msg=started GET=/cellar/accounts from=::1 ctrl=Account action=list
req_id=pTlK94BRv4-1 lvl=info msg=headers Accept=*/* Accept-Encoding="gzip, deflate" Connection=keep-alive User-Agent=HTTPie/1.0.2                                                                                                      
req_id=pTlK94BRv4-1 lvl=info msg=completed status=200 bytes=114 time=203.016µs ctrl=Account action=list
HTTP/1.1 200 OK
Content-Length: 114
Content-Type: application/vnd.account+json; type=collection
Date: Tue, 18 Dec 2018 13:58:15 GMT

[
    {
        "href": "/cellar/accounts/1",
        "id": 1,
        "name": "account 1"
    },
    {
        "href": "/cellar/accounts/2",
        "id": 2,
        "name": "account 2"
    }
]

$ kill 27478
[1]+  Terminated: 15          ./cellar

問題なさそうです。

依存としてのツール

goa はプロジェクトを構成するコードの大部分を自動生成する仕組みになっており、そのために goagen という専用のツールを用意しています。 Go Modules は、そういったプロジェクトに必要となるツールの管理も出来るようになっています。 Go modules by example に詳しい手順が載っているので、これを参考に goagen を管理してみます。

GOBIN にツールのインストール先を設定する

Go はデフォルトだと go install で作られたバイナリを $GOPATH/bin に配置しますが、 $GOBIN を設定することで対象のパスを変更することが出来ます。今回はツールもプロジェクトのディレクトリ配下に置くようにするため $PWD/bin に設定します。

$ export GOBIN=$PWD/bin
$ export PATH=$GOBIN:$PATH

ツールを import するためのファイルを作成する

ツールの package を import するためのファイルを作成します。先頭行のコメントは Build Constraints と呼ばれるもので、デフォルトのビルド対象から除外することが出来ます。

$ cat tools.go
// +build tools

package tools

import (
   _ "github.com/goadesign/goa/goagen"
)

ツールをインストールする

ではツールをインストールしてみます。

$ go get github.com/goadesign/goa/goagen@master
go: finding github.com/goadesign/goa/goagen master
go: finding github.com/goadesign/goa master
go: finding github.com/manveru/faker latest
go: finding github.com/zach-klippenstein/goregen latest
go: finding github.com/dimfeld/httppath latest
go: finding github.com/spf13/pflag v1.0.3
go: finding github.com/spf13/cobra v0.0.3
go: finding golang.org/x/tools/go/ast/astutil latest
go: downloading github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: downloading github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: finding gopkg.in/yaml.v2 v2.2.2
go: downloading github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: downloading github.com/spf13/pflag v1.0.3
go: downloading github.com/spf13/cobra v0.0.3
go: finding golang.org/x/tools/go/ast latest
go: finding golang.org/x/tools/go latest
go: finding golang.org/x/tools latest
go: downloading golang.org/x/tools v0.0.0-20181218204010-d4971274fe38
go: downloading gopkg.in/yaml.v2 v2.2.2
go: finding gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
$ tree ./bin/
./bin/
└── goagen

0 directories, 1 file

$PWD/bin/goagen が作成されました。

インストールしたツールを使う

このプロジェクトには Makefile が含まれており、 make することで以下のタスクが実行されるようになっています。

  • 関連する package の go get
  • goagen によって前回生成されたコードの削除
  • goagen によるコードの再生成
  • go build

go get 対象の goa のパッケージに @master を付与しておきます。

diff --git a/Makefile b/Makefile
index 9790e3a..80a82d7 100644
--- a/Makefile
+++ b/Makefile
@@ -14,9 +14,9 @@
 # - all is the default target, it runs all the targets in the order above.
 #
 DEPEND=    bitbucket.org/pkg/inflect \
-  github.com/goadesign/goa \
-  github.com/goadesign/goa/goagen \
-  github.com/goadesign/goa/logging/logrus \
+   github.com/goadesign/goa@master \
+   github.com/goadesign/goa/goagen@master \
+   github.com/goadesign/goa/logging/logrus@master \
    github.com/sirupsen/logrus \
    gopkg.in/yaml.v2 \
    golang.org/x/tools/cmd/goimports

make します。

$ make
go: finding github.com/goadesign/goa/goagen master
go: finding github.com/goadesign/goa/logging/logrus master
go: finding github.com/goadesign/goa master
go: finding github.com/goadesign/goa/logging master
go: finding bitbucket.org/pkg/inflect latest
go: finding golang.org/x/tools/cmd/goimports latest
go: finding golang.org/x/tools/cmd latest
go: finding golang.org/x/tools latest
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
app/test
app/test/account_testing.go
app/test/bottle_testing.go
app/test/health_testing.go
app/test/js_testing.go
app/test/public_testing.go
app/test/swagger_testing.go
public/swagger
public/swagger/swagger.json
public/swagger/swagger.yaml
public/schema
public/schema/schema.json
tool/cellar-cli
tool/cellar-cli/main.go
tool/cli
tool/cli/commands.go
client
client/client.go
client/account.go
client/bottle.go
client/health.go
client/js.go
client/public.go
client/swagger.go
client/user_types.go
client/media_types.go
public/js
public/js/client.js
public/js/axios.min.js
public/js/index.html
public/js/example.go

無事にビルドできました。

余談ですが、 goagen は今まで GOPATH を前提とした実装になっていたので Go Modules では動きませんでした。実は今回の記事を書くにあたって Go Modules をサポートする Pull request を出したので goa をお使いの方は試してみてもらえると嬉しいです。 (記事の公開前にマージされて良かった😌)

github.com

依存解決でダウンロードされたパッケージ

実は今回の作業を始める前に $GOPATH/src を空にしておいたので、念のため中身を確認してみます。

$ tree ~/go/src/
/Users/tchssk/go/src/

0 directories, 0 files

何もないですね。

Go Modules は依存解決でダウンロードしたパッケージを $GOPATH/pkg/mod/ の配下にキャッシュするようです。

$ tree -L 2 ~/go/pkg/mod/
/Users/tchssk/go/pkg/mod/
├── bitbucket.org
│   └── pkg
├── cache
│   ├── download
│   └── vcs
├── github.com
│   ├── armon
│   ├── dimfeld
│   ├── go-kit
│   ├── go-logfmt
│   ├── goadesign
│   ├── hashicorp
│   ├── manveru
│   ├── satori
│   ├── sirupsen
│   ├── spf13
│   └── zach-klippenstein
├── golang.org
│   └── x
└── gopkg.in
    └── yaml.v2@v2.2.2

21 directories, 0 files

このキャッシュは go clean -modcache で削除することが出来ます。

$ go clean -modcache
$ tree -L 2 ~/go/pkg/mod/
/Users/tchssk/go/pkg/mod/ [error opening dir]

0 directories, 0 files

また、依存パッケージは go mod vendor でプロジェクト内にコピーすることもできます。

$ go mod vendor                
go: finding github.com/hashicorp/go-immutable-radix v1.0.0
go: finding github.com/spf13/pflag v1.0.3          
go: finding github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: finding github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: finding github.com/goadesign/goa v1.0.1-0.20181216194621-14b7b9950e87
go: finding github.com/sirupsen/logrus v1.2.0      
go: finding github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: finding github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
go: finding golang.org/x/tools v0.0.0-20181220024903-92cdcd90bf52
go: finding golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: finding github.com/go-logfmt/logfmt v0.4.0        
go: finding github.com/pmezard/go-difflib v1.0.0      
go: finding github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: finding golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
go: finding github.com/davecgh/go-spew v1.1.1         
go: finding github.com/stretchr/testify v1.2.2       
go: finding github.com/hashicorp/go-uuid v1.0.0     
go: finding github.com/konsorten/go-windows-terminal-sequences v1.0.1
go: finding github.com/dimfeld/httptreemux v5.0.1+incompatible
go: finding github.com/stretchr/objx v0.1.1
go: finding github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding github.com/go-kit/kit v0.8.0                  
go: finding github.com/spf13/cobra v0.0.3                 
go: finding github.com/hashicorp/golang-lru v0.5.0               
go: finding golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
go: finding bitbucket.org/pkg/inflect v0.0.0-20130829110746-8961c3750a47
go: finding gopkg.in/yaml.v2 v2.2.2
go: finding gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
go: downloading github.com/goadesign/goa v1.0.1-0.20181216194621-14b7b9950e87
go: downloading golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: downloading github.com/spf13/cobra v0.0.3                          
go: downloading github.com/go-kit/kit v0.8.0                  
go: downloading github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
go: downloading gopkg.in/yaml.v2 v2.2.2
go: downloading github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: downloading github.com/spf13/pflag v1.0.3
go: downloading github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: downloading github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: downloading github.com/dimfeld/httptreemux v5.0.1+incompatible
go: downloading github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: downloading golang.org/x/tools v0.0.0-20181220024903-92cdcd90bf52
go: downloading github.com/hashicorp/go-immutable-radix v1.0.0
go: downloading github.com/hashicorp/golang-lru v0.5.0
go: finding github.com/inconshreveable/mousetrap v1.0.0       
go: downloading github.com/go-logfmt/logfmt v0.4.0       
go: downloading github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding google.golang.org/appengine v1.3.0               
go: downloading github.com/inconshreveable/mousetrap v1.0.0   
go: downloading google.golang.org/appengine v1.3.0
go: finding golang.org/x/net v0.0.0-20180724234803-3673e40ba225  
go: finding github.com/golang/protobuf v1.2.0
go: finding golang.org/x/text v0.3.0    
go: downloading github.com/golang/protobuf v1.2.0
$ tree -L 2 ./vendor/
./vendor/
├── github.com
│   ├── armon
│   ├── dimfeld
│   ├── go-kit
│   ├── go-logfmt
│   ├── goadesign
│   ├── golang
│   ├── hashicorp
│   ├── inconshreveable
│   ├── kr
│   ├── manveru
│   ├── satori
│   ├── spf13
│   └── zach-klippenstein
├── golang.org
│   └── x
├── google.golang.org
│   └── appengine
├── gopkg.in
│   └── yaml.v2
└── modules.txt

20 directories, 1 file

Go Modules の今後

この記事を書いていたらちょうど The Go Blog で Go Modules の記事が公開されていました。

Go Modules in 2019 - The Go Blog

Go 1.12, scheduled for February 2019, will refine module support but still leave it in auto mode by default.

Our aim is for Go 1.13, scheduled for August 2019, to enable module mode by default (that is, to change the default from auto to on) and deprecate GOPATH mode.

2019 年 2 月予定の Go 1.12 は現在と同じようにデフォルトで auto モードで動作するようですね。 Module モードをデフォルトにするのは 2019 年 8 月予定の Go 1.13 からの予定で、そのときには GOPATH モードを非推奨にするということです。楽しみですね。