このgistは Cloud Foundry Advent Calendar 2013 の16日目の記事です。
現在、CloudFoundryのComponentsはGo化しつつあります。それにより、Rubyで実装されていたものに対して性能向上していたり、ソースが読みやすくなっていたりする(こちらは主観ですが・・)半面、開発者にとっての課題も生まれています。その課題のひとつが__依存パッケージ管理__です。まずはRubyの外部パッケージ管理について簡単に振り返りつつ、Goのそれを見ていこうと思います。
Rubyでは外部パッケージはGemファイルとなっており、大抵の場合、Bundlerで管理します。また、最新版が動くとは限らないため設定ファイルにバージョンを指定してそれを使用します。Gemファイル自体はRubyGems.orgに置かれており、ここからダウンロードされます。
Goではソースコード中のimport句でパッケージを指定します。設定ファイルは使いません。以下に例を示します。
import (
"os"
"github.com/codegangsta/cli"
)
- 2行目の"os"は、Goの標準パッケージです。
- 3行目に"github.com"から始まるパッケージを書いています。こうしておくと、以下の手順でgithubからパッケージを取得します。
上記のimport文を含む.goファイルに対し、go getを行った際に
- github上のソースをcloneして$GOPATH/src以下に配置します。
- 1.のファイル(masterブランチの最新)をコンパイルします。
Revision指定できないの??と思った方。鋭いです。お察しの通り__できません__(少なくとも現行バージョンの1.2では不可能です)。
Goの公式ページ(FAQ)では以下の2点を理由としています。
- Goの利用者が直面する膨大な状況に対し、うまく作用するアプローチを知らない。
- パブリックなパッケージは常に後方互換性を持たせるべきで、異なる機能性を持たせたいときは、パッケージ自体の名前を変えて、import pathも変更するべき。
一つ目の理由を読む限りでは、今後、何らかの形でバージョン指定ができるようになるかもしれません。
二つ目の理由は共感できる部分もあるのですが、問題はこの作法に従うかどうかはパッケージ作成者に委ねられることです。加えて言うならば、masterにバグが入ることが考慮されていません。例えば、パッケージ利用者には必要のない機能追加によってバグが入り込む、最悪動作しないといった悲劇が起きます。
※余談ですが、Goはデフォルト引数が使えないのと、関数のオーバーロードが使えない仕様のために、他の言語に比べて後方互換性を持たせるのが難しいと思いました。
もし、依存パッケージが意図せず変更されてしまうのが嫌ならば、依存パッケージをローカルリポジトリにコピーして、そちらにimportパスを変更する方法をとるのが良いとしています(実際にGoogle内部で取っている方法だそうです)。
はい。依存パッケージが意図せず変更されてしまうのが嫌なので、依存パッケージをローカルリポジトリにコピーしてimportパスを変更しましょう。
ということで、gorouterで使っているパッケージにこの変更を行ってみます。
## workingディレクトリを作成
$ mkdir gopath
$ cd gopath
$ export GOPATH=`pwd`
## githubからgorouterをclone
$ mkdir -p src/github.com/cloudfoundry
$ cd src/github.com/cloudfoundry
$ git clone https://github.com/cloudfoundry/gorouter.git
$ cd gorouter
$ go get -v
なお、ここでsrcディレクトリ下のディレクトリ構造は以下のようになっています。gorouter以外はgo getで取ってきたパッケージですね。
$ tree ../../../ -L 3 -d
../../../
├── code.google.com
│ └── p
│ └── gogoprotobuf
├── github.com
│ ├── cloudfoundry
│ │ ├── gorouter
│ │ ├── gosteno
│ │ ├── loggregatorlib
│ │ └── yagnats
│ └── rcrowley
│ └── go-metrics
└── launchpad.net
└── goyaml
「依存パッケージをローカルリポジトリにコピーして、importパスを変更する」と書きましたが、具体的には以下を実施する必要があります。(わかりやすくするため、依存パッケージとしてyagnatsパッケージのみを対象にしています)
- 依存パッケージ(yagnats)をローカルリポジトリ(gorouter)内にコピーする。
- ローカルリポジトリ(gorouter)内のimport句でyagnatsを指定しているパスをすべてローカルリポジトリを向くように書き換える
- VCSのメタデータファイル(.git)を削除する
面倒ですね!実は、これを自動で行ってくれるgovenというツールがあります。早速使ってみましょう。
go getしたパッケージは自動的にビルドするので、下記コマンドを実行するだけでgovenが使えるようになります。go getすごい!
$ go get github.com/kr/goven
goven実行時に変更があったファイルが一覧で出力されます。
$ $GOPATH/bin/goven github.com/cloudfoundry/yagnats
integration_test.go
perf_test.go
router.go
router_test.go
importパスが書きかえられています。
$ git diff integration_test.go
diff --git a/integration_test.go b/integration_test.go
index 633b6ba..d69c2e6 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -4,7 +4,7 @@ import (
"os/exec"
"time"
- "github.com/cloudfoundry/yagnats"
+ "github.com/cloudfoundry/gorouter/github.com/cloudfoundry/yagnats"
. "launchpad.net/gocheck"
"github.com/cloudfoundry/gorouter/config"
yagnatsがローカルリポジトリにコピーされ、.gitファイルも削除されています。
$ ls -a ../../../../src/github.com/cloudfoundry/gorouter/github.com/cloudfoundry/yagnats/
. .travis.yml client.go connection_test.go helpers_test.go packets_test.go
.. LICENSE client_test.go examples logger.go parser.go
.gitignore README.md connection.go fakeyagnats packets.go parser_test.go
前項では、依存パッケージのバージョンを固定していましたが、依存パッケージも含めて開発を行っている場合など、バージョンを固定したくないときもあります。そんなときはバージョン管理システムのサブモジュール機能を利用しましょう。
importパスは、govenが変更してくれたものを使うと簡単です。
$ rm -rf ../../../../src/github.com/cloudfoundry/gorouter/github.com/cloudfoundry/yagnats/
$ git submodule add git://github.com/cloudfoundry/yagnats.git github.com/cloudfoundry/yagnats
これで、go getする前に、git submodule update --init --recursiveを実行するだけで、意図したバージョンのパッケージが使えるようになりました。
- Goの依存パッケージのバージョンを固定するには、ローカルリポジトリにコピーしてしまうのがシンプルでよい。その際、govenを利用すると修正が楽。
- Goの依存パッケージのバージョンをコントロールしたいときは、別途バージョン管理システムを利用する必要がある。