ビジネスで6年のベストプラクティスを実践

2014年、GopherConカンファレンスのオープニングで「Go: Production Environmentsのベストプラクティス 」と題したプレゼンテーションを行いました。 SoundCloudで、私たちはGoの最初のユーザーの1人でしたが、それまでに2年間で既に書かれており、何らかの形でGoを戦闘でサポートしていました。 この間、私たちは何かを学び、この経験の一部を共有しようとしました。



それ以来、運用とインフラストラクチャを担当するSoundCloudチームで1日中Goでプログラミングを続け、現在はWeaveworksWeave ScopeWeave Meshを使用しています。 また、オープンソースのマイクロサービスツールキットであるGoキットにも熱心に取り組みました。 そしてこの間ずっと、私はGoプログラマーコミュニティの発展に積極的に参加し、ヨーロッパやアメリカでの会議や会議で多くの開発者と出会い、成功と失敗のストーリーを収集しました。



2015年11月、Goリリースの6周年に 、最初のパフォーマンスを思い出しました。 時の試練に合格したベストプラクティスはどれですか? それらのどれが時代遅れであるか、効果がありませんか? 新しいテクニックはありますか? 3月に、 QCon Londonカンファレンスで講演する機会がありました。そこでは、2014年のベストプラクティスと2016年までのGoのさらなる開発について話しました。 この投稿は私のプレゼンテーションからの抜粋です。



私は、テキストの重要なポイントを「トップヒント-最高のヒント」の形式で強調しました。



そして、ここにコンテンツがあります:



  1. 開発環境
  2. リポジトリ構造
  3. 書式設定とスタイル
  4. 構成
  5. プログラム開発
  6. ロギングとメトリック
  7. テスト中
  8. 依存関係管理
  9. 組み立てと展開
  10. おわりに


開発環境



Go開発環境の規則は、GOPATHの使用に基づいています。 2014年に、単一のグローバル変数GOPATHが存在する必要があるという見解を擁護しました。 それ以来、私の立場は幾分和らいでいます。 私はまだ、他の条件が同じであれば、これが最良の選択肢であると考えていますが、プロジェクト、チーム、その他の機能にも大きく依存します。



あなたまたはあなたの会社が主にバイナリを作成する場合、プロジェクトごとに別々のGOPATHを使用すると特定の利点が得られます。 このような場合、Dave Cheneyと寄稿者の新しいgbユーティリティを使用して、この目的のために標準のgo



ツールを置き換えることができます。 このユーティリティはすでに多くの肯定的なレビューを受けています。



一部の開発者は、2つのディレクトリ(2エントリ)でGOPATHを使用しています。たとえば、 $HOME/go/external:$HOME/go/internal



です。 go-teamは常にそのようなケースの処理方法を知っていました: go get



は依存関係を最初のパスのディレクトリにダウンロードするため、このソリューションは、内部コードをサードパーティから厳密に分離する必要がある場合に役立ちます。



一部の開発者がGOPATH/bin



PATH



に入れるのを忘れていることに気付きました。 ただし、これにより、 go get



で取得した実行可能ファイルの実行が簡単になり、(推奨) go install



コードアセンブリメカニズムの操作も簡単になります。 しない理由はありません。



上のヒント - $PATH



$GOPATH/bin



と、インストールされたプログラムへのアクセスが容易になります。



あらゆる種類のエディターとIDEのおかげで、開発環境は継続的に改善されています。 あなたがvimファンなら、すべてが完璧に機能しました:Fatih Arslanの疲れ知らずで信じられないほど効果的な仕事のおかげで、 vim-goプラグインはクラスの最高のツールである本当の芸術作品に変わりました。 私はEmacsにはあまり馴染みがありませんが、goinmodel Dominik Honnefがこの分野で依然として支配しています。



引き続き、多くの人がSublime Text + GoSublimeバンドルを引き続き使用しています。 彼女とスピードで競うのは難しいです。 しかし、明らかに、最近、 Electronに基づいた編集者により多くの注意が払われました。 Atom + go-plusには多くのファンがいます。特に、ある言語からJavaScriptに頻繁に切り替える必要のある開発者の間で特にそうです。 Visual Studio Code + vscode-goバンチはダークホースでした:Sublime Textよりも遅く実行されますが、Atomよりも著しく高速であり、同時にクリックして定義する(クリックしてオブジェクトの場所に移動する)など、私にとって重要な重要な機能を完全にサポートします) Thomas Adamから紹介されて以来、私はこのバンドルを6か月間毎日使用しています。 素晴らしいこと。



本格的なIDEについては、特別に作成されたLiteIDEを挙げることができます 。これは定期的に更新され、独自のファンがいます。 Go Intellijの興味深いプラグインもあり、常に改善されています。



リポジトリ構造



プロジェクトがより成熟するのに十分な時間があり、その結果、多くの明確なアプローチが開発されました。 プロジェクトが何であるかは、リポジトリの構造によって異なります。 閉じたプロジェクトまたは会社の内部プロジェクトについて話している場合は、先に進むことができます:独自のGOPATHを持たせ、カスタムビルドツールを使用し、喜びをもたらし、生産性を向上させる場合は何でもします。



ただし、パブリックプロジェクト(たとえば、オープンソース)の場合、ルールはより厳格になります。 コードはgo get



と互換性がある必要があります。これは、ほとんどのGo開発者があなたの作業を活用したい方法だからです。

理想的なリポジトリ構造は、エンティティのタイプによって異なります。 これらが排他的に実行可能なバイナリファイルまたはライブラリである場合、コンシューマがベースパスに沿ってgo get



またはimportを使用できることを確認する必要があります。 したがって、パッケージのメインコードまたはメインコードをgithub.com/name/repo



にインポートし、サブパッケージにはサブフォルダーを使用します。



リポジトリがバイナリファイルとライブラリの組み合わせである場合、 メインエンティティを定義して、リポジトリのルートに配置する必要があります。 たとえば、リポジトリの大部分が実行可能ファイルで構成されているが、ライブラリとしても使用できる場合、おそらく次のように構造化することをお勧めします。



 github.com/peterbourgon/foo/ main.go // package main main_test.go // package main lib/ foo.go // package foo foo_test.go // package foo
      
      





有用なアドバイス: lib/



サブフォルダーでは、フォルダー自体ではなく、ライブラリーの名前に従ってパッケージに名前を付ける方が良いです。 つまり、この例では、 package lib



ではなくpackage foo



です。 これはかなり厳密なGoイディオムの例外ですが、実際にはユーザーにとって非常に便利です。 HTTPサービスのストレステスト用ツールである素晴らしいtsenart / vegetaリポジトリも同様に配置されています。



トップヒント -fooリポジトリが主に実行可能なバイナリファイルで構成されている場合は、ライブラリコードをlib/



サブフォルダーに入れ、 package foo



名前を付けpackage foo





リポジトリが基本的にライブラリであるが、1つまたは2つの実行可能プログラムも含まれている場合、構造は次のようになります。



 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つの理由で便利です。





トップヒント -リポジトリの主な目的がライブラリの場合、実行可能プログラムのコードをcmd/



内のサブフォルダーに配置します。



ここでの主なアイデア:ユーザーの世話-プロジェクトの基本機能の使用を簡素化します。 ユーザーのニーズに焦点を当てたこの抽象的なアイデアは、Goの精神そのものに合っているように思えます。



書式設定とスタイル



ここで大きな変化はありません。 これはGoが正しい道を歩んだ場所の1つであり、コミュニティの合意とそれによる言語の安定性に本当に感謝しています。 コードレビューコメントは優れており、コード改訂中に基準を満たすために必要な最低限の基準セットである必要があります。 また、タイトルに異議を唱える状況や矛盾がある場合は、Andrew Gerrandの優れた慣用的な命名規則を使用できます。



ヒント -Andrew Gerrandの命名規則を活用してください。



ツールに関しては、すべてが良くなりました。 エディターを構成して、保存時にgofmtがトリガーされるようにするか、goimportを改善します (ここにオブジェクトが1つもないことを願っています)。 go vetユーティリティを使用しても誤検知はほとんど発生しないため、事前コミットフックの一部として使用できます。 また、 gometalinterの優れたコード品質管理ユーティリティに注意してください 。 誤検知が発生する可能性があるため、何らかの形で独自の契約を指定することは理にかなっています。



構成



構成は、ランタイム環境とプロセスの間にあります。 明示的で十分に文書化されている必要があります。 私は今でもflagパッケージを使用し、使用することをお勧めしますが、それでも設定がより馴染みのあることを好みます。 引数の詳細で簡潔な形式があるように、引数の標準構文をgetoptsスタイルで取得したいと思います。 また、使用法のテキストをもっとコンパクトにしたいです。



Twelve-Factor Appの規則に従うアプリケーションは、環境変数を構成に使用するように動機付けます。 各変数もflagとして定義されていれば 、これは正常だと思います。 ここでは自明性が重要です。アプリケーションの実行時の動作の変更は、簡単に検出および文書化された方法で行われる必要があります。



すでに2014年に言ったが、繰り返す必要があると考えている。func main()内でフラグを定義して解析するfunc main()



のみが、ユーザーが使用できるフラグを決定する権利を持っています。 ライブラリで動作を構成できる場合、構成パラメーターは型コンストラクターの一部である必要があります。 構成をパッケージのグローバルスコープに移動すると、利点の錯覚が生じますが、節約は誤りです。コードのモジュール性を壊すため、他の開発者が依存関係の関係を理解するのが難しくなり、独立した並列化されたテストを書くこともはるかに難しくなります。



Tipヒント -func func main()



のみが、ユーザーが使用できるフラグを決定する権限を持ちます。



コミュニティは、これらのすべてのプロパティが組み合わされるフラグの包括的なパッケージを非常にうまく作成できると思います。 既に存在する可能性があります。 もしそうなら、 私に知らせてください 。 私は間違いなくそれを使用します。



プログラム開発



会話では、プログラム開発の他の多くの側面を議論するための出発点として構成を使用しました(2014年にこのトピックを取り上げませんでした)。 まず、コンストラクターを見てみましょう。 すべての依存関係を正しくパラメーター化すると、コンストラクターが非常に大きくなる可能性があります。



 foo, err := newFoo( *fooKey, bar, 100 * time.Millisecond, nil, ) if err != nil { log.Fatal(err) } defer foo.close()
      
      





構成オブジェクトを使用して、そのような構成を表現した方がよい場合があります。構成オブジェクトは、構成されたオブジェクトの動作を決定するオプションのパラメーターを取る構造ですfooKey



パラメーターfooKey



必須であり、他のすべてが妥当なデフォルト値を持っているか、 fooKey



と仮定します。



私はしばしば、構成オブジェクトがいくらか断片化された方法で構築されるプロジェクトに出くわします:



 //    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()
      
      





ただし、いわゆる構造初期化構文を使用して、単一の式を使用して一度にオブジェクトを構築する方がはるかに優れています。



 //    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



の定義を反映して、すべてのフィールドが美しく区切られ、インデントされます。



cfg



オブジェクトを作成し、すぐに使用することに注意してください。 この場合、 newFoo



コンストラクターに構造体宣言を直接埋め込むことで、中間状態のもう1つのステップを回避し、別のコード行を保存できます。



 //    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") // ... }
      
      





そのため、ここではデフォルトで有用なものを提供する必要があります。 インターフェイスの種類のおかげで、インターフェイスのノーオペレーション実装(つまり、操作を実行しない実装、スタブ。翻訳者のメモ)を提供するものを渡すことができます。 そのため、stdlib ioutilパッケージにはio.Writer



というioutil.Discard



オペレーションio.Writer



が付属しています。



トップヒント -デフォルトのノーオペレーション実装では、nilチェックを避けます。



これをfooConfig



オブジェクトに渡すこともできますが、これはかなり脆弱なソリューションです。 呼び出し元のコードが呼び出しの場所でこれを行うのを忘れた場合、再びnil



パラメーターを取得します。 代わりに、コンストラクター内で保護できます。



 func newFoo(..., cfg fooConfig) *foo { if cfg.Output == nil { cfg.Output = ioutil.Discard } // ... }
      
      





これは、Go Make Zero値のイディオムの単なるアプリケーションです。 つまり、null値( nil



)を使用して、適切なデフォルトの動作(no-op)を提供できます。



先端のヒント -特に構成オブジェクトでは、null値を有用にします。



コンストラクターに戻りましょう。 パラメーターfooKey



bar



period



およびoutput



依存関係です。 foo



オブジェクトの起動と操作の成功は、それらのそれぞれに依存します。 Goでの6年間の毎日のプログラミングと大規模プロジェクトの観察で正確に学んだことは、 依存関係を明示的にする必要があるということです。



先端のヒント -依存関係を明示的に!



あいまいな依存関係または暗黙的な依存関係が、技術サポート、混乱、バグ、未払いの技術的負債のための人件費の信じられないほどの量の理由だと思います。 foo



型のprocess()メソッドを考えてみましょう:



 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



なりf.Logger.Printf







 func (f *foo) process() { fmt.Fprintf(f.Output, "start\n") result := f.Bar.compute() f.Logger.Printf("bar: %v", result) // . // ... }
      
      





以前は、特定の種類の作業を担保としてのロギングと見なしていました。 したがって、グローバルロガーなどのサポートライブラリを使用して、負担を軽減できることを嬉しく思います。 ただし、メトリックスと同様に、ロギングはサービスの機能において決定的な役割を果たすことがよくあります。 そして、可視性のグローバル空間で依存関係を非表示にすることで、ロギングなどの一見無害な形で、またはパラメーター化を気にしない他のより重要な主題コンポーネントの形で、私たちに打撃を与えることができます。 。 厳格なルールで将来の痛みから身を守りましょう:すべての中毒を明示的にしましょう。



トップヒント -ロガーは依存関係であり、他のコンポーネント、データベースクライアント、コマンドライン引数などへのリンクも同様です。



もちろん、ロガーの妥当なデフォルトを取得する必要があります。

 func newFoo(..., cfg fooConfig) *foo { // ... if cfg.Logger == nil { cfg.Logger = log.New(ioutil.Discard, ...) } // ... }
      
      





ロギングとメトリック



全体としての問題について話すと、ログを記録することで、戦闘での経験がはるかに多くなり、問題に対する敬意が強くなりました。 ロギングは、予想よりもはるかに高価であり、システムのボトルネックにすぐに変わる可能性があります。 このトピックについては、別の投稿で詳しく説明しましたが、簡単に言えば:





ロギングが高価な場合、メトリックは安価です。 コードベースの重要なコンポーネントからメトリックを削除します。 キューなどのリソースの場合は、 Brendan GreggのUSEメソッドを使用して測定します:Utilization、Saturation、Error count(rate)。 これが何らかのエンドポイントである場合、トムウィルキーのREDメソッドに従って測定します:要求カウント(率)、エラーカウント(率)、期間。



この問題で選択する機会があれば、 プロメテウスを測定システムとして使用することをお勧めします。 そしてもちろん、メトリックも依存関係です!



ロガーとメトリックから脱線して、グローバル状態を直接見てみましょう。 Goに関するいくつかの事実を次に示します。





これらの事実は個別に許容されますが、一般的には困難です。 つまり、固定グローバルロガーを使用して、コンポーネントによってログに送信された出力をどのようにテストできますか? このデータをリダイレクトする必要がありますが、並行してテストする方法は? まさか? 答えは不十分です。 または、たとえば、異なる要件を持つ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.Client



。 しかし、これは具体的なタイプであるため、この関数をテストする場合は、特定のhttp.Clientを提供する必要があります。これにより、HTTPを介した実際の接続の確立が強制されます。 これは良くありません。 より良い方法: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



. : foo



foo



, , http.Client



, .







テスト中



2014 , - . (stdlib) . . Go , . , . testing .



TDD/BDD , DSL , , . , . , , , , . When in Go, do as Gophers do ( Go, , ) : — Go, , , .



, . GOPATH, , , DSL . , , . , .



. (Mitchell Hashimoto) ( SpeakerDeck , YouTube ), .



: , Go , . , , , .



Top Tip — .



http.Client



, , - , . - -, HTTP-, , , . - - .



Top Tip — , .





. 2014 , (vendor). - : . , Go 1.6 GO15VENDOREXPERIMENT vendor/ . . , , . :





Top Tip — .



. Go . , . , 1.5 , . , : 1 , 2 . , : .



Top Tip — .



, () API. , .



open source , , . , , . GO15VENDOREXPERIMENT , .



, . Etcd , , , Go Windows. , , , . , , , - .





, ( ) go install



go build



. install



$GOPATH/pkg



, . $GOPATH/bin



, .



Top Tipgo install



go build



.



, , gb . . , Go 1.5 - « ». GOOS GOARCH, go-. .



, , , , Ruby, Python, JVM. : , — FROM scratch. Go , .



: , , — -. . , AMI EC2, . .



おわりに



Top Tips:



  1. $GOPATH/bin



    $PATH



    , .
  2. foo , lib/ package foo



    .
  3. — , cmd/.
  4. .
  5. func main()



    , .
  6. , . , , .
  7. nil no-op- .
  8. , .
  9. !
  10. , , , . .
  11. .
  12. , .
  13. .
  14. .
  15. go install



    go build



    .


Go , , - . — — . ( Go Proverbs ), , « » (up the stack) , , Go.



Go.



All Articles