Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Goと依存パッケージ管理

このgistは Cloud Foundry Advent Calendar 2013 の16日目の記事です。

はじめに

現在、CloudFoundryのComponentsはGo化しつつあります。それにより、Rubyで実装されていたものに対して性能向上していたり、ソースが読みやすくなっていたりする(こちらは主観ですが・・)半面、開発者にとっての課題も生まれています。その課題のひとつが__依存パッケージ管理__です。まずはRubyの外部パッケージ管理について簡単に振り返りつつ、Goのそれを見ていこうと思います。

依存パッケージ管理(Ruby)

Rubyでは外部パッケージはGemファイルとなっており、大抵の場合、Bundlerで管理します。また、最新版が動くとは限らないため設定ファイルにバージョンを指定してそれを使用します。Gemファイル自体はRubyGems.orgに置かれており、ここからダウンロードされます。

依存パッケージ管理(Go)

Goではソースコード中のimport句でパッケージを指定します。設定ファイルは使いません。以下に例を示します。

import (
        "os"
        "github.com/codegangsta/cli"
)
  • 2行目の"os"は、Goの標準パッケージです。
  • 3行目に"github.com"から始まるパッケージを書いています。こうしておくと、以下の手順でgithubからパッケージを取得します。

上記のimport文を含む.goファイルに対し、go getを行った際に

  1. github上のソースをcloneして$GOPATH/src以下に配置します。
  2. 1.のファイル(masterブランチの最新)をコンパイルします。

Revision指定できないの??と思った方。鋭いです。お察しの通り__できません__(少なくとも現行バージョンの1.2では不可能です)。

Goで依存パッケージのバージョン指定ができない理由

Goの公式ページ(FAQ)では以下の2点を理由としています。

  • Goの利用者が直面する膨大な状況に対し、うまく作用するアプローチを知らない。
  • パブリックなパッケージは常に後方互換性を持たせるべきで、異なる機能性を持たせたいときは、パッケージ自体の名前を変えて、import pathも変更するべき。

一つ目の理由を読む限りでは、今後、何らかの形でバージョン指定ができるようになるかもしれません。

二つ目の理由は共感できる部分もあるのですが、問題はこの作法に従うかどうかはパッケージ作成者に委ねられることです。加えて言うならば、masterにバグが入ることが考慮されていません。例えば、パッケージ利用者には必要のない機能追加によってバグが入り込む、最悪動作しないといった悲劇が起きます。
※余談ですが、Goはデフォルト引数が使えないのと、関数のオーバーロードが使えない仕様のために、他の言語に比べて後方互換性を持たせるのが難しいと思いました。

もし、依存パッケージが意図せず変更されてしまうのが嫌ならば、依存パッケージをローカルリポジトリにコピーして、そちらにimportパスを変更する方法をとるのが良いとしています(実際にGoogle内部で取っている方法だそうです)。

依存パッケージをローカルリポジトリにコピーしてimportパスを変更する

はい。依存パッケージが意図せず変更されてしまうのが嫌なので、依存パッケージをローカルリポジトリにコピーして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

git submodule を使う

前項では、依存パッケージのバージョンを固定していましたが、依存パッケージも含めて開発を行っている場合など、バージョンを固定したくないときもあります。そんなときはバージョン管理システムのサブモジュール機能を利用しましょう。

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の依存パッケージのバージョンをコントロールしたいときは、別途バージョン管理システムを利用する必要がある。

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment