51
46

More than 5 years have passed since last update.

Goのプログラムをパッケージに分ける方法

Last updated at Posted at 2015-02-14

注意(2017/03/29): これはもうだいぶ古い情報です。githubなどのパッケージをバージョン固定で保持する方法としてはvendoringが1.5から入り、1.8時代には純正ツールとしてdepがでてきました。depはそのうちバンドルされるでしょう。dep以外のツールも過去ありましたが、過去の話なので、これからの未来を生きる若者はdepだけを見ていきましょう。

Goはパッケージの仕組みを持っています。最初は何も考えずに package main と先頭に書いてコードを書いていけばいいのですが(ライブラリ以外)、中規模のアプリを作るようになってきて名前空間を分けたくなったら、この仕組みの力を借りることになります。

試行錯誤の仮定を野良パターンっぽく書いてみます。実際にこの順番でコードを修正していくことになると思います。それぞれの項目にはパターンランゲージの「フォース」っぽく前提を書いておきます。実際にはPLoPに提出されてないパターンはすべて野良パターンです。

1. とりあえず、独立性の高い子供をディレクトリに分けよう

前提: 他でも使えそうな汎用性の高いコードがある

最初のステップとして、まず、独立性の高い部品を、ディレクトリに切り出して、パッケージ名を与えます。 common という名前は色々衝突しそうでアレゲですが、サンプルということで。実際には「これは何をするか?(What)」が分かる名前にします。

app (mainパッケージ)
 +- common (commonパッケージ)

commonに移動したファイルの先頭を package common に書き換えます。appのimportに "./common" を足します。移動したファイルに含まれている構造体やら関数を使う場合は、先頭に common. を付けてやります。IntelliJ IDEAのgoプラグインを使っていると、ローカルのimportは常に失敗扱いで赤線引かれてしまいますが、手動インストールで入れる1.0アルファ版なら修正されてます。

2. 独立性の高いモジュールを実際に、複数アプリからシェアしたい

前提: コードコピーではなくて、ムダのない形でコードシェアがしたい

1の方法を使えばパッケージを分割できました。 common パッケージを実際にgithubなどにアップして、 import "github.com/user/common みたいにしてしまえば、共有は難しくありません。

欠点はあります。この場合は、特定のリビジョンにバージョン固定ができないので、たまに過去のバージョンのバイナリが欲しいとなったときに結構困ります(昨日困った)。あと、ライブラリなどで、その共通コード部分にバグがあってパッチを作る場合、そのバグが修正されたことを示すには、

  1. 共通コード部のバグを修正する
  2. ライブラリのフロントパッケージ内のimport文を修正して、修正版のパッケージを参照するようにする

という二段構えの変更が必要になります。手法としては、下記のブログに日本語で翻訳されている方法があります。

  • $GOPATH/src/github.com 以下でブランチを切って行う
  • $GOPATH/src/github.com の他の人場所に、自分のリポジトリを明示的にcloneする

3. 機能ごとに複数のパッケージに分けたい

前提: 各機能同士の依存はないが、それぞれの機能の共通部を持たせたい

実際には2の項目の類似項目ですね(解決策もほぼ一緒)。アプリが複数の部品から構成されているとします。とりあえず1の方法を使ってパッケージに分けますね。

app (mainパッケージ)
 +- feature1 (feature1パッケージ)
 +- feature2 (feature2パッケージ)

これでおしまい。めでたしめでたし、と行かないこともあります。切り出した機能モジュールが共通コードを持っている場合です。共通コードのコピーを各パッケージに持たせるのもありえるかもしれませんが(共通コードがすごく小さく、依存関係によってもたらされるゴタゴタの方が大きいケース)、共通部がそれなりの大きさを持つ場合には、独立したパッケージにします。で、github.comのパッケージなど(Launchpadでも可)にして、 "github.com/user/common" みたいな形式にするのであれば難しいことはありません。

4. うちのgithub、github:eだったわ・・・・

前提: 公開できない業務用のコードを書いている

githubなら、 import "github.com/user/common" という書き方ができるんですが、社内のgithub:eの場合は、golang側にパッチを当てないと、そっちのモジュールをgo getできないという欠点があります。毎回パッチ版を用意するのも良いのですが、ビルドツールが入っていて、簡単にインストールして即開発できる、みたいなgoの価値とはちょっとずれるしめんどいですね。しかも、社内限定の修正とか、引き継ぎが面倒になるだけです。

実際に簡単に解決するならgitサブモジュールが現実解かと思います(git使っているなら)。サブモジュールにして、 import "./common" みたいに書けばOKです。1の解法とほぼ同じ結果になります。では3の各機能の共通部を持たせたい場合にはどうすればいいでしょうか?

今のところ、こんな解法があります。

app (mainパッケージ)
 +- common (commonパッケージ)
 +- feature1 (feature1パッケージ)
 +- feature2 (feature2パッケージ)

で、feature1パッケージからcommonパッケージを参照するには、 import "../common" と書けば参照できます。

もちろん、これにも欠点がないわけではありません。まず、feature1を使うときには、同じ階層(もしくは特定の場所)にcommonパッケージがあること、という暗黙の前提ができてしまいます。長大規模開発になってきて、同じ名前の別のパッケージを利用しなければ、みたいになると、大量のコードの修正が必要になってしまうでしょう。node.jsみたいに、現在のパッケージのnode_modules的なフォルダを作って階層的にパッケージを管理する、みたいな方式もありえるかもしれませんが(import "../go_modules/common"的な)、子と、孫が同じパッケージを利用していた場合はどうするの?(nodeは獲得みたいな上を順番に探す機能があって解決している(ただしgitを使ってない場合に限る))とかはありますよね。重箱の隅かもしれないけど、実際に起きると結構解決がつらそうです。

5. GopherJSでもコンパイルしたいが・・・

前提: 作ったコードをブラウザ上でも動かしたいが、GopherJSにはバグがある

上記までで、github:eで使うには、URLを使ったパッケージ指定よりも、相対パスで使う方が良いということになりましたが、残念ながらGopherJSを利用する場合には、GopherJSのバグのためにその方法を取ることができません。

GopherJSはGoで書かれたGo互換処理系で、JavaScriptのコードを生成します。これを使えば、クライアントとサーバの両方をGoで統一できますが、100%互換ではなく、ライブラリの実装に一部非互換のところがあります。で、ライブラリ以外にも、相対パスに対応していません。開発チーム側もこの問題は認識しているものの、他に優先度の高い項目があるため、現在はwon't fixという扱いになっています。ウェブのクライアントの実装に使いたい場合は、URL指定にする必要があります。

で、github:eでGopherJSを使いたい場合はどうすればいいのかというと、今のところ解決策はないです。

6. APIのバージョンを持たせたい

前提: 破壊的なバージョンアップをする必要があった

実際にここまで大きくなったアプリを僕は作ったことがありませんが、Golang業界の流行りは、各モジュールの中に、 v1, v2 という名前をバージョンごとに用意して、それを親アプリ側が明示してimportする方法のようです。 Google API ClientのDrive APIなんかがこの例にあたります。

これの実現方法としては、2つあります。Google API Clientなどは、v1, v2というフォルダを作っています。v1とv2のコードの重複なんかは気にしないで独立して動くパッケージにしておけばいいようです。欠点としては、v1からv2の移行が完全に別コピーなので、バージョン管理システムでうまく差分が取れない(手動でdiffしないと)ということですかね。これは、Google API Clientがコードジェネレータで生成しているっぽい感じなので差分を見ることがない、という割り切りかもしれません。

もう1つの実現方法は、 gopkg.in です。例えば、 yamlライブラリがバージョンを2つすでに用意していますが、gopkg.inがgitの特定のブランチやタグにリダイレクトしてくれます。タグの場合は、v2と指定したらv2.x.yの中の最新を取ってくるとか、メジャー、マイナー、パッチのバージョンを意識した管理ができるようです。

これの欠点については、次の6の項目として独立した項目にしました。

7. バージョン対応したら複数のバージョンが混ざって管理が大変に

前提: npmの悪夢再び

5で破壊的変更に対応できるような管理ができるようになりました。これで超大規模も安心です、とは行かないのが悩みどころ。欠点としては、超大規模になって、Aモジュールがv1を使って、Bモジュールがv2を使っていて、Aモジュールで生成したv1形式の構造体がBモジュールにわたってきてコンパイルが失敗する・・・みたいなことが発生してしまうと、構造体の変換を手動で・・・みたいなこともあるかもしれません。

Google社内には「社内ではライブラリのバージョンを上げるときに、それに依存してるライブラリは、依存先のバージョンに必ず追従しなければいけない」という鬼ルールがあるらしいです。単一組織の場合は破壊的変更でも、APIバージョンを分けずに覚悟を持って追従する、というのが1つの解決策になります。CI環境の整備が必要になるかもしれません。

ただし、他の人の作ったモジュールを使う場合、その人がコード修正をこまめにオンタイムでやってくれるとは限りません。もちろん、コードをフォークするというのも手ですが、この手のパッケージの依存関係の微修正は後から見てもわかりにくい(コンテキストも日々変化している)ので、きちんとドキュメント化が必要でしょう。

8. そのパッケージ分け、本当に必要だったか?

前提: あんまり分けない方がよかった、って思うことが多い

パッケージに分けた結果、あまりコードのシンプル化に貢献しないのであれば、フラットな名前空間に戻してしまうのも手です(import . "パッケージ名 というのは却下)。

PythonやRubyなんかは、同じライブラリ内でも積極的にディレクトリを分けたり、名前空間を分けようとしますが、Golangの文化としては、なるべくフラットで粘るというのがある気がします。微妙なコード修正であっても、静的チェックで確認できるので、衝突してわかりにくいバグを生まないように名前空間を細かく分けよう!みたいなのは必要ないのかもしれません。

51
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
46