DRYな備忘録

Don't Repeat Yourself.

Goのパッケージ管理ツールgodepの紹介と、僕がハマった罠

Goのプロジェクトの依存パッケージをどうするかという問題があります。

package myapp

import (
    "gopkg.in/hoge/fuga"
    "myserver.com/foo/bar"
    "buz"
)

func main() {
    // ...
}

などのアプリケーションをサーバにデプロイするとき、fugaパッケージやbarbuzパッケージをどこから調達するかです。

たとえばNodeJSならpackage.jsonnpm installが、RubyならGemfilebundle installがこのような依存パッケージを調達してくれます。

godep

Goにおける依存パッケージの解決方法のひとつに、godepというものがあります。

Goのパッケージマネージャの中ではいちばんメジャーなもののようです。(mattnさんのgomやrosylillyさんのgondlerもわたし気になります)

使い方

1. 依存関係を保存する

デプロイしたいアプリケーションのルートディレクトリでgodep saveを実行します*1

This will save a list of dependencies to the file Godeps/Godeps.json, and copy their source code into Godeps/_workspace.

これは、アプリケーションが参照するすべてのパッケージのソースコードを、ローカルマシンの$GOPATH/srcから探してきて、アプリケーションのルート直下のGodeps/_workspaceにコピーします*2。これと同時に、その依存パッケージのバージョンを含めたリスト情報をGodeps/Godeps.jsonに記録します。

2. 依存関係をリポジトリからインストールする

依存パッケージをgo getしていないマシンにおいて、godep resotoreを実行します。

The godep restore command is the opposite of godep save. It will install the package versions specified in Godeps/Godeps.json to your GOPATH.

Godeps.jsonの記述にしたがって、リモートのリポジトリから依存パッケージをインストール(go get)しようとします。しかしながらこれは、依存パッケージのリポジトリがgopkg.inやgithub.comなどのようにパブリックに(あるいはインターナルに到達可能に)ホスティングされている場合にのみ効果を発揮します。

3. そうではなく、saveしたソースコードから実行する

Goのバイナリはgo build``go install、あるいはgo runと同時に、生成され、それが実行されますが、普通は依存パッケージをローカルの$GOAPTHから解決しようとします. これらのコマンドの前にgodep go runなどとgodepをつけることで、これをGodeps/_workspaceから解決しようとします。

ためしに、(godep saveしてある前提で)

% cd myapp
% go run main.go
This is main.
% rm -rf $GOPATH/src/buz #依存パッケージを削除しちゃう
% go run main.go
main.go:3:8: cannot find package "buz" in any of:
    /Users/otiai10/.go/src/buz (from $GOROOT)
    /Users/otiai10/proj/go/src/buz (from $GOPATH)
# 叱られるが
% godep go run main.go
This is buz.Buz (method of imported package)

このように、godepプレフィックスをつけてgo * コマンドを実行すると、依存パッケージをGodeps/_workspaceから解決してビルドをつくる。

デプロイにあたって

したがって、サーバへデプロイする場合に、ローカルでアプリケーションの依存をgodep saveでアプリケーションのGodepsディレクトリに保存してしまい、デプロイスクリプトでこのアプリケーションを起動するときにgodep go run main.go(あるいはgodep go install myapp; myapp)とすればアプリケーションの依存は解決される(というか、一緒にデプロイしてしまうかんじ)ことになります。

パッケージの更新も保存しておく

とのことから、最も単純な開発フローにおいては依存パッケージ側で変更があればアプリケーションでそれをsaveする(godep update foo)をする必要があります。

ためしに、buzパッケージの一部分を編集して以下のようにしてコミットしてみました。

func Buz() {
-    fmt.Println("This is buz.Buz (method of imported package)")
+    fmt.Println("This is buz.Buz version 2!!")
}

そしてアプリケーションでgodep update buzをするとGodeps/_workspaceソースコードも更新されます。そしてgodep go run main.goとすると、

This is buz.Buz version 2!!

となるはずです。

僕がハマった罠

さらに、buzパッケージをv3に... する前に、godep go install ./ をしてみます。

すると実は、Godep/_workspace/pkg以下にbuzパッケージのv2のビルドが生成されます。実はこれがくせ者でした。

あとは普通にbuzパッケージをv3にします。

func Buz() {
-    fmt.Println("This is buz.Buz version 2!!")
+    fmt.Println("v3だよ! This is buz.Buz")
}

で、godep update buzすると、Godeps/_workspace/src以下のbuzパッケージのソースもv3になったので、意気揚々とgodep go run main.goすると、、

This is buz.Buz version 2!!

!?

丁寧に書くのも飽きたので結論

  1. godep savegodep updateは、godepが依存パッケージのソースコードGodeps/_workspace/srcに保存する
  2. godep go install すると、godepが依存パッケージのビルドをGodeps/_workspace/pkgに保存する
  3. 以降、godep go rungodep go installなどのビルドを伴う処理においてGodeps/_workspace/pkg以下にビルド済みのものがあればそちらを使って依存を解決する
  4. godep updateではGodeps/_workspace/pkg以下のビルドは更新されないので、src以下が新しくなってるのにパッケージが古いままのような挙動を示す


godepって、なんか挙動にモットーのようなものを感じる*3ので、なんらかの意図があってこうなってるんだと思いますが、まだそういった記述にたどり着けてないです。

現場からは以上です

DRY

追記 2015/02/12

なんかknown issueだった模様。

*1:この際、"directory "xxx" is not using a known version control system"などと怒られるのは、このアプリケーションがgitなどによって管理されていない(.gitが無い)場合です

*2:この際、"godep: cannot find package "foo" in any of"などと怒られるのは、このマシンの$GOPATHにfooパッケージが無いからです

*3:たとえば、godep restoreしたときは必ずリモートレポジトリをgo getしに行くのであって、ローカルのGodeps/_workspace/から$GOPATHへrestoreするようなことはない、とか