A Tour of Versioned Go (vgo)
(Go & Versioning、パート 2)
※訳註1: この記事は、Russ Coxによって 2018 年 2 月 20 日に投稿された記事 “A Tour of Versioned Go (vgo)” の和訳版です。筆者に許可をとり、@nekketsuuu が和訳・公開しました。シリーズ物ですので、気になる方は全体をご覧ください。
※訳註2: 以下の内容は Go 1.9 以下では動作しません。Go 1.10 以降を使ってください。
私にとって設計することとは、作って、壊して、また作って、それを繰り返すことだ。新しいバージョン付けの提案を書くにあたって、私はプロトタイプとして vgo
を作り、細かい詳細を詰めた。この投稿はどのような感じで vgo
が使えるのかを示す。
今、vgo
は go get golang.org/x/vgo
を実行することでダウンロードでき、試せる。Vgo
は go
コマンドの完全な互換品で (あり、フォークしたコピーでも) ある。Go
コマンドの代わりに vgo
コマンドを走らせると、vgo
は既に $GOROOT
へインストールされている標準のコンパイラとライブラリを使う (Go 1.10beta1 以降に対応)。
何が上手くいっていて何がそうでないのかを学んだ結果として、vgo
の意味論やコマンドラインの詳細が変わるかもしれない。しかし、我々は go.mod
ファイル・フォーマットの後方互換性を破るような変更は避け、今 go.mod
をプロジェクトに追加しても将来同様に使えるようにしたいと考えている。今回の提案を洗練させた結果として、vgo
をアップデートすることになるだろう。
例
この節は vgo
の使い方をデモする。あなた独自の環境のもとで以下の実験を試して欲しい。
まずは、vgo
をインストールしよう。
$ go get -u golang.org/x/vgo
Vgo
はサッとテストしただけなので、もしかすると面白いバグに遭遇するかもしれない。Issue を報告するには、Go 本体の issue tracker を使い、タイトルの先頭に「x/vgo:」をつけてほしい。
Hello, world (あるいは、こんにちは世界)
それでは面白い “hello, world” を書いてみよう。GOPATH/src の外側にディレクトリを作り、その中で作業しよう。
$ cd $HOME
$ mkdir hello
$ cd hello
次に hello.go
ファイルを作る。
package main // import "github.com/you/hello"
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}
あるいは、ダウンロードもできる。
$ curl -sS https://swtch.com/hello.go >hello.go
空の go.mod
ファイルを作り、この module のルートを示す。そうしたらこのプログラムをビルドして実行してみよう。
$ echo >go.mod
$ vgo build
vgo: resolving import "rsc.io/quote"
vgo: finding rsc.io/quote (latest)
vgo: adding rsc.io/quote v1.5.2
vgo: finding rsc.io/quote v1.5.2
vgo: finding rsc.io/sampler v1.3.0
vgo: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
vgo: downloading rsc.io/quote v1.5.2
vgo: downloading rsc.io/sampler v1.3.0
vgo: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
$ ./hello
Hello, world. (訳註: 「こんにちは世界」と出るかもしれない。)
$
明示的に vgo get
をしなかったことに注意してほしい。単に vgo build
をした場合、まだ知らない import に出会う度、それを有する module を探して、その最新バージョンを現在の module の必要要件として追加している。
任意の vgo
コマンドは副作用として、必要ならば go.mod
を書き換える。今回の場合、vgo build
は新しく go.mod
を書き換えている。
$ cat go.mod
module "github.com/you/hello"
require "rsc.io/quote" v1.5.2
$
このように go.mod
が作られたので、次の vgo build
は再度 import 解決をすることはなく、たくさん出力することもしない。
$ vgo build
$ ./hello
Hello, world.
$
もし rsc.io/quote
v1.5.3 や v1.6.0 が明日リリースされたとしても、明示的にアップグレードしない限り、このディレクトリにおけるビルドは v1.5.2 を使い続ける (下の方参照)。
この go.mod
ファイルは最小限の必要条件をリストアップしており、既にリストされているものから分かるものは省略されている。今回の場合、rsc.io/quote
v1.5.2 は rsc.io/sampler
と golang.org/x/text
の特定のバージョンを必要としているが、これは既に書かれているためこの go.mod
ファイルで繰り返すのは冗長であり、省略されている。
ただし vgo list -m
を使うと module がビルドに必要としている全てのものを出力させることができる。
$ vgo list -m
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$
ここで、もしかするとなぜこの単純な “hello world” が golang.org/x/text
を使っているのか不思議に思ったかもしれない。先程 rsc.io/quote
が rsc.io/sampler
に依存していることが分かったが、これは golang/x/text
を language matching のために使っているのだ。
$ LANG=fr ./hello
Bonjour le monde.
$
Upgrade
先程、新しい module のビルドに新しい import の解決が必要なら、vgo
は最新版を取ることを見た。さっきは rsc.io/quote
が必要とされ、v1.5.2 が最新だった。しかし新しい import を解決するときを除いて、vgo
は単に go.mod
に書かれたバージョンを使用する。今回の例では、rsc.io/quote
は間接的に特定のバージョンの golang.org/x/text
と rsc.io/sampler
に依存していた。実は、先程の vgo list
コマンドに -u
(check for updated packages) を加えることで、それら両方のパッケージにはより新しいリリースがあることが分かる。
$ vgo list -m -u
MODULE VERSION LATEST
github.com/you/hello - -
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.5.2 (2018-02-14 10:44) -
rsc.io/sampler v1.3.0 (2018-02-13 14:05) v1.99.99 (2018-02-13 17:20)
$
両方のパッケージが新しいリリースをしているので、hello
プログラムのためにこれらをアップグレードしたいとしよう。
それではまず golang.org/x/text
をアップグレードしてみよう。
$ vgo get golang.org/x/text
vgo: finding golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
vgo: downloading golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
$ cat go.mod
module "github.com/you/hello"
require (
"golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
"rsc.io/quote" v1.5.2
)
$
この vgo get
コマンドは与えられた module の最新版を探し、go.mod
を書き換えることでそれを今の module の必要条件として追記する。さて、これで次のビルドでは新しい text module が使われることとなった。
$ vgo list -m
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$
もちろん、アップグレードした後ちゃんと上手くいくことをテストすべきだ。今回の依存 module rsc.io/quote
と rsc.io/sampler
はまだ新しい text module と一緒にテストされていなかった。今作った設定のもとで、次のようにするとテストを走らせることができる。
$ vgo test all
? github.com/you/hello [no test files]
? golang.org/x/text/internal/gen [no test files]
ok golang.org/x/text/internal/tag 0.020s
? golang.org/x/text/internal/testtext [no test files]
ok golang.org/x/text/internal/ucd 0.020s
ok golang.org/x/text/language 0.068s
ok golang.org/x/text/unicode/cldr 0.063s
ok rsc.io/quote 0.015s
ok rsc.io/sampler 0.016s
$
元々の go
コマンドでは、パッケージに all
と書くと GOPATH から見つかる全てのパッケージのことを意味していた。多くの場合それは多すぎるので、vgo
では all
の意味を「今の module 中の全てのパッケージと、それらが再帰的に import している全てのパッケージ」に狭めた。さて、rsc.io/quote
module のバージョン 1.5.2 はバグのあるパッケージを含んでいる。
$ vgo test rsc.io/quote/...
ok rsc.io/quote (cached)
--- FAIL: Test (0.00s)
buggy_test.go:10: buggy!
FAIL
FAIL rsc.io/quote/buggy 0.014s
(exit status 1)
$
我々の module に含まれる何かにバグがあったわけだが、しかしこれは我々には関係ないので、これは all
には含まれていなかったというわけである。どのテストでも、アップデートされた x/test
は上手く動くようだ。この時点で、go.mod
をコミットして良いだろう。
別のオプションとして、ビルドで必要になる全ての module をアップグレードすることもできる。これには vgo get -u
を用いる。
$ vgo get -u
vgo: finding golang.org/x/text latest
vgo: finding rsc.io/quote latest
vgo: finding rsc.io/sampler latest
vgo: finding rsc.io/sampler v1.99.99
vgo: finding golang.org/x/text latest
vgo: downloading rsc.io/sampler v1.99.99
$ cat go.mod
module "github.com/you/hello"
require (
"golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
"rsc.io/quote" v1.5.2
"rsc.io/sampler" v1.99.99
)
$
ここで vgo get -u
は text module をアップグレードされたままにし、同様に rsc.io/sampler
も最新版の v1.99.99 にアップグレードされた。
もう一度テストを走らせてみよう。
$ vgo test all
? github.com/you/hello [no test files]
? golang.org/x/text/internal/gen [no test files]
ok golang.org/x/text/internal/tag (cached)
? golang.org/x/text/internal/testtext [no test files]
ok golang.org/x/text/internal/ucd (cached)
ok golang.org/x/text/language 0.070s
ok golang.org/x/text/unicode/cldr (cached)
--- FAIL: TestHello (0.00s)
quote_test.go:19: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
FAIL rsc.io/quote 0.014s
--- FAIL: TestHello (0.00s)
hello_test.go:31: Hello([en-US fr]) = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
hello_test.go:31: Hello([fr en-US]) = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Bonjour le monde."
FAIL
FAIL rsc.io/sampler 0.014s
(exit status 1)
$
どうやら rsc.io/sampler
v1.99.99 の何かがおかしいようだ。確かにそうだ。
$ vgo build
$ ./hello
99 bottles of beer on the wall, 99 bottles of beer, ...
$
この vgo get -u
が依存する全てのものを最新版に引き上げる挙動は、go get
が GOPATH にないパッケージをダウンロードしてくるときのものと一緒だ。もし GOPATH になにも無いシステムであれば、下のようになる。
$ go get -d rsc.io/hello
$ go build -o badhello rsc.io/hello
$ ./badhello
99 bottles of beer on the wall, 99 bottles of beer, ...
$
とても重要な差異として vgo
はデフォルトではこのように動作しない。また、ダウングレードによってアンドゥをすることもできる。
Downgrade
パッケージをダウングレードするには、まず vgo list -t
を使い、タグ付けされていて利用可能なバージョンを調べよう。
$ vgo list -t rsc.io/sampler
rsc.io/sampler
v1.0.0
v1.2.0
v1.2.1
v1.3.0
v1.3.1
v1.99.99
$
そうしたら vgo get
によって特定のバージョンにすることができる。たとえば v1.3.1 にしてみよう。
$ cat go.mod
module "github.com/you/hello"
require (
"golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
"rsc.io/quote" v1.5.2
"rsc.io/sampler" v1.99.99
)
$ vgo get rsc.io/sampler@v1.3.1
vgo: finding rsc.io/sampler v1.3.1
vgo: downloading rsc.io/sampler v1.3.1
$ vgo list -m
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.1
$ cat go.mod
module "github.com/you/hello"
require (
"golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
"rsc.io/quote" v1.5.2
"rsc.io/sampler" v1.3.1
)
$ vgo test all
? github.com/you/hello [no test files]
? golang.org/x/text/internal/gen [no test files]
ok golang.org/x/text/internal/tag (cached)
? golang.org/x/text/internal/testtext [no test files]
ok golang.org/x/text/internal/ucd (cached)
ok golang.org/x/text/language (cached)
ok golang.org/x/text/unicode/cldr (cached)
ok rsc.io/quote 0.016s
ok rsc.io/sampler 0.015s
$
あるパッケージをダウングレードしたら、他のものもダウングレードしないといけないかもしれない。たとえば下のように。
$ vgo get rsc.io/sampler@v1.2.0
vgo: finding rsc.io/sampler v1.2.0
vgo: finding rsc.io/quote v1.5.1
vgo: finding rsc.io/quote v1.5.0
vgo: finding rsc.io/quote v1.4.0
vgo: finding rsc.io/sampler v1.0.0
vgo: downloading rsc.io/sampler v1.2.0
$ vgo list -m
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.4.0
rsc.io/sampler v1.2.0
$ cat go.mod
module "github.com/you/hello"
require (
"golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
"rsc.io/quote" v1.4.0
"rsc.io/sampler" v1.2.0
)
$
ダウングレードの究極の形として、依存しているものを全て削除することもできる。これにはバージョンとして none
を指定する。
$ vgo get rsc.io/sampler@none
vgo: downloading rsc.io/quote v1.4.0
vgo: finding rsc.io/quote v1.3.0
$ vgo list -m
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.3.0
$ cat go.mod
module "github.com/you/hello"
require (
"golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
"rsc.io/quote" v1.3.0
)
$ vgo test all
vgo: downloading rsc.io/quote v1.3.0
? github.com/you/hello [no test files]
ok rsc.io/quote 0.014s
$
それでは、全てが最新版であった状態に戻って、rsc.io/sampler
も v1.99.99 にしておこう。
$ vgo get -u
vgo: finding golang.org/x/text latest
vgo: finding rsc.io/quote latest
vgo: finding rsc.io/sampler latest
vgo: finding golang.org/x/text latest
$ vgo list -m
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.5.2
rsc.io/sampler v1.99.99
$
Exclude
今回の hello world にとって v1.99.99 が良くないことが分かったので、将来的な問題を避けるために、このことをどこかに記録しておきたいとしよう。これは go.mod
に exclude
命令を追加することで行うことができる。
exclude "rsc.io/sampler" v1.99.99
するとここからの操作はまるでその module が存在しないかのように振る舞う。
$ echo 'exclude "rsc.io/sampler" v1.99.99' >>go.mod
$ vgo list -t rsc.io/sampler
rsc.io/sampler
v1.0.0
v1.2.0
v1.2.1
v1.3.0
v1.3.1
v1.99.99 # excluded
$ vgo get -u
vgo: finding golang.org/x/text latest
vgo: finding rsc.io/quote latest
vgo: finding rsc.io/sampler latest
vgo: finding rsc.io/sampler latest
vgo: finding golang.org/x/text latest
$ vgo list -m
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.1
$ cat go.mod
module "github.com/you/hello"
require (
"golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
"rsc.io/quote" v1.5.2
"rsc.io/sampler" v1.3.1
)
exclude "rsc.io/sampler" v1.99.99
$ vgo test all
? github.com/you/hello [no test files]
? golang.org/x/text/internal/gen [no test files]
ok golang.org/x/text/internal/tag (cached)
? golang.org/x/text/internal/testtext [no test files]
ok golang.org/x/text/internal/ucd (cached)
ok golang.org/x/text/language (cached)
ok golang.org/x/text/unicode/cldr (cached)
ok rsc.io/quote (cached)
ok rsc.io/sampler (cached)
$
この exclude
は、今の module をビルドするときにのみ適用される。もし今の module がより大きなビルドによって必要とされたとき、この exclude
は適用されない。たとえば、rsc.io/quote
の go.mod
に書かれた exclude
は我々の “hello, world” のビルドには適用されなかった。この取り決めによって、module の作者にとっては自身のビルドに対してほぼ何でも制御できるようにする一方、その module に依存する module にとってはほとんどどんな制御も制限しない。
さて、今すべき次の行動は、rsc.io/sampler
の作者に連絡をとって、v1.99.99 の問題を報告し、それが v1.99.100 で修正されるようにすることだ。しかし残念なことに、その作者はそのバグを直さないというブログ記事を出してしまった。
Replace
依存関係の問題を特定できたなら、それが修復されたコピーを代わりに使って、一時的に何とかする必要がある。それでは rsc.io/quote
の何らかの挙動を変えたいとしよう。もしかするとこの問題は rsc.io/sampler
の方で対処できるかもしれないし、それ以外の方法で何とかなるかもしれない。まずは通常の git コマンドを使って quote
module を見てみよう。
$ git clone https://github.com/rsc/quote ../quote
Cloning into '../quote'...
そして ../quote/quote.go
を編集して func Hello
の何かを変更しよう。たとえばここでは、戻り値を sampler.Hello()
から sampler.Glass()
に変更し、もっと面白い挨拶にしてみる。
$ cd ../quote
$ <edit quote.go>
$
フォークを変更したら、変更命令を go.mod
に書き加えることで、実際のものの代わりに変更したものをビルドで使うことができる。
replace "rsc.io/quote" v1.5.2 => "../quote"
このようにして、変更されたものを使ってプログラムをビルドできる。
$ cd ../hello
$ echo 'replace "rsc.io/quote" v1.5.2 => "../quote"' >>go.mod
$ vgo list -m
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.5.2
=> ../quote
rsc.io/sampler v1.3.1
$ vgo build
$ ./hello
I can eat glass and it doesn't hurt me.
$
変更後のものに異なる module の名前を与えることもできる。たとえば github.com/rsc/quote をフォークして、何かしら変更を push したとする。
$ cd ../quote
$ git commit -a -m 'my fork'
[master 6151719] my fork
1 file changed, 1 insertion(+), 1 deletion(-)
$ git tag v0.0.0-myfork
$ git push https://github.com/you/quote v0.0.0-myfork
To https://github.com/you/quote
* [new tag] v0.0.0-myfork -> v0.0.0-myfork
$
するとそれを変更後のものとして使うことができる。
$ cd ../hello
$ echo 'replace "rsc.io/quote" v1.5.2 => "github.com/you/quote" v0.0.0-myfork' >>go.mod
$ vgo list -m
vgo: finding github.com/you/quote v0.0.0-myfork
MODULE VERSION
github.com/you/hello -
golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote v1.5.2
=> github.com/you/quote v0.0.0-myfork
rsc.io/sampler v1.3.1
$ vgo build
vgo: downloading github.com/you/quote v0.0.0-myfork
$ LANG=fr ./hello
Je peux manger du verre, ça ne me fait pas mal.
$
後方互換性
たとえ vgo
を自分のプロジェクトで使いたいと思ったとしても、おそらく全てのユーザーに vgo
を使わせたいとまでは思わないだろう。その代わり、go
コマンドのユーザーにほとんど同じビルドをさせるためのベンダー・ディレクトリを作成することができる (もちろん GOPATH の中でビルドすることになる)。
$ vgo vendor
$ mkdir -p $GOPATH/src/github.com/you
$ cp -a . $GOPATH/src/github.com/you/hello
$ go build -o vhello github.com/you/hello
$ LANG=es ./vhello
Puedo comer vidrio, no me hace daño.
$
「ほとんど同じ」ビルドだといったが、これはツールチェインから見えている、最終的なバイナリに記録される import path が異なるからだ。ベンダリングされたビルドでは、ベンダー・ディレクトリが見える。
$ go tool nm hello | grep sampler.hello
1170908 B rsc.io/sampler.hello
$ go tool nm vhello | grep sampler.hello
11718e8 B github.com/you/hello/vendor/rsc.io/sampler.hello
$
What's Next?
是非 vgo
を試して欲しい。あなたのリポジトリをバージョンでタグ付けし始めて欲しい。まず go.mod
ファイルを作成し、確認してみよう。Issue は golang.org/issue に、タイトルの先頭に「x/vgo:」を付けた上で投稿して欲しい。明日は更に投稿する予定だ。それでは!
訳註
翻訳ミス、誤字等の指摘は、編集リクエストやこのページへのコメントで行って頂けると幸いです。
記事の内容そのものに対する指摘は、Russ による元の記事へのコメントや、Go subreddit への投稿、golang-nuts への投稿をお願いします。意見が Russ に届くことが重要なので、日本語では意味が薄そうです。
ライセンス表示: この記事は、Russ Cox によって書かれたものを @nekketsuuu が翻訳・公開したものです。CC BY 4.0 ライセンスの元で公開します。