日本時間の 8/25 早朝,Go 1.11 がリリースされました 🎉 🎉
https://go.googlesource.com/go/+/go1.11
Go 1.11 には WebAssembly 対応を始めとするド派手な新機能からコアライブラリ・ツールチェインの細かな改善まで,いろいろな変更が入っています.個人的には golang.org/x/tools/go/packages
が気になっていました../...
がうまく扱える API かな?とか思ってたけど,内部で普通に go list
叩いちゃってるのがかわいい.
そして,Go 1.11 には現実世界を生きる Gopher にとっては待望ともいえる機能が Experimental ですが入ってます.それが Modules です.今年2月に提案され話題となった Go & Versioning およびそのプロトタイプ実装であった vgo が go
コマンドに取り込まれ, Modules という名前でリリースされました.
この記事では,この Modules を実際のプロジェクトに導入しようと試みたうえで,実際の開発フローに取り入れていくために何が優れており・何が足りないかを考えてみたいと思います.
現状の依存管理ツール - dep
dep は Go が公式に "実験的に" 作っている依存管理ツールです.glide など以前から存在していたツールからの migration の仕組みも持っており,dep への移行を促すプロジェクトも多いです.
dep やそれ以前の Go における dependency management の歴史・変遷も面白いのですが,そのあたりは Go & Versioning やその翻訳記事・vgo 関連のトークスライドなどにまとまっているため今回は深くは触れません.
Wantedly における Go プロジェクトの依存管理は,去年の10月頃に glide から dep に移行しました.マイクロサービスもしくは CLI での利用が主ですので,そこまで大規模な事例はないですが,1年ほど使い続けて感じた dep の「難しかったところ」をあげてみます.
- Docker による開発環境との相性がよくない
- Wantedly によくあるプロジェクトでは,コマンド一発で docker 上でアプリケーションが起動するようになっている
- Docker 上の開発環境では,Docker コンテナが死んだときに依存を入れ直さなくていいように,
vendor
ディレクトリはコンテナとは別 volume にすることが多い - 一方で,
dep
は vendor
への書き込みが失敗したときにロールバックできるように,dep ensure
時に既存の vendor
ディレクトリを vendor.orig
にリネームする - このとき,ディレクトリの owner がおかしなことになって
dep ensure
が失敗してしまう - (Go はどこでも動くので, Docker 上での開発をやめても問題ないといえばないけど…)
- 標準ツールではないので,CI や本番ビルドで使うのが面倒
curl -L -s
https://.../dep-linux-amd64
-o ./bin/dep chmod +x ./bin/dep
をマイクロサービス増やすたびに3回書く(開発用 Dockerfile, 本番用 Dockerfile,CI)dep
は決定版というもののでもないので,dep ensure -vendor-only
や dep ensure -add <pkg>
などコマンドを覚えるのがしんどい
- executable なツールの依存をうまく扱えない
mockgen
や protoc-gen-*
など- とりあえず
required
にいれといて,make setup
で go build -o bin/bar github.com/foo/bar
するような運用でカバーしてる
(※ いっぱい書きましたが, dep は存在しなかった時代を考えたくないくらいには良いツールです)
特に 普段 Go を触らない人たちにとっては,よくわかんない感じに dep ensure
が落ちる・コマンド体型が難しい(他ではあまり見ない)など,Developer Experience 的にしんどい場面が多々ありました.
Modules (vgo) を試す
試すための準備
GO111MODULE
という環境変数を on
にします- direnv 等で,作業ディレクトリ中は常に on になるようにしておくと便利です
- go を 1.11 にします
dep -> Modules への移行
これは dep への移行時と同じく,Gopkg.lock
を読んでいい感じにする機能が入っています.なので,基本的には go mod init
をしたあとに普通にビルド(go build
)するだけです.
$ go mod init
go: creating new go.mod: module github.com/wantedly/sugoi-api
go: copying requirements from Gopkg.lock
$ grapi build # go build
go: finding github.com/spf13/cobra v0.0.3
go: finding github.com/philhofer/fwd v1.0.0
go: finding github.com/pkg/errors v0.8.0
...
実プロジェクトで試したところ,fork したリポジトリを参照している場合(Gopkg.toml
で source
を指定した場合)にビルドに失敗しました.ここまでは見てくれないようなので,いい感じにコミットハッシュを書き換えます.
package github.com/wantedly/sugoi-api
require (
// snip.
// パッケージは fork 元だが,コミットハッシュは fork 後のやつ
github.com/basvanbeek/ocsql v0.0.0-20180807130626-1b1c0827cbf1
// snip.
)
+// fork 元 => fork 後
+replace github.com/basvanbeek/ocsql v0.0.0-20180807130626-d946a62502e3 => github.com/bgpat/ocsql v0.0.0-20180807130626-1b1c0827cbf1
これで再度ビルドすると,正しく実行できるバイナリが生成されました.
今回踏まなかっただけで,dep のマイナー機能を使っている場合は無視される可能性があります.そのときは気合でがんばりましょう.
依存パッケージのダウンロード
dep
では dep ensure -vendor-only
で依存パッケージのダウンロードをしていました.他の言語だとRuby の bundle install
や Node.js の npm install
もしくは yarn
に相当します.
一方 Modules が有効になった環境であれば,go build
や go test
時などに必要なパッケージを勝手にダウンロードしてくれます.
$ go test ./...
go: finding github.com/google/go-cmp v0.2.0
go: downloading github.com/google/go-cmp v0.2.0
? github.com/wantedly/sugoi-api/cmd/server [no test files]
...
便利.
明示的な依存パッケージのダウンロードは go mod download
で可能です.
$ go mod download
go: finding github.com/google/go-cmp v0.2.0
依存パッケージの追加
Modules では,go.mod
が存在するディレクトリで go get
をするだけで適切にバージョン管理されます.
$ go get github.com/golang/mock
$ git diff
diff --git a/go.mod b/go.mod
index c405511..edc0042 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
github.com/go-sql-driver/mysql v1.4.0 // indirect
github.com/gogo/protobuf v1.0.0
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
+ github.com/golang/mock v1.1.1 // indirect
github.com/golang/protobuf v0.0.0-20180712042225-70b3af33377e
github.com/gomodule/redigo v2.0.0+incompatible
github.com/google/go-cmp v0.2.0
diff --git a/go.sum b/go.sum
index 686e134..eeeeaa6 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,8 @@ github.com/gogo/protobuf v1.0.0 h1:2jyBKDKU/8v3v2xVR2PtiWQviFUyiaGk2rpfyFT8rTM=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v0.0.0-20180712042225-70b3af33377e h1:+juDoIDv8ng1gAwO/VZdSNzDb1HiSE6Bt+4a4g0nVWU=
github.com/golang/protobuf v0.0.0-20180712042225-70b3af33377e/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
便利.
executable なツールの依存管理
これに関してはあまり状況は変わってないです.議論自体はされていて,とりあえずのBest practice 的なものが紹介されています.下記のようなファイルを tools.go
として置いておいて,go.mod
に管理させることで既存の go mod
を使ったフローに乗れるそうです.
// +build tools
package tools
import (
_ "golang.org/x/tools/cmd/stringer"
)
Modules で管理されてるディレクトリでは go build
や go install
はその go.mod
のコンテキストで実行されるので,正しくバージョン管理されたバイナリを作ることができます.
ただ,流石に tools.go
書いて go build -o bin/bar github.com/foo/bar
して…を毎回やるのはしんどいです.そこで,gex というツールを書いてみました.
# `go get` して `go.mod` に反映し,`tools.go` にも書く
$ gex --add golang.org/x/tools/cmd/stringer
# 必要に応じて `go build` され,`$PWD/bin` 以下にバイナリを吐きそれを実行する
$ gex stringer
Modules 環境じゃなくても使える気がするので,executable なツールのバージョン管理に困っていた人は試してみてください.
アプリケーションエンジニアとしての所感
Modules を使ってみて最も良いと思ったのは,やはり go get
をするだけで依存パッケージを追加できるという最もシンプルなフローに戻ってこれることでした.また,git pull
直後など足りないパッケージがあれば勝手に落としてきてくれるのもかなり好印象です.Go にバンドルされると CI のセットアップ等が超楽になるのも嬉しい!
一方で,dep からの移行をいますぐやるかというとかなり悩ましいです.Modules は未だ Experimental なので何が起きるかわからないというのが大きな問題です.また,dep などからの移行パスが完全に用意されているわけではないので,そこそこの規模のプロジェクトではある程度歯を食いしばる覚悟が必要かもしれません.
(個人的に)期待していた executable なツールの管理ですが,これはまだまだ検討段階といった感じでした.Best practice にそったツールも作ってみたので,いろいろ試して本家にフィードバックできたらと考えています.
全体的にかなり体験がいいので,自分が Owner になっているマイクロサービス等で小さく master に投入して試していくのはやってみようかと思います.
その他,考えること
CLI・ツール開発者にとっては,これまで以上に「go get
されてもちゃんと動くこと」を強く意識する必要がありそうです.よくある「ビルド時に ldflags
でバージョン番号を埋め込む」方式などは,go get
されると悲しい感じになります.一方で,今後は go get
がタグをサポートすることになるので,そういう意味では開発は楽になるかもしれません.
ライブラリ開発者にとっては,…ちゃんと Semantic Versioning に従おうな というアタリマエのことが最重要です.breaking change はなるべく避け,避けきれないときは major version 上げましょう.あと,自分がメンテしてるライブラリに go.mod
を置くのを早めにやったほうが喜ばれそうです.
さいごに
この記事では,Go 1.11 で導入された Modules を実際に導入し(ようとし)てみてぶつかった問題や考えるべきことについて述べました.記事中に何度も書いてますが,Modules は Experimental な機能です.導入は自己責任でお願いします(?).
しかし,自分たちがたくさん使ってフィードバックしていくことで,本リリースされたときによりよいもの・欲しいものが出やすくなると思います.用法用量をまもって正しく導入しようとしてみましょう.