QCon London 2016において、Peter Bourgon氏は「Successful Go Program Design, 6 Years On」というプレゼンを行い、Goでプログラミングするときに使うべきパターンと避けるべきパターンについて説明した。
GOPATH: 環境変数PATHにGOPATH/bin
を加え、関係バイナリを簡単にアクセスできるようにする。Bourgon氏は一つのグローバルなGOPATH
を使うことを推奨する。たいていの場合、これでうまくいく。自分のコードと外部依存のコードを明確に分離したい人は、2つのGOPATH
を作るのが好みだろう。gbを使って、環境変数をセットせずにプロジェクトごとに構築するという選択肢もある。
リポジトリ構成: リポジトリの構成はプロジェクトに依存する。プライベートなプロジェクトで決して公開しないなら、好きな構成で構わない。オープンソースの場合には、go get
で簡単に取得できるよう、リモートパッケージのガイダンスに従うべきだ。以下に示すように、Bourgon氏はベースディレクトリにプログラムの主たる成果物を置き、サブディレクトリにヘルパーパッケージを置くことを推奨する。
コードの整形: 使ったコードを読みやすくするため、Bourgon氏はGoの標準スタイルを尊重するのが重要だと言う。彼の意見では、きちんと整形されていないコードは初心者が書いたものだと、コミュニティでは扱われるそうだ。保存するたびにgofmt
を実行してコードを整形してもよい。コードの整形について、彼はGo Code Reviewガイドラインを参照し、レビュアーとオーナーに共通言語とプラクティスを与えるよう述べた。また「もしあなたがやれば全員が感謝するだろう」と、Goにおける変数、関数、エクスポートなどの命名にはAndrew Gerrand氏のおすすめを支持している。
コンフィグレーション: Bourgon氏はコンフィグレーション設定について「明示的にきちんと文書化する」ようアドバイスした。彼は標準ライブラリであるflag
パッケージを使っているが、「もっとわかりやすければよかったのに」と思っている。彼はコンフィグレーションドメインを明示的にすることが重要だと言う。コンフィグレーションを環境変数で渡すと、システムのパラメータを理解するのに十分な情報をユーザーに与えることができない。コンフィグレーション情報はヘルプに入れるべきだと考えている。
パッケージ名: パッケージが含んでいるものではなく、パッケージが提供するものに基づいて命名すべきだ。HelloWorld
メッセージを含んでいるパッケージは、common
やconsts
ではなく、greetings
と呼ぶべきだ。パッケージに含まれているものではなく、パッケージがやることがわかるように命名しなくてはならない。
ドットインポート: パッケージ名の前にドットを付けることで、パッケージ名を書かずに各パッケージに定義された変数にアクセスできるが、Bourgon氏は「ドットインポート」を使わないよう勧めている。これは不慣れな人にとって読みにくく、変数がどのパッケージに属しているか理解しなくてはならないためだ。Goは「常に暗黙よりも明示を好む」。
フラグ: フラグの初期化をinit()
関数でやるのは望ましくなく、main()
関数でやるべきだとBourgon氏は考える。これはグローバルスコープで呼び出して、テストで役に立つ依存するものに触るのを防ぐためだ。
コンストラクタ: 無効あるいは不完全な状態のオブジェクトを渡すのを避けるため、コンストラクタの呼び出しでは、初期化したstruct
をパラメータとすることを勧める。以下に例を示す。
foo := newFoo(*fooKey, fooConfig{
Bar: bar,
Baz: baz,
Period: 100 * time.Millisecond,
})
使えないデフォルト: フィールドの変数をnil
で初期化し、使うたびにnil
テストするのではなく、変数を何もしない値で初期化する方が良い。例えば、output
変数にはioutil.Discard
を使う。
相互参照するコンポーネント: 2つのコンポーネントが相互参照を含む場合がある。例えば、次の2つのstruct
のように、一方をコンストラクトするには他方をコンストラクトする必要があるが、後者をコンストラクトするときには前者が必要になる。
type bar struct {
baz *baz
}
type baz struct {
bar *bar
}
Bourgon氏はこの問題を扱うための3つの方法を紹介した。
1. 一体化。相互に関連する2つのオブジェクトを1つにまとめられる場合がある。この場合、barbaz
structになる。
2. 分離。2つのコンポーネントを分離しておきたいなら、次のような戦略をとることができる。
type bar struct {
a *atom
monad
}
type baz struct {
atom
m *monad
}
a := &atom{...}
m := newMonad(...)
bar := newBar(a, m, ...)
baz := newBaz(a, m, ...)
3. コミュニケーション。前の2つの方法が適切ない場合、互いにメッセージを渡す方法がある。
type bar struct {
toBaz chan<- event
}
type baz struct {
fromBar <-chan event
}
c := make(chan event)
bar := newBar(c, ...)
baz := newBaz(c, ...)
依存関係: Bourgon氏が紹介したすぐれたコツの1つは「依存関係を明示的にする」ことだ。例えば、
func (f *foo) process() {
log.Printf("bar: %v", result) // ...
}
は次のように置き換えるべきだ。
func (f *foo) process() {
f.Logger.Printf("bar: %v", result) // ...
}
log.Printf
は実際にはLogger
を呼び出すが、依存関係が隠されている。これを明示するには、構築フェーズでLogger
を作成し、その値がnil
ならioutil.Discard
に初期化する必要がある。
チャネル: Bourgon氏はgoroutineとgoroutineを統合するchannelとの間でメモリを共有する際には、mutexを使うことを推奨する。
ロギング: ロギングはコストがかかり、アプリケーションのボトルネックになる可能性がある。したがって、彼のアドバイスは、人間が読む、あるいは機械が利用する、絶対に必要な情報だけをログ記録することだ。infoとdebug情報だけをログ記録しよう。
計測: Bourgon氏は計測は安価だと考え、Prometheusを使って全リソースをモニターすることを推奨する。
グローバル状態: 暗黙のグローバルな依存関係とグローバル状態を取り除こう。
テスト: パッケージテストを実行しよう。Design for testing by writing using a functional style which implies making 依存関係をパラメータとして明示するような機能スタイルを使って書くことで、テストを設計しよう。グローバル状態への依存を避けよう、インターフェイスを使おう。
依存物の管理: プロジェクトのリポジトリにある依存物をコピーして、バイナリの構築時に利用しよう。Bourgon氏はニーズにあわせて、gvt、vendetta, glide、gbなどを使うことを提案する。
ビルド: go build
の代わりにgo install
を使おう。go install
は依存するものをキャッシュして、呼び出しやすいようGOPATH/bin
に置いてくれるためだ。
これらの推奨は、マイクロサービス構築のための分散プログラミングツールキットGo kitを作るときに彼が適用したものだ。
Bourgon氏は2009年以降、SoundCloudとWeaveworksでGoを使っており、時系列イベントデータベースRoshiとGo kitを開発している。
QCon London 2016のセッション「Successful Go Program Design, 6 Years On」は今年の終わりに公開される予定だ。
Rate this Article
- Editor Review
- Chief Editor Action