2016年5月27日
6年間におけるGoのベストプラクティス
本記事は、原著者の許諾のもとに翻訳・掲載しております。
(本稿は、QCon London 2016で行った講演の内容に基づいています。スライドとビデオは近日中に掲載予定です)
2014年に開催された最初のGopherConで、私は「 Best Practices in Production Environments(本番環境でのベストプラクティス) 」と題した講演を行いました。 SoundCloud の私たちはGoのアーリーアダプターで、その時点までに既に2年近く、本番環境向けの様々なGoコードを書き、実行し、メンテナンスしていました。そして私たちはいくつかのことを学んだので、その教訓をまとめ、多くの人に伝えたいと思ったのです。
それ以来、私はフルタイムでGoを使う仕事を続けています。SoundCloudではその後の活動やインフラチームで、そして現在は Weaveworks で Weave Scope や Weave Mesh の開発に使っています。また、オープンソースのマイクロサービス向けツールキット Go kit の開発にも力を入れています。その間ずっと、Goコミュニティでも活動を続け、欧州や米国の各所であった集まりや会議で多くの開発者と会い、成功した話も失敗した話も聞いてきました。
2015年11月にGoが 誕生6周年 を迎えたことで、私は最初の講演を思い起こしてみました。当時のベストプラクティスのうち、時を経ても変わらず有効な事例、時代遅れや非生産的になった事例は? 新たなベストプラクティスは生まれたのか? 3月には QCon London で講演を行い、2014年のベストプラクティスを振り返るとともに、2016年のGoはどのような進化を遂げてきたかに注目しました。本稿はその講演の内容をまとめたものです。
キーポイントは、「一言アドバイス」としてリンク付きでハイライト表示しています。
✪ 一言アドバイス: この「一言アドバイス」を活用して、Go生活をレベルアップしましょう。
以下が本稿の目次です。
開発環境 パーマリンク オリジナル
Goには、GOPATHを中心とした開発環境の規約があります。2014年、私は単一のグローバルなGOPATHを強く提唱していましたが、その姿勢は今では少し弱まりました。他の事情が同じならば単一のグローバルなGOPATHが一番いい、という考えに変わりはありませんが、プロジェクトやチームによっては他の方法も有効かもしれないと思うようになりました。
自身や組織が構築しているものが主にバイナリの場合は、プロジェクトごとのGOPATHが便利かもしれません。Dave Cheneyとコントリビュータが開発した gb は、標準のgoツーリングをこのユースケース向けに置き換える新ツールです。多くの開発者から、gbを使った成功事例が多数報告されています。
中には、 $HOME/go/external:$HOME/go/internal
のような2エントリのGOPATHを使うGo開発者もいますが、goツールはかねてからこの処理方法を認識しています。go getが最初のパスをフェッチするので、サードパーティコードと内部のコードを厳密に分離する必要がある場合は便利かもしれません。
1つ気づいたのですが、開発者に忘れられがちな点があります。GOPATH/binをPATHに追加するということです。これをやっておけば、go getで得たバイナリを簡単に実行でき、コードをビルドするという(望ましい)go installのメカニズムが扱いやすくなります。設定しない手はありません。
✪ 一言アドバイス: $GOPATH/binを$PATHに追加しましょう。インストールしたバイナリが扱いやすくなります。
エディタと統合開発環境(IDE)については、多くの改良がなされてきました。vim使いにとっては、今や最高の環境が整っています。 Fatih Arslan が大変優れた取り組みを続けてくれているおかげで、 vim-go プラグインは本当に素晴らしい状態となり、その分野で最良のツールとなっています。私はEmacsにはそれほど詳しくありませんが、 Dominik Honnef の go-mode.el はとても重要なツールだと思います。
上位層へと移行しながらも、多くの開発者は今でも Sublime Text + GoSublime の組み合わせでうまくやっています。これはあまり速い方法ではなく、近頃はElectronを使ったエディタに注目が集まっているようです。 Atom + go-plus は愛用者も多く、特に言語を度々JavaScriptに切り替えなければならない開発者は好んで使っています。意外だったのは Visual Studio Code + vscode-go で、この方法はSublime Textより遅いものの、Atomよりは明らかに速く、私にとって重要な、クリックで定義に飛ぶ機能などがデフォルトでサポートされていて素晴らしいと思います。私はこの組み合わせを Thomas Adam に紹介されて以来、もう半年ほど毎日使っています。とても面白いですよ。
完全なIDEに関しては、専用に作られた LiteIDE が定期的にアップデートされており、一定の愛用者が存在します。また、 IntelliJ Goプラグイン も着実に改良されています。
リポジトリ構造 パーマリンク オリジナル
長い時間をかけてプロジェクトが成熟してくると、いくつかの明らかなパターンが見えてきます。リポジトリを構造化する方法は、プロジェクトが どのようなものか によって決まります。まず、プロジェクトが私的または社内向けの場合は、好きなようにしていいでしょう。独自のGOPATHに対応させたり、カスタムビルドツールを使ったりするなど、便利になり生産性が上がるようになることは何でも取り入れるのです。
一方、プロジェクトが公的な場合(例:オープンソース)、ルールは少々厳しくなります。多くのGo開発者は、あなたが作ったパッケージの取り込みにgo getを使うはずですので、go getにきちんと対応しておくべきでしょう。
何が理想的な構造であるかは、構築しているもののタイプによって決まります。リポジトリがバイナリ専用またはライブラリ専用の場合は、ユーザがgo getやimportをベースのパスで実行できるようにしておきましょう。すなわち、github.com/name/repoにはmainパッケージやインポート可能な主要リソースを置き、サブディレクトリはヘルパーパッケージ用とするのです。
リポジトリにバイナリとライブラリが混在する場合は、どちらを メイン にするか決めて、それをリポジトリのルートに置くべきです。例えば、バイナリがメインで、ライブラリにも使うなら、構造は以下のようになるでしょう。
github.com/peterbourgon/foo/
main.go // package main
main_test.go // package main
lib/
foo.go // package foo
foo_test.go // package foo
1つ効果的なのは、サブディレクトリlib/に置くパッケージの名前を、ディレクトリではなくライブラリにちなんで付ける方法です。上記のケースでは、libパッケージではなくfooパッケージとしています。この命名法は、かなり厳密なGoのイディオムにおける例外となりますが、実際はユーザに対して非常に親切なやり方です。このように構造化されたリポジトリの好例としては、HTTP負荷テストツール tsenart/vegeta があります。
✪ 一言アドバイス: リポジトリfooが主にバイナリ用なら、ライブラリコードはサブディレクトリlib/に置き、fooパッケージと名付けましょう。
リポジトリが主にライブラリ用で、バイナリも多少含むという場合は、構造は以下のようになるでしょう。
github.com/peterbourgon/foo
foo.go // package foo
foo_test.go // package foo
cmd/
foo/
main.go // package main
main_test.go // package main
前述の構造を逆にして、ライブラリコードをルートに置き、サブディレクトリcmd/foo/をバイナリコード格納用にするのです。中間にcmd/を介すると便利な理由は2つあります。1つは、Goのツーリングでは自動的にmainパッケージのディレクトリにちなんでバイナリの名前が付けられるので、リポジトリにある他のパッケージとかち合う可能性のない最適な名前が得られることです。もう1つは、間に/cmd/の入ったパスでユーザがgo getを実行すれば、取り込もうとしているものについてのあいまいさがなくなることです。 gb はそのように設計されているビルドツールです。
✪ 一言アドバイス: リポジトリが主にライブラリ用なら、バイナリはcmd/配下の個別のサブディレクトリに置きましょう。
リポジトリ構造で目指すべきは、ユーザのための最適化です。あなたのプロジェクトを最も一般的な形で、利用しやすいものにしましょう。このユーザ中心の考え方は、Goの理念の一部だと思います。
フォーマッティングとスタイル パーマリンク オリジナル
フォーマッティングとスタイルに関しては、大きな変化はありません。これはGoでは以前から適切に扱われていた領域で、コミュニティ内の合意があり、言語が安定していることを大変ありがたく感じています。
Code Review Comments は大変有用で、コードレビューの際に準拠する最低基準となるものです。名前に関してもめたり一貫性がなかったりする場合は、Andrew Gerrandの示した 慣用的な命名規則 が優れたガイドラインとなるでしょう。
✪ 一言アドバイス: Andrew Gerrandの 命名規則 に準じましょう。
ツーリングに関しては、良くなる一方です。エディタは保存時に、gofmtか、さらに良いのは goimports を呼び出すように設定するといいでしょう(この点には異論はないと思います)。go vetツールでは( ほぼ )誤った結果は出ないので、pre-commitフックに組み込むことを考えてもよいかもしれません。そして気になる点のチェックを行うには、 gometalinter が優れていると思います。それでも誤った結果が出る 可能性はある ので、何らかの形で 独自のルールをコード化しておく のも悪くありません。
設定 パーマリンク オリジナル
設定は、実行時環境とプロセスの間をカバーする領域であり、明示的な形で、きちんと文書化されていることが望ましいでしょう。私が今でも使っていて推奨しているのはflagパッケージですが、もう少し分かりやすくなってほしいと現在感じていることは否めません。getopts式のロング形式とショート形式の引数構文が標準でサポートされておらず、ヘルプの文言が簡潔でないのは残念なことです。
Twelve-Factor App は、環境変数を設定のために使うことを推奨していますが、 各変数がフラグとしても定義されているならば 良い方法だと思います。明示性は重要です。アプリケーションの実行時の振る舞いを変える際には、その変化が認識でき、文書化されていることが望ましいのです。
これは2014年に述べたことですが、大事な点なのでもう一度繰り返します。 フラグはmain関数で定義、パースしましょう 。main関数だけが、ユーザの利用できるフラグを決められるべきです。ライブラリコードで振る舞いをパラメータ化する必要があるなら、そのパラメータは型コンストラクタの一部とすべきでしょう。設定をパッケージグローバルな場所に移せば便利になるというのは思い違いであり、見せかけの省力化にすぎません。コードのモジュール性が損なわれ、開発者や未来のメンテナンス担当者が依存関係を理解するのが難しくなりますし、並列実行が可能な独立したテストを書くのはもっと大変になってしまいます。
✪ 一言アドバイス: main関数だけが、ユーザの利用できるフラグを決められるようにしましょう。
こうした様々な特性を兼ね備え、スコープが適切に設定されたflagsパッケージがコミュニティから生まれる見込みは、大いにあると思います。ひょっとしたら、既に存在しているのかもしれません。もしあるならぜひ使ってみたいので、情報をご存じの方は どうぞお知らせください 。
プログラム設計 パーマリンク
講演では、プログラム設計のいくつかの問題を議論するための出発点として、設定の話をしました(2014年の講演では、プログラム設計は取り上げませんでした)。まずはコンストラクタを見てみましょう。全ての依存関係を適切にパラメータ化している場合、コンストラクタはかなり大きくなる傾向があります。
foo, err := newFoo(
*fooKey,
bar,
100 * time.Millisecond,
nil,
)
if err != nil {
log.Fatal(err)
}
defer foo.close()
このようなコンストラクションは、設定オブジェクトで最適に表現されることもあります。すなわちコンストラクタに対する構造体パラメータであり、コンストラクタは構築されるオブジェクトに対する オプションの パラメータを取ります。例えば、fooKeyが必要なパラメータで、その他全ては賢明なデフォルトを持つかオプションであるとしましょう。よく見かけるのは、設定オブジェクトを少しずつ構築するプロジェクトです。
// Don't do this.
cfg := fooConfig{}
cfg.Bar = bar
cfg.Period = 100 * time.Millisecond
cfg.Output = nil
foo, err := newFoo(*fooKey, cfg)
if err != nil {
log.Fatal(err)
}
defer foo.close()
ですが、いわゆる構造体の初期化構文を活用して、オブジェクトを一斉に単一の文で構築した方がずっとよくなります。
// This is better.
cfg := fooConfig{
Bar: bar,
Period: 100 * time.Millisecond,
Output: nil,
}
foo, err := newFoo(*fooKey, cfg)
if err != nil {
log.Fatal(err)
}
defer foo.close()
オブジェクトの無効な中間状態に依存する文がありません。そして全てのフィールドはきちんと区切られてインデントされ、fooConfigの定義を反映しています。
設定オブジェクトは、構築したらすぐに使うということに注意してください。この例では、構造体の宣言をnewFooコンストラクタの中に直接組み込むことで、中間状態の段階を1つ省き、コードの行数も1行減らせます。
// This is even better.
foo, err := newFoo(*fooKey, fooConfig{
Bar: bar,
Period: 100 * time.Millisecond,
Output: nil,
})
if err != nil {
log.Fatal(err)
}
defer foo.close()
よくなりましたね。
✪ 一言アドバイス: 無効な中間状態を回避するため、構造体のリテラルの初期化を使いましょう。構造体の宣言は、可能な場所に組み込みましょう。
では、賢明なデフォルトについて考えてみます。Outputパラメータは、nil値を取り得るものです。議論を進めるために、io.Writerだとしましょう。特別なことをしない限り、これをfooオブジェクトで使うためには、先にnilチェックを実行する必要があります。
func (f *foo) process() {
if f.Output != nil {
fmt.Fprintf(f.Output, "start\n")
}
// ...
}
これではイマイチです。存在チェックをせずに出力を扱える方が、ずっと安全で便利です。
func (f *foo) process() {
fmt.Fprintf(f.Output, "start\n")
// ...
}
ここでは有用なデフォルトを提供すべきでしょう。インターフェース型では、インターフェースのno-op(何もしない)実装を提供するものを渡すのも良い方法です。実は、標準ライブラリであるioutilパッケージには、ioutil.Discardと呼ばれるno-opのio.Writerが備わっています。
✪ 一言アドバイス: デフォルトのno-op実装を介してnilチェックを回避しましょう。
それをfooConfigオブジェクトに渡すことも考えられますが、それでもまだ脆弱です。呼び出し先での処理が抜けていれば、やはりnilパラメータで終わるでしょう。そこで代わりに、コンストラクタ内で一種の安全対策を講じることができます。
func newFoo(..., cfg fooConfig) *foo {
if cfg.Output == nil {
cfg.Output = ioutil.Discard
}
// ...
}
これは、 ゼロ値を役立たせる というGoのイディオムのちょっとした応用例です。パラメータのゼロ値(nil)によって、デフォルトの振る舞い(no-op)を適切なものにすることができるのです。
✪ 一言アドバイス: 特に設定オブジェクトでは、ゼロ値を役立たせましょう。
再びコンストラクタを見てみます。パラメータのfooKey、bar、period、outputは、全て 依存関係 を持っています。fooオブジェクトは、開始と実行に関してこの各パラメータに 依存 しているのです。私がこの6年間毎日のように、Goのコードを自由に書き大規模なGoプロジェクトを観察してきた中で、1つだけ学んだ教訓があるとすれば、それは 依存関係を明示的にする ということです。
✪ 一言アドバイス: 依存関係は明示的にしましょう!
膨大な量に及ぶメンテナンスの負荷、混乱、バグ、そして技術的負債が、不明瞭または非明示的な依存関係に由来する可能性があると、私は考えています。型fooに関する以下のメソッドを考えてみましょう。
func (f *foo) process() {
fmt.Fprintf(f.Output, "start\n")
result := f.Bar.compute()
log.Printf("bar: %v", result) // Whoops!
// ...
}
fmt.Printfは自己完結しており、グローバルな状態に影響も依存もしていません。関数型言語的に言えば、 参照透過性 がある状態です。つまり依存関係は存在しません。f.Barは明らかに依存関係を持っています。そして面白いことにlog.Printfは、自身に対する操作を持たないPrintf関数があるため分かりにくくなっていますが、パッケージグローバルなロガーオブジェクトに対して作用します。よって、これにも依存関係があります。
依存関係はどうすればよいでしょうか? 明示的にするのです。 processメソッドは処理の一環としてログに出力するので、メソッドまたはfooオブジェクト自体が、ロガーオブジェクトと依存関係を持つ必要があります。例えば、log.Printfはf.Logger.Printfにすべきでしょう。
func (f *foo) process() {
fmt.Fprintf(f.Output, "start\n")
result := f.Bar.compute()
f.Logger.Printf("bar: %v", result) // Better.
// ...
}
ログへの書き出しといった特定部類の処理は、付随的なものと見なしがちです。そのため、見かけ上の負荷を減らそうと、私たちはパッケージグローバルなロガーなどのヘルパーを喜んで活用します。しかし、インストゥルメンテーションのようなロギングはサービスの運用に不可欠な場合が多く、依存関係をグローバルスコープ内に隠してしまうと後で大変なことになりそうですが、実際そのとおりです。これはロガーのような一見当たり障りのないものであれ、ひょっとすると他のもっと重要な、わざわざパラメータ化していなかったドメイン特有のコンポーネントであれ、当てはまることです。厳密にすることで、未来の苦労は減らせます。依存関係は 必ず 明示的にしましょう。
✪ 一言アドバイス: ロガーには、他のコンポーネント、データベースハンドル、コマンドラインフラグなどへの参照のような、依存関係があります。
もちろん、この例のロガーも、賢明なデフォルトを取るようにするべきです。
func newFoo(..., cfg fooConfig) *foo {
// ...
if cfg.Logger == nil {
cfg.Logger = log.New(ioutil.Discard, ...)
}
// ...
}
ロギングとインストゥルメンテーション パーマリンク オリジナル
目下の問題について述べます。ロギングを使った制作を多く経験しているのですが、それによってこの問題への関心が高まりました。ロギングは想像するよりもずっと高価で、システムのボトルネックになりやすいのです。この件については 別のブログ投稿 で範囲を広げて書きましたが、まとめると次のようなことです。
- 実行可能な情報 のみをログする。将来、人間かマシンが読むことになる。
- ログレベルの粒度を上げすぎない。情報とデバッグのみでまず事足りる。
- 構造化ログを使う。偏見ながら、 go-kit/log が良い。
- ロガーは依存関係
ロギングは高価なのに対し、インストゥルメンテーションは安価です。コードベースのあらゆる重要なコンポーネントをインストゥルメンティング、計測すべきです。それがキューのようなリソースの場合は、 Brendan GreggのUSEメソッド に従い、利用率、集中度、そしてエラーカウント(率)を計測しましょう。エンドポイントのようなものであれば、 Tom WilkieのREDメソッド に従い、要求カウント(率)、エラーカウント(率)、そして持続時間を計測します。
他に選択肢があるとすれば、おそらく Prometheus が適切なインストゥルメンテーションシステムです。そして当然、メトリクスも依存関係です。
ロガーとメトリクスを使い、より直接的にグローバル状態をピボット、アドレス指定していきましょう。以下にGoに関する事実をいくつか挙げます。
- log.Printは固定、グローバルのlog.Loggerを使う。
- http.Getは固定、グローバルのhttp.Clientを使う。
- http.Serverは既定では固定、グローバルのlog.Loggerを使う。
- database/sqlは固定、グローバルのドライバレジストリを使う。
- func initはパッケージグローバル状態に副作用を及ぼすためだけに存在する。
これらの事実は小規模では便利ですが、規模が大きくなると扱いにくくなります。すなわち、固定グローバルロガーを使うコンポーネントのログ出力は、どうやってテストすればよいのでしょうか。その出力をリダイレクトしなければなりませんが、どうすれば並行テストができるのでしょうか。無理?それでは不十分に感じます。あるいは、異なる条件でHTTP要求する2つの独立したコンポーネントがある場合は、どうやって管理するのでしょうか。既定のグローバルhttp.Clientでは、それは非常に困難です。次の例を考えてみましょう。
func foo() {
resp, err := http.Get("http://zombo.com")
// ...
}
http.Getはパッケージhttpにおいてグローバルを要求します。暗示的なグローバルの依存を持っているのです。これを取り除くのは簡単です。
func foo(client *http.Client) {
resp, err := client.Get("http://zombo.com")
// ...
}
ただhttp.Clientをパラメータとして渡しましょう。しかしそれは具象的な型であり、この関数をテストしたければ、実際のHTTPコミュニケーションを強制する具象的http.Clientも用意しなければなりません。面倒です。HTTPリクエストを実行できるインターフェースを渡すことで、もう少しうまく解決できます。
type Doer interface {
Do(*http.Request) (*http.Response, error)
}
func foo(d Doer) {
req, _ := http.NewRequest("GET", "http://zombo.com", nil)
resp, err := d.Do(req)
// ...
}
http.Clientは自動的にDoerインターフェースの要求を満たしますが、これでテストでDoer実装のモックアップを自由に渡せるようになりました。素晴らしいではありませんか。func fooのユニットテストはfooのふるまいのみをテストするものであり、http.Clientが、公言されている通りに動くことを安全に確かめられるのです。
テストの話をしましょう。
テスト パーマリンク オリジナル
2014年、私は様々なテスト用フレームワークとヘルパーライブラリを使った経験を振り返り、1つとして使えるものはないという結論に達して、stdlibのアプローチであるテーブルベースの平易なパッケージテストを薦めることにしました。言ってしまえば、今もそれがベストの助言だと考えています。Goのテストで留意すべき重要な点は、 これは単にプログラミングだ 、ということです。他のプログラミングと何ら異なる点はなく、独自のメタ言語を是認します。そのため、やはりパッケージテストがそのタスクには都合が良いのです。
TDD/BDDパッケージは新しい、まだなじみのないDSL、制御構造で、あなたと将来のメンテナンス担当者の認知的な負担を増やします。個人的には、そのコストに見合うコードベースは見たことがありません。グローバル状態と同様、これらのパッケージは不経済の極みであり、たいていは他の言語やエコシステムのふるまいの猿真似をしたに過ぎないプロダクトだと思います。 Goに入ってはGophersに従え 。シンプルで、意味の分かりやすいテストを書くための言語は既にあります。Goです。そしてあなたはそれをよく知っているはずです。
と言いながら、私は自分独自のコンテキストと偏見があるのを認識しています。GOPATHに対する意見と同様に、譲れるところは譲り、テスト用DSLやフレームワークを便利に使っているチームや組織の意見にも従っています。パッケージを使いたいなら、使えばいいのです。但し、明確な根拠を持って進めねばなりません。
もう1つの驚くほど興味深いトピックは、テストのための設計です。この件について、近頃Mitchell Hashimotoが、ここBerlinでぜひ耳を傾けるべき講演を行いました( SpeakerDeck 、 YouTube )。
概して最善と思われるのは、Goを一般に関数型のスタイルで書くことです。可能な限り、依存関係を明示的に列挙し、小さく、綿密にスコープを絞ったインターフェースとして用意します。ソフトウェアエンジニアリングの上で良い規律になるだけでなく、そうすることで、テストのしやすいコードへと自動的に最適化されていくように感じられるでしょう。
✪ 一言アドバイス: 多数の小さなインターフェースを使い、依存関係を形成しましょう。
上述のhttp.Clientの例で見たように、ユニットテストはテストの必要な部分をテストするために記述すること、それ以外にないことを覚えておきましょう。処理関数をテストするのであれば、要求の入るHTTPトランスポートや、結果が書き込まれるディスク上のパスまでテストすることはありません。インターフェースパラメータの仮の実装として入力と出力を用意し、メソッドやコンポーネントのビジネスロジックだけに注力しましょう。
✪ 一言アドバイス: テストの必要な部分のみテストしましょう。
依存関係管理 パーマリンク オリジナル
最もホットなトピックです。2014年、まだいろいろなことが初期段階にあった頃、私ができる唯一のアドバイスはベンダリングでした。それは今日にも変わっていません。ベンダリングは今なお、バイナリの依存関係の管理には有効な方法です。特にGO15VENDOREXPERIMENTとそれに付随するvendor/ subdirectoryはGo 1.6では初期設定になっています。そのレイアウトを使っていきましょう。そして、有り難いことに、ツールはどんどん良くなっています。いくつか薦めたいものを挙げてみます。
- FiloSottile/gvt は最小限のアプローチで、gbツールからベンダサブコマンドを展開し、単独で使えるようにします。
- Masterminds/glide は最大限のアプローチで、内部でベンダリングを使い、フル機能の依存関係管理ツールの使用感を再現しようとします。
- kardianos/govendor は上述の中間に位置するもので、おそらくベンダリング固有の名詞、動詞に最も充実したインターフェースを提供し、マニフェストファイルの対話を推進します。
- constabulary/gb はgoツーリングを全て捨て去り、全く別のリポジトリレイアウトとビルドメカニズムを用いています。バイナリを作成していて、ビルド環境を命令できるのであれば最適な選択肢です。例:コーポレート設定の場合。
✪ 一言アドバイス: バイナリの依存関係をベンダリングする際はトップのツールを使いましょう。
ライブラリに関する重要な注意事項です。Goにおいて、依存関係管理はバイナリオーサーの懸念点です。ベンダリングされた依存関係を持つライブラリの使用は非常に難しく、いっそ最初から使えないと言ったほうが親切かもしれません。1.5でベンダリングが公式に導入されて以来、数か月間に渡り、多くのコーナーケース、エッジケースが展開されました(特に詳しく知りたい場合は、 そのうちの1つ に関する フォーラム を見てください)。
あまり深入りせずとも、すぐに学びました。ライブラリは依存関係のベンダリングをするものではありません。
✪ 一言アドバイス: ライブラリは依存関係のベンダリングをするものではありません。
もし、あなたのライブラリが、エクスポートされた(公開)APIレイヤに逃げないよう依存関係を密封している場合は、例外になり得るかもしれません。エクスポートされた関数、メソッドのシグネチャ、構造、その他全てにおいて一切依存型が参照されていない場合です。
バイナリとライブラリの両方を持ったオープンソースのリポジトリをメンテナンスする共通のタスクがある時は、残念ながら進退ここに窮まります。バイナリ用に依存性をベンダリングしたくても、ライブラリのためにはベンダリングする必要がなく、GO15VENDOREXPERIMENTはこのレベルの粒度を認めません。悔しい失敗です。
はっきり言って、この問題に対する答えは分かりません。etcdの仲間たちは シンボリックリンクを使った解決 法 を編み出しましたが、私は自信を持って薦められません。シンボリックリンクはgoのツールチェーンで十分にサポートされていない上、Windows上では作ることもできません。これを機能させるには幸運に頼るしかないと言えます。私たちはこれらの懸念全てを コアチームに提起しました ので、遠くない将来に何かが起こることを期待しています。
ビルドとデプロイ パーマリンク オリジナル
ビルドに関して言えば、Dave Cheneyから1つ大事なことを学びました。go buildよりもgo install、です。インストールの動詞は$GOPATH/pkgの依存関係からビルドアーティファクトをキャッシュし、ビルドの速度を上げます。さらにバイナリを$GOPATH/binに置くことで、検索や起動をより簡単にします。
✪ 一言アドバイス: go buildよりもgo installを使いましょう。
バイナリを作成する時は、思い切って gb などの新しいビルドツールを使ってみてください。認知的な負担が大きく減るかもしれません。反対に、Go 1.5以降はクロスコンパイラが内蔵されていることも覚えておきましょう。適切なGOOSとGOARCH環境変数、そして適切なgoコマンドを設定しましょう。これで余分なツールは必要ありません。
デプロイは、私たちGopherにとっては、Ruby、PythonあるいはJVMさえ比較にならないほど簡単です。メモ:コンテナ内にデプロイするなら、 Kelsey Hightowerのアドバイス に従い、スクラッチでやりましょう。Goがこの信じがたい機会を与えてくれているのですから、使わない手はありません。
より一般的なアドバイスは、プラットフォームやオーケストレーションシステムを選ぶ前に、結局選択肢がないとしても、よく考えることです。マイクロサービスの流行に飛びつく際も同様です。自動的にスケールするEC2グループへにAMIとしてデプロイされた流麗なモノリスは、小さなチームにとって非常に生産的な設定です。誇大広告には抵抗すること、少なくとも注意深く考慮するようにしましょう。
まとめ パーマリンク
一言アドバイス:
- $GOPATH/binを$PATHに追加しましょう。インストールしたバイナリが扱いやすくなります。
- リポジトリfooが主にバイナリ用なら、ライブラリコードはサブディレクトリlib/に置き、fooパッケージと名付けましょう。
- リポジトリが主にライブラリ用なら、バイナリはcmd/配下の個別のサブディレクトリに置きましょう。
- Andrew Gerrandの 命名規則 に準じましょう。
- main関数だけが、ユーザの利用できるフラグを決められるようにしましょう。
- 無効な中間状態を回避するため、構造体のリテラルの初期化を使いましょう。構造体の宣言は、可能な場所に組み込みましょう。
- デフォルトのno-op実装を介してnilチェックを回避しましょう。
- 特に設定オブジェクトでは、ゼロ値を役立たせましょう。
- 依存関係は明示的にしましょう!
- ロガーには、他のコンポーネント、データベースハンドル、コマンドラインフラグなどへの参照のような、依存関係があります。
- 多数の小さなインターフェースを使い、依存関係を形成しましょう。
- テストの必要な部分のみテストしましょう。
- バイナリの依存関係をベンダリングする際はトップのツールを使いましょう。
- ライブラリは依存関係のベンダリングをするものではありません。
- go buildよりもgo installを使いましょう。
Goは生まれた時から保守的な言語であり、完成度が高いため、驚きは少なく、大きな変更もないまま今に至ります。結果として、そして予測どおり、コミュニティの中で何がベストプラクティスであるかというスタンスが劇的に切り替わることもありませんでした。代わりに私たちは、初期の頃に比較的よく知られていた比喩と慣用句が具象化されていく過程や、設計パターン、ライブラリとしての「上位層への」段階的な移行、そしてプログラム構造が、探究を通してGo的なコードへと変貌を遂げていくのを見てきました。
Goプログラミングの楽しく実り多い次なる6年に乾杯。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa