Practical Go:実世界でサポートされるプログラムを書くためのヒント

この記事では、Goコードを作成するためのベストプラクティスに焦点を当てています。 プレゼンテーションのスタイルで構成されていますが、通常のスライドはありません。 各項目を簡潔かつ明確に説明します。



最初に、プログラミング言語のベストプラクティスの意味について同意する必要があります。 ここでは、Goテクニカルマネージャー、ラスコックスの言葉を思い出すことができます。



時間の要因や他のプログラマーを追加すると、ソフトウェアエンジニアリングがプログラミングに起こります。


したがって、ラスはプログラミングの概念とソフトウェアエンジニアリングを区別します。 前者の場合はプログラムを自分で作成し、後者の場合は他のプログラマーが時間をかけて作業する製品を作成します。 エンジニアが出入りします。 チームは成長または縮小します。 新機能が追加され、バグが修正されました。 これがソフトウェア開発の性質です。



内容





1.基本原則



私はあなたの中でGoの最初のユーザーの一人かもしれませんが、これは私の個人的な意見ではありません。 これらの基本原則は、Go自体の根底にあります。



  1. シンプルさ
  2. 読みやすさ
  3. 生産性


ご注意 「パフォーマンス」または「同時実行性」については言及していません。 Goより速い言語がありますが、単純に比較することはできません。 並列処理を最優先とする言語もありますが、読みやすさやプログラミングの生産性の観点から比較することはできません。



パフォーマンスと同時実行性は重要な属性ですが、シンプルさ、読みやすさ、生産性ほど重要ではありません。



シンプルさ



「シンプルさは信頼性の前提条件です」 -Edsger Dijkstra


シンプルさを追求する理由 Goプログラムがシンプルであることが重要なのはなぜですか?



私たちはそれぞれ、理解できないコードに遭遇しましたよね? プログラムの別の部分が壊れてしまうため、変更を行うのが怖いときは、あなたがあまり理解しておらず、修正方法もわかりません。 これが困難です。



「ソフトウェアの設計には2つの方法があります。1つ目は単純な方法で明白な欠陥をなくすことであり、2つ目はソフトウェアを非常に複雑にして明白な欠陥がないことです。 最初の方がはるかに難しい。」 -C.E. R. Hoar


複雑さは、信頼できるソフトウェアを信頼できないソフトウェアに変えます。 複雑さはソフトウェアプロジェクトを殺すものです。 したがって、シンプルさがGoの最終目標です。 どんなプログラムを書くにしても、それらは単純でなければなりません。



1.2。 読みやすさ



「可読性は保守性の不可欠な部分です」-Mark Conference、2018 Conference、JVM Conference


コードが読み取り可能であることが重要なのはなぜですか? 読みやすさのために努力する必要があるのはなぜですか?



「プログラムは人々のために書かれるべきであり、機械はそれらを実行するだけです」 -ハル・アベルソンとジェラルド・サスマン、「コンピュータープログラムの構造と解釈」


Goプログラムだけでなく、一般にすべてのソフトウェアは人のために人によって書かれています。 マシンもコードを処理するという事実は二次的です。



書き込まれたコードは、人々によって繰り返し読み取られます。数千回ではないにしても、数百回です。



「プログラマーにとって最も重要なスキルは、アイデアを効果的に伝える能力です。」 - ガストン・ホーカー


読みやすさは、プログラムの機能を理解するための鍵です。 コードを理解できない場合、どのように維持するのですか? ソフトウェアをサポートできない場合、書き換えられます。 これはあなたの会社がGoを使用する最後の機会かもしれません。



自分でプログラムを作成している場合は、自分に合った方法を実行してください。 しかし、これが共同プロジェクトの一部である場合、またはプログラムが要件、機能、またはそれが機能する環境を変更するのに十分な期間使用される場合、目標はプログラムを保守可能にすることです。



サポートされているソフトウェアを作成する最初のステップは、コードが明確であることを確認することです。



1.3。 生産性



「デザインとは、今日動作するようにコードを整理する技術ですが、常に変化をサポートします。」 -サンディ・メッツ


最後の基本原則として、開発者の生産性に名前を付けたいと思います。 これは大きなトピックですが、それは比率に帰着します:有用な作業に費やす時間と、理解できないコードベースでのツールまたは絶望的な放浪からの応答を待つ時間です。 Goプログラマーは、多くの作業を処理できると感じるはずです。



Go言語は、C ++プログラムのコンパイル中に開発されたという冗談です。 クイックコンパイルはGoの重要な機能であり、新しい開発者を引き付ける重要な要素です。 コンパイラーは改善されていますが、一般的に、Goでは他の言語でのわずかなコンパイルに数秒かかります。 Go開発者は、動的言語のプログラマと同じくらい生産的であると感じていますが、これらの言語の信頼性に問題はありません。



開発者の生産性について根本的に話すと、Goプログラマーはコードを読むことは書くよりも本質的に重要であることを理解します。 このロジックでは、Goはツールを使用して、特定のスタイルですべてのコードをフォーマットすることさえできます。 これにより、特定のプロジェクトの特定の方言を学習する際のわずかな困難が排除され、通常のコードと比較して見た目が間違っているため、エラーの特定に役立ちます。



Goプログラマは、奇妙なコンパイルエラー、複雑なビルドスクリプトのデバッグ、または運用環境でのコードの展開に何日も費やしません。 そして最も重要なことは、同僚が書いたことを理解しようとして時間を無駄にしないことです。



Go開発者がスケーラビリティについて話すとき、それは生産を意味します。



2.識別子



最初に説明するトピック-identifiersは、 名前の同義語です。変数、関数、メソッド、型、パッケージなどの名前です。



「悪い名前は貧弱なデザインの症状です」 - デイブ・チェイニー


Goの制限された構文を考えると、オブジェクト名はプログラムの可読性に大きな影響を及ぼします。 可読性は優れたコードの重要な要素であるため、優れた名前を選択することが重要です。



2.1。 簡潔さではなく明快さに基づいた名前識別子



「コードが明白であることが重要です。 1行でできること、3行でする必要があります。」 - ユキア・スミス


Goは、扱いにくいワンライナーやプログラムの最小行数に対して最適化されていません。 ディスク上のソースコードのサイズも、エディターでプログラムを入力するのに必要な時間も最適化しません。



「良い名前はいい冗談のようなものです。 説明する必要がある場合、それはもはや面白くありません。」 - デイブチェイニー


最大の明瞭さの鍵は、プログラムを識別するために選択する名前です。 良い名前にはどのような特質がありますか?





これらのプロパティのそれぞれをさらに詳しく検討してみましょう。



2.2。 IDの長さ



Goのスタイルは、短い変数名で批判されることがあります。 Rob Pikeが言ったように 、「Goプログラマーは正しい長さの識別子を必要としています 。」



Andrew Gerrandは、重要性を示すより長い識別子を提供しています。



「名前の宣言からオブジェクトの使用までの距離が長いほど、名前は長くなるはずです」 -Andrew Gerrand


したがって、いくつかの推奨事項を作成できます。





例を考えてみましょう。



type Person struct { Name string Age int } // AverageAge returns the average age of people. func AverageAge(people []Person) int { if len(people) == 0 { return 0 } var count, sum int for _, p := range people { sum += p.Age count += 1 } return sum / count }
      
      





10行目では、範囲p



変数p



宣言され、次の行から1回だけ呼び出されます。 つまり、変数はページ上に非常に短時間しか存在しません。 読者がプログラムでのp



の役割に興味がある場合、2行だけを読むだけで十分です。



比較のために、 people



関数パラメーターで宣言され、7行が有効です。 sum



count



についても同じことが言えますので、長い名前を正当化します。 読者はそれらを見つけるためにより多くのコードをスキャンする必要があります:これはより多くの識別名を正当化します。



sum



s



を、 count



c



(またはn



)を選択できますが、これにより、プログラム内のすべての変数の重要性が同じレベルに低下します。 people



p



に置き換えることができますが、問題がありますfor ... range



繰り返し変数を呼び出すものfor ... range



。 短命の反復変数は、それが由来するいくつかの値よりも長い名前を取得するため、一person



は奇妙に見えます。



ヒント 段落間の空行はテキストの流れを中断するため、関数のストリームを空行で区切ります。 AverageAge



は3つの連続した操作がAverageAge



ます。 最初に、ゼロによる除算、次に総年齢と人数の結論、最後の平均年齢の計算をチェックします。


2.2.1。 主なものはコンテキストです



ほとんどの命名のヒントはコンテキスト固有であることを理解することが重要です。 これは原則であり、規則ではない、と言いたいです。



i



index



IDの違いは何ですか? たとえば、そのようなコードを明確に言うことはできません



 for index := 0; index < len(s); index++ { // }
      
      





基本的に読みやすい



 for i := 0; i < len(s); i++ { // }
      
      





この場合、領域i



またはindex



for



ループの本体によって制限され、追加の冗長性はプログラムの理解にほとんど役立たないため、2番目のオプションは悪くないと思います。



しかし、これらの関数のうち、読みやすいのはどれですか?



 func (s *SNMP) Fetch(oid []int, index int) (int, error)
      
      





または



 func (s *SNMP) Fetch(o []int, i int) (int, error)
      
      





この例では、 oid



はSNMPオブジェクトIDの省略形であり、 o



追加の省略形は、コードを読み取るときに、コード内の文書化された表記法から短い表記法への読み取りを強制します。 同様に、 index



i



に減らすと、SNMPメッセージでは各OIDのサブ値がインデックスと呼ばれるため、理解が難しくなります。



ヒント 1つの広告で長い正式なパラメータと短い正式なパラメータを組み合わせないでください。


2.3。 タイプごとに変数に名前を付けないでください



ペットを「犬」や「猫」と呼ばないでしょ? 同じ理由で、変数名にタイプ名を含めないでください。 タイプではなくコンテンツを記述する必要があります。 例を考えてみましょう:



 var usersMap map[string]*User
      
      





この発表は何がいいですか? これはマップであり、 *User



タイプと関係があることがわかり*User



。これはおそらく良いことです。 ただし、 usersMap



実際にはマップであり、静的に型付けされた言語であるGoは、スカラー変数が必要な場所で誤ってこの名前を使用しないため、 Map



サフィックスは冗長です。



他の変数が追加される状況を考えてみましょう。



 var ( companiesMap map[string]*Company productsMap map[string]*Products )
      
      





タイプマップの3つの変数: usersMap



usersMap



およびproductsMap



があり、すべての行が異なるタイプにマップされています。 これらはマップであることがわかります。また、コードがmap[string]*User



予期しているcompanyMapを使用しようとすると、コンパイラがエラーをスローすることもわかっていmap[string]*User



。 この状況では、サフィックスMap



がコードの明瞭さを向上させないことは明らかです。これらは単なる追加文字です。



変数の型に似た接尾辞を避けることをお勧めします。



ヒント users



の名前が​​本質を十分に明確に説明していない場合、 usersMap



usersMap



ます。


このヒントは、関数パラメーターにも適用されます。 例:



 type Config struct { // } func WriteConfig(w io.Writer, config *Config)
      
      





*Config



パラメーターの*Config



名は冗長です。 これが*Config



であることは既に知っています。



この場合、変数の有効期間が短い場合は、 conf



またはc



検討してください。



エリア内のある時点で*Config



が複数ある場合、 original



conf1



conf2



の名前は、 original



の名前とupdated



た名前よりも意味conf2



少なくなります。後者は混同しにくいためです。



ご注意 パッケージ名に適切な変数名を盗まないでください。



インポートされた識別子の名前には、パッケージの名前が含まれます。 たとえば、 context



パッケージのContext



タイプはcontext.Context



と呼ばれます。 これにより、パッケージで変数または型context



を使用できなくなります。



 func WriteLog(context context.Context, message string)
      
      





これはコンパイルされません。 context.Context



ローカルで宣言する場合、たとえばctx



ような名前が伝統的に使用されているのはそのためです。



 func WriteLog(ctx context.Context, message string)
      
      





2.4。 単一の命名スタイルを使用する



良い名前のもう1つの特性は、予測可能であることです。 読者はすぐにそれを理解しなければなりません。 これが一般名である場合、読者は前回から意味を変えていないと想定する権利があります。



たとえば、コードがデータベース記述子を回る場合、パラメーターが表示されるたびに、同じ名前にする必要があります。 d *sql.DB



dbase *sql.DB



DB *sql.DB



およびdatabase *sql.DB



ようなあらゆる種類の組み合わせの代わりに、次の1つを使用することをおDB *sql.DB



database *sql.DB







 db *sql.DB
      
      





コードを理解する方が簡単です。 db



が表示されている場合、それは*sql.DB



あり、ローカルで宣言されているか、呼び出し元によって提供されていることがわかります。



メソッドの受信者に関する同様のアドバイス。 このタイプの各メソッドに同じ受信者名を使用します。 そのため、このタイプのさまざまなメソッドの中で、読者が受信者の使用を理解しやすくなります。



ご注意 Go Recipients Short Name Agreementは、以前に表明された推奨事項と矛盾します。 これは、 snake_case



代わりにsnake_case



を使用するなど、初期段階で行われた選択が標準スタイルになるケースの1つです。


ヒント Goスタイルは、タイプから派生した受信者の1文字の名前または略語を示します。 受信者名がメソッドのパラメーター名と競合する場合があることが判明する場合があります。 この場合、パラメーター名をもう少し長くして、順番に使用することを忘れないでください。


最後に、一部の1文字の変数は、伝統的にループとカウントに関連付けられています。 たとえば、 i



j



k



は通常for



ループの帰納的変数、 n



通常カウンターまたは累積加算器に関連付けられ、 v



はエンコード関数の典型的な値の略語、 k



通常マップキーに使用され、 s



string



型のパラメーターの略語としてよく使用されstring







上記のdb



例と同様に、プログラマー i



が帰納的変数であることを期待しています。 彼らがコードでそれを見れば、彼らはすぐにループを見ることを期待する。



ヒント ネストされたループが非常に多く、 i



j



およびk



変数が不足している場合は、関数をより小さな単位に分割することができます。


2.5。 単一の宣言スタイルを使用する



Goには、変数を宣言するための少なくとも6つの異なる方法があります。





まだすべてを覚えていないに違いない。 Go開発者はおそらくこれを間違いと見なしますが、何も変更するには遅すぎます。 この選択で、均一なスタイルを確保する方法は?



私は、自分自身で可能な限り使用しようとする変数を宣言するスタイルを提案したいと思います。





Goにはある型から別の型への自動変換がないため、最初と3番目の例では、代入演算子の左側の型は右側の型と同じである必要があります。 コンパイラは、右側の型から宣言された変数の型を推測できるため、例はより簡潔に記述できます。



 var players = 0 var things []Thing = nil var thing = new(Thing) json.Unmarshall(reader, thing)
      
      





ここでは、 players



の初期値はいずれの場合もゼロであるため、 players



明示的に0



に初期化され0



、これは冗長です。 したがって、null値を使用することを明確にする方が適切です。



 var players int
      
      





2番目のオペレーターはどうですか? タイプを判別して書き込むことはできません



 var things = nil
      
      





nil



nil



ないためです。 代わりに、選択肢があります:または、ゼロ値を使用してスライスします...



 var things []Thing
      
      





...または要素がゼロのスライスを作成しますか?



 var things = make([]Thing, 0)
      
      





2番目の場合、スライスの値ゼロではなく 、短い形式の宣言を使用して読者に明確にします。



 things := make([]Thing, 0)
      
      





これは、明示的に初期化することにしたことを読者に伝えます。



したがって、3番目の宣言に進みます。



 var thing = new(Thing)
      
      





ここでは、変数の明示的な初期化と、一部のGoプログラマーが気に入らない「ユニークな」キーワードnew



導入の両方があります。 推奨される短い構文を使用すると、次のようになります



 thing := new(Thing)
      
      





これにより、 thing



明示的にnew(Thing)



結果に初期化されますが、それでも非定型のnew



は残されます。 この問題は、リテラルを使用して解決できます。



 thing := &Thing{}
      
      





これはnew(Thing)



似ており、そのような複製はGoプログラマを混乱させます。 ただし、これは、 Thing{}



へのポインタとゼロのThing



値を使用して明示的に初期化することを意味します。



しかし、 thing



ゼロ値で宣言されているという事実を考慮し、 json.Unmarshall



thing



のアドレスを渡すために演算子のアドレスを使用する方がjson.Unmarshall



です。



 var thing Thing json.Unmarshall(reader, &thing)
      
      





ご注意 もちろん、ルールには例外があります。 たとえば、2つの変数が密接に関連している場合があるため、次のように記述するのは奇妙です。



 var min int max := 1000
      
      





より読みやすい宣言:



 min, max := 0, 1000
      
      





要約すると:





ヒント 複雑なものを明示的に指摘します。



 var length uint32 = 0x80
      
      





ここで、特定の数値型を必要とするライブラリでlength



を使用できます。このオプションは、長さ型が短い宣言でよりもuint32として具体的に選択されることをより明確に示します。



 length := uint32(0x80)
      
      





最初の例では、明示的に初期化したvar宣言を使用して意図的にルールを破ります。 標準からの逸脱により、読者は異常なことが起こっていることを理解できます。


2.6。 チームのために働く



ソフトウェア開発の本質は、読みやすくサポートされたコードを作成することだとすでに述べました。 あなたのキャリアのほとんどは、おそらく共同プロジェクトで働くでしょう。 この状況での私のアドバイスは、チームが採用したスタイルに従うことです。



ファイルの途中でスタイルを変更するのは面倒です。 個人の好みを損ねるものの、一貫性は重要です。 私の経験則では、コードがgofmt



に適合する場合、問題は通常議論する価値はありません。



ヒント コードベース全体で名前を変更する場合は、これを他の変更と混ぜないでください。 誰かがgit bisectを使用している場合、彼は別の変更されたコードを見つけるために何千もの名前変更を歩き回るのを好みません。


3.コメント



より重要なポイントに移る前に、コメントするのに数分かかりたいと思います。



「良いコードには多くのコメントがあり、悪いコードには多くのコメントが必要です。」 -実用プログラマー、デイブ・トーマスとアンドリュー・ハント


コメントは、プログラムを読みやすくするために非常に重要です。 各コメントは、次の3つのうち1つだけを実行する必要があります。



  1. コードの機能を説明します。
  2. 彼がそれをどのように行うを説明してください。
  3. 理由を説明してください


最初の形式は、公開キャラクターに関するコメントに最適です。



 // Open     . //           .
      
      





2番目は、メソッド内のコメントに最適です。



 //     var results []chan error for _, dep := range a.Deps { results = append(results, execute(seen, dep)) }
      
      





3番目の形式(「理由」)は、最初の2つを置き換えたり置き換えたりしないという点でユニークです。 このようなコメントは、現在の形式でコードを書くことに至った外部要因を説明しています。 多くの場合、このコンテキストがなければ、コードがこのように記述されている理由を理解することは困難です。



 return &v2.Cluster_CommonLbConfig{ //  HealthyPanicThreshold HealthyPanicThreshold: &envoy_type.Percent{ Value: 0, }, }
      
      





この例では、HealthyPanicThresholdがゼロパーセントに設定されている場合に何が起こるかがすぐにはわかりません。 コメントは、値0がパニックしきい値を無効にすることを明確にすることを目的としています。



3.1。 変数と定数のコメントは、目的ではなく、その内容を説明する必要があります



先ほど、変数または定数の名前はその目的を説明するべきだと言いました。 しかし、変数または定数に関するコメントは、 目的ではなく内容を正確に説明する必要があります。



 const randomNumber = 6 //    
      
      





この例では randomNumber



6にrandomNumber



ている理由とその randomNumber



コメントで説明しています。 コメントには、 randomNumber



が使用される場所は記述されていません。 以下に例を示します。



 const ( StatusContinue = 100 // RFC 7231, 6.2.1 StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2 StatusProcessing = 102 // RFC 2518, 10.1 StatusOK = 200 // RFC 7231, 6.3.1
      
      





HTTPのコンテキストで 100



、RFC 7231のセクション6.2.1で定義されているように番号100



StatusContinue



として知られていStatusContinue







ヒント 初期値のない変数の場合、コメントには、この変数の初期化の責任者を記述する必要があります。



 // sizeCalculationDisabled ,   //     . . dowidth. var sizeCalculationDisabled bool
      
      





ここで、コメントは読者にdowidth



関数dowidth



の状態を維持する責任があることをsizeCalculationDisabled



ます。


ヒント 目の前に隠れます。 これはケイトグレゴリーからのアドバイスです。 変数の最適な名前がコメントに隠れている場合があります。



 //   SQL var registry = make(map[string]*sql.Driver)
      
      





名前registry



がその目的を十分に説明していないため、著者によってコメントが追加されました-それはレジストリですが、レジストリとは何ですか?



変数の名前をsqlDriversに変更すると、SQLドライバーが含まれていることが明らかになります。



 var sqlDrivers = make(map[string]*sql.Driver)
      
      





これで、コメントは冗長になり、削除できます。


3.2。 常に公開されているキャラクターを文書化する



パッケージのドキュメントはgodocによって生成されるため、パッケージで宣言されている各パブリック文字(変数、定数、関数、メソッド)にコメントを追加する必要があります。



Googleスタイルガイドの2つのガイドラインを次に示します。







 package ioutil // ReadAll   r      (EOF)   // ..    err == nil, not err == EOF. //  ReadAll     ,     //  . func ReadAll(r io.Reader) ([]byte, error)
      
      





この規則には1つの例外があります。インターフェイスを実装するメソッドを文書化する必要はありません。 具体的には、これを行わないでください。



 // Read   io.Reader func (r *FileReader) Read(buf []byte) (int, error)
      
      





このコメントは何の意味もありません。 彼はこのメソッドが何をするのかを述べていません。さらに悪いことに、彼はドキュメントを探すためにどこかに送っています。 この状況では、コメントを完全に削除することを提案します。



io



パッケージの例を次に示します。



 // LimitReader  Reader,    r, //    EOF  n . //   *LimitedReader. func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} } // LimitedReader   R,     //   N .   Read  N  //    . // Read  EOF,  N <= 0    R  EOF. type LimitedReader struct { R Reader // underlying reader N int64 // max bytes remaining } func (l *LimitedReader) Read(p []byte) (n int, err error) { if lN <= 0 { return 0, EOF } if int64(len(p)) > lN { p = p[0:lN] } n, err = lRRead(p) lN -= int64(n) return }
      
      





LimitedReader



宣言の直前には、それを使用する関数があり、 LimitedReader.Read



宣言はLimitedReader



自体の宣言のLimitedReader



続くことに注意してください。 LimitedReader.Read



自体は文書化されていませんが、これはio.Reader



実装であることが理解できます。



ヒント 関数を書く前に、それを説明するコメントを書いてください。 コメントを書くのが難しいと思うなら、これはあなたが書こうとしているコードを理解するのが難しいというサインです。


3.2.1。 悪いコードにコメントしないで、書き直してください



「悪いコードにはコメントしないでください-書き直してください」 -ブライアン・カーニハン


コメントでコードフラグメントの難しさを示すだけでは不十分です。 これらのコメントの1つに出くわした場合は、リファクタリングを思い出させてチケットを開始する必要があります。 金額がわかっている限り、技術的負債を抱えて生活できます。



標準ライブラリでは、問題に気づいたユーザーの名前とともにTODOスタイルでコメントを残すのが一般的です。



 // TODO(dfc)  O(N^2),     .
      
      





これは問題を修正する義務ではありませんが、指定されたユーザーが質問に連絡するのに最適な人物である場合があります。 他のプロジェクトでは、日付またはチケット番号をTODOに添付しています。



3.2.2。 コードをコメントアウトする代わりに、リファクタリングします



「良いコードは最高のドキュメントです。 コメントを追加しようとするとき、「このコメントが不要になるようにコードを改善するにはどうすればよいですか?」という質問を自問してください。


関数は1つのタスクのみを実行する必要があります。 一部のフラグメントが残りの関数に関連していないためにコメントを書きたい場合は、コメントを別の関数に抽出することを検討してください。



小さい機能は明確であるだけでなく、互いに別々にテストするのが簡単です。 コードを別の関数に分離すると、その名前でコメントを置き換えることができます。



4.パッケージ構造



「控えめなコードを書く:他のモジュールに余分なものを示さず、他のモジュールの実装に依存しないモジュール」 -Dave Thomas


各パッケージは、本質的に別個の小さなGoプログラムです。 関数またはメソッドの実装が呼び出し側にとって重要ではないように、パッケージのパブリックAPIを構成する関数、メソッド、およびタイプの実装も重要ではありません。



優れたGoパッケージは、ソースコードレベルで他のパッケージとの最小限の接続を目指しており、プロジェクトが成長しても、1つのパッケージの変更がコードベース全体にカスケードされません。 このような状況は、このコードベースで作業するプログラマを大きく阻害します。



このセクションでは、パッケージの設計について、その名前やメソッドや関数を書くためのヒントなどについて説明します。



4.1。 良いパッケージは良い名前で始まります



適切なGoパッケージは、品質の名前で始まります。これは、1語に限定された短いプレゼンテーションと考えてください。



前のセクションの変数名と同様に、パッケージ名は非常に重要です。このパッケージのデータ型について考える必要はありません。「このパッケージはどのサービスを提供しますか?」という質問をする方がよいでしょう。



協議会パッケージ名は、コンテンツではなく機能ごとに選択してください。


4.1.1。適切なパッケージ名は一意でなければなりません



各パッケージには、プロジェクト内で一意の名前が付けられています。パッケージの目的で名前を付けるというアドバイスに従えば、問題はありません。2つのパッケージの名前が同じであることが判明した場合、ほとんどの場合:



  1. パッケージ名が一般的すぎます。
  2. . , .


4.2。 base



, common



util





悪い名前の一般的な理由は、いわゆるサービスパッケージです。ここでは、時間の経過とともにさまざまなヘルパーとサービスコードが蓄積されます。そこで一意の名前を見つけるのは難しいので。これにより、多くの場合、パッケージ名は含まれているユーティリティから派生します。またはの



ような名前は、大規模なプロジェクトで発生します。このプロジェクトでは、パッケージの深い階層がルート化され、補助機能が共有されます。関数を新しいパッケージに抽出すると、インポートが失敗します。この場合、パッケージの名前はパッケージの目的を反映していませんが、プロジェクトの不適切な編成によるインポート機能の失敗のみを反映しています。そのような状況では、パッケージの呼び出し元を分析することをお勧めします。utils



helpers







utils



helpers



、および可能であれば、対応する関数を呼び出しパケットに移動します。これにヘルパーコードの複製が含まれる場合でも、2つのパッケージ間にインポート依存関係を導入するよりも優れています。



「(少しの)複製は、間違った抽象化よりもはるかに安価です。」 -Sandy Mets


ユーティリティ関数を含む1つのモノリシックパッケージではなく、ユーティリティ関数を多くの場所で使用する場合は、それぞれが1つの側面に焦点を当てた複数のパッケージを作成することをお勧めします。



協議会サービスパッケージには複数形を使用します。たとえば、strings



文字列処理ユーティリティの場合。


base



またはのような名前のパッケージはcommon



、2つ以上の実装の特定の共通機能またはクライアントとサーバーの共通タイプが別のパッケージにマージされるときによく発生します。このような場合、1つのパッケージ内のクライアント、サーバー、および共通コードを、その機能に対応する名前と組み合わせて、パッケージの数を減らす必要があると思います。



たとえば、するnet/http



個々のパッケージをしないclient



server



、代わりに、ファイルがあるclient.go



server.go



、対応するデータタイプと、だけでなく、transport.go



トータルの輸送のためには。



協議会識別子名にはパッケージ名が含まれることを覚えておくことが重要です。



  • Get



    パッケージの関数は、別のパッケージからnet/http



    http.Get



    リンクになります。

  • Reader



    パッケージのは、strings



    他のパッケージにインポートされるときに変換されますstrings.Reader





  • Error



    パッケージのインターフェイスは、net



    明らかにネットワークエラーに関連付けられています。


4.3。 深く潜ることなくすぐに戻ってきます



Goは制御フローで例外を使用しないためtry



、andの最上位の構造を提供するためにコードを深く掘り下げる必要はありませんcatch



複数レベルの階層の代わりに、Goコードは関数の進行とともに画面を下っていきます。私の友人のマット・ライアーは、この練習を「視線」と呼んでいます



これは、境界演算子を使用して実現されます。関数への入力に前提条件がある条件付きブロック。パッケージの例を次に示しますbytes







 func (b *Buffer) UnreadRune() error { if b.lastRead <= opInvalid { return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune") } if b.off >= int(b.lastRead) { b.off -= int(b.lastRead) } b.lastRead = opInvalid return nil }
      
      





関数UnreadRune



入ると、状態がチェックされb.lastRead



、前の操作がでないReadRune



場合、エラーがすぐに返されます。関数の残りは、b.lastRead



より大きい値に基づいて機能しopInvalid



ます。



同じ関数と比較しますが、境界演算子はありません:



 func (b *Buffer) UnreadRune() error { if b.lastRead > opInvalid { if b.off >= int(b.lastRead) { b.off -= int(b.lastRead) } b.lastRead = opInvalid return nil } return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune") }
      
      





成功する可能性が高いブランチの本体は最初の条件に埋め込まれ終了ブラケットを慎重に一致させることによりif



、成功した終了の条件をreturn nil



検出する必要があります。関数の最後の行はエラーを返すようになったため、対応する開始ブラケットまで関数の実行を追跡して、このポイントに到達する方法を見つける必要がありますこのオプションは読みにくいため、プログラミングとコードサポートの品質が低下するため、Goは初期段階で境界演算子を使用してエラーを返すことを好みます。







4.4。 null値を有用にする



各変数宣言は、明示的な初期化子がないと仮定すると、ゼロ化されたメモリの内容に対応する値、つまりzeroで自動的に初期化されます。値のタイプは、数値タイプの場合-ゼロ、ポインタータイプの場合-nil、スライス、マップ、およびチャネルでも同じオプションのいずれかによって決定されます。



既知のデフォルト値を常に設定する機能は、プログラムのセキュリティと正確性にとって重要であり、Goプログラムをより簡単かつコンパクトにすることができます。これは、Goのプログラマーが「構造に有用なゼロ値を与える」と言うときに念頭に置いていることです。ミューテックスの内部状態を表す2つの整数フィールドを含む



sync.Mutex



考えます。これらのフィールドは、宣言で自動的にnull値を取りますsync.Mutex



この事実はコード内で考慮されるため、このタイプは明示的な初期化なしでの使用に適しています。



 type MyInt struct { mu sync.Mutex val int } func main() { var i MyInt // i.mu is usable without explicit initialisation. i.mu.Lock() i.val++ i.mu.Unlock() }
      
      





便利なnull値を持つ型の別の例はbytes.Buffer



です。明示的に初期化することなく、宣言して書き込みを開始できます。



 func main() { var b bytes.Buffer b.WriteString("Hello, world!\n") io.Copy(os.Stdout, &b) }
      
      





この構造のゼロ値は、len



両方cap



が等しいこと0



、およびy array



(バックアップスライス配列の値を持つメモリへのポインタ)を意味しますnil



つまり、明示的にカットする必要はなく、単純に宣言できます。



 func main() { // s := make([]string, 0) // s := []string{} var s []string s = append(s, "Hello") s = append(s, "world") fmt.Println(strings.Join(s, " ")) }
      
      





ご注意 var s []string



上部の2行のコメント行と似ていますが、同一ではありません。nilのスライス値と長さゼロのスライス値には違いがあります。次のコードはfalseを出力します。



 func main() { var s1 = []string{} var s2 []string fmt.Println(reflect.DeepEqual(s1, s2)) }
      
      





初期化されていないポインター変数の有用な、ただし予期しないプロパティ-nil pointers-は、nilである型のメソッドを呼び出す機能です。これを使用して、デフォルト値を簡単に提供できます。



 type Config struct { path string } func (c *Config) Path() string { if c == nil { return "/usr/home" } return c.path } func main() { var c1 *Config var c2 = &Config{ path: "/export", } fmt.Println(c1.Path(), c2.Path()) }
      
      





4.5。パッケージレベルの状態を避ける



弱く接続されたサポートしやすいプログラムを作成する鍵は、1つのパッケージを変更しても、最初のパッケージに直接依存しない別のパッケージに影響を与える可能性が低いことです。



Goで弱い接続を実現するには、2つの優れた方法があります。



  1. インターフェイスを使用して、関数またはメソッドに必要な動作を記述します。

  2. グローバル状態を避けてください。


Goでは、関数またはメソッドのスコープ内およびパッケージのスコープ内で変数を宣言できます。大文字の識別子を持つ変数が公開されている場合、そのスコープは実際にはプログラム全体でグローバルです。パッケージはいつでもこの変数のタイプと内容を参照します。



グローバル変数はプログラム内の各関数の目に見えないパラメーターになるため、可変グローバル状態はプログラムの独立した部分間の密接な関係を提供します!グローバル変数に依存する関数は、この変数の型が変更されると違反する可能性があります。グローバル変数の状態に依存する関数は、プログラムの別の部分がこの変数を変更すると違反する可能性があります。



グローバル変数が作成する接続を減らす方法:



  1. 対応する変数をフィールドとして、それらを必要とする構造に移動します。

  2. インターフェイスを使用して、動作とこの動作の実装との関係を減らします。


5.プロジェクトの構造



パッケージをプロジェクトに結合する方法について説明しましょう。これは通常、単一のGitリポジトリです。



パッケージと同様に、各プロジェクトには明確な目標が必要です。ライブラリの場合、XML解析やジャーナリングなど、1つのことを行う必要があります。1つのプロジェクトで複数の目標を組み合わせてはいけません。これは恐ろしいライブラリーを避けるのに役立ちますcommon







協議会私の経験では、リポジトリはcommon



最終的に最大の消費者と密接に関連しているためcommon



、ブロック段階で消費者と更新者の両方を更新せずに以前のバージョンの修正(バックポート修正)を行うことは難しく、多くの無関係な変更が発生し、途中で壊れますAPI


アプリケーション(Webアプリケーション、Kubernetesコントローラーなど)がある場合、プロジェクトには1つ以上のメインパッケージがある場合があります。たとえば、Kubernetesコントローラーにはcmd/contour



、Kubernetesクラスターに展開されたサーバーとして、およびデバッグクライアントとして機能するパッケージが1つあります。



5.1。 少ないパッケージ、しかし大きい



コードレビューで、他の言語からGoに切り替えたプログラマの典型的な間違いの1つに気付きました。彼らはパッケージを乱用する傾向があります。



Goでは、視認性の精巧なシステムを提供していません:言語は、Java(のように十分なアクセス修飾子ではありませんpublic



protected



private



および暗黙的default



)。C ++のフレンドリーなクラスの類似物はありません。



Goには、2つのアクセス修飾子しかありません。これらはパブリック識別子とプライベート識別子で、識別子の最初の文字(大文字/小文字)で示されます。識別子がパブリックの場合、その名前は大文字で始まり、他のGoパッケージはそれを参照できます。



ご注意 「エクスポートされた」または「エクスポートされていない」という言葉は、パブリックおよびプライベートの同義語として聞こえます。


アクセス制御機能が制限されている場合、過度に複雑なパッケージ階層を回避するためにどのような方法を使用できますか?



協議会を除くすべてのパッケージでcmd/



internal/



ソースコードが存在する必要があります。


私は、より大きな大きいパケットを好まない方が良いと繰り返し言ってきました。デフォルトの位置は、新しいパッケージを作成しないことです。これにより、公開される型が多すぎて、使用可能なAPIの範囲が広くなり、小さくなります。以下では、この論文をさらに詳しく検討します。



協議会Javaから来ましたか?



JavaまたはC#の世界から来た場合は、暗黙のルールを覚えておいてください。Javaパッケージは単一のソースファイルに相当します.go



Goパッケージは、Mavenモジュール全体または.NETアセンブリと同等です。


5.1.1。インポート手順を使用してファイルごとにコードをソートする



サービスごとにパッケージを整理する場合、パッケージ内のファイルに対して同じことを行う必要がありますか?1つのファイル.go



を複数に分割するタイミングを知る方法は行き過ぎて、ファイルのマージについて考える必要がある場合、どのようにわかりますか?



私が使用する推奨事項は次のとおりです。





. .


ご注意 Go . ( — Go). .


5.1.2.



このツールgo



は、testing



2つの場所でパッケージサポートします。パッケージhttp2



がある場合は、ファイルhttp2_test.go



記述してパッケージ宣言を使用できますhttp2



。これは、コードをコンパイルしhttp2_test.go



としてそれはパッケージの一部ですhttp2



。口語音声では、このようなテストは内部と呼ばれます。



このツールgo



は、test終わる特別なパッケージ宣言もサポートしていますhttp_test



これにより、テストファイルはコードと同じパッケージに存在できますが、そのようなテストがコンパイルされると、それらはパッケージのコードの一部ではなく、独自のパッケージに存在します。これにより、別のパッケージがコードを呼び出しているかのようにテストを作成できます。このようなテストは外部と呼ばれます。



ユニットユニットテストには内部テストを使用することをお勧めします。これにより、各機能またはメソッドを直接テストでき、外部テストの官僚主義を回避できます。



ただし、テスト関数()の例を外部テストファイルに配置する必要ありますExample



。これにより、godocで表示したときに、サンプルが適切なパッケージプレフィックスを受け取り、簡単にコピーできるようになります。



. , .



, , Go go



. , net/http



net



.



.go



, , .


5.1.3. , API



プロジェクトに複数のパッケージがある場合、パブリックAPI用ではなく、他のパッケージで使用することを目的としたエクスポート関数が見つかる場合があります。この状況では、ツールは、プロジェクトに対して開いているが他のユーザーには閉じられているコードを配置するために使用できるgo



特別なフォルダー名internal/



認識します



このようなパッケージを作成するには、名前の付いたinternal/



ディレクトリまたはそのサブディレクトリに配置します。チームgo



は、パスを含むパッケージのインポートをinternal



確認すると、ディレクトリまたはサブディレクトリ内の呼び出しパッケージの場所を確認しますinternal/







たとえば、パッケージ.../a/b/c/internal/d/e/f



はディレクトリツリーからパッケージのみをインポートできます.../a/b/c



、まったく.../a/b/g



または他のリポジトリはインポートできません(参照ドキュメント)。



5.2。 最小のメインパッケージ



関数main



とパッケージmain



main.main



、シングルトンのように機能するため、最小限の機能を備えている必要があります。プログラムはmain



、テストを含めて1つの関数のみを持つことができます。



これmain.main



はシングルトンであるため、呼び出されるオブジェクトには多くの制限があります。これらはmain.main



またはmain.init



、および一だけ呼び出されますこれにより、コードのテストの作成が困難になりmain.main



ます。したがって、可能な限り多くのロジックをメイン関数から、理想的にはメインパッケージから導出するよう努力する必要があります。



協議会func main()



フラグを分析し、データベース、ロガーなどへの接続を開き、実行を高レベルのオブジェクトに転送する必要があります。


6. API構造



プロジェクト設計に関する最後のアドバイスは、最も重要だと思います。



前の文はすべて、原則として拘束力はありません。これらは、個人的な経験に基づいた単なる推奨事項です。これらの推奨事項をコードレビューにあまり押し込みません。



ここでは、後方互換性を損なうことなく他のすべてを修正できるため、エラーはより深刻に扱われます:ほとんどの場合、これらは単なる実装の詳細です。



パブリックAPIに関しては、最初の段階から構造を真剣に検討する価値があります。後続の変更はユーザーにとって破壊的なものになるからです。



6.1。 設計上悪用しにくいAPIを設計する



«APIは、「適切な使用のための、シンプルで悪用することは困難であるべき - ジョシュ・ブロッホ


Josh Blochのアドバイスは、おそらくこの記事で最も価値があります。APIを単純なものに使用するのが難しい場合、すべてのAPI呼び出しは必要以上に複雑になります。API呼び出しが複雑で明白でない場合、見落とされる可能性があります。



6.1.1。同じタイプの複数のパラメーターを受け入れる関数には注意してください。



一見シンプルですが、APIの使用が難しい例として、同じタイプの2つ以上のパラメーターが必要な場合があります。2つの関数シグネチャを比較します。



 func Max(a, b int) int func CopyFile(to, from string) error
      
      





これら2つの機能の違いは何ですか?明らかに、1つは最大2つの数値を返し、もう1つはファイルをコピーします。しかし、それはポイントではありません。



 Max(8, 10) // 10 Max(10, 8) // 10
      
      





Maxは可換です。パラメーターの順序は重要ではありません。8と10、または10と8を比較するかどうかに関係なく、最大8と10は10です。



しかし、CopyFileの場合、これはそうではありません。



 CopyFile("/tmp/backup", "presentation.md") CopyFile("presentation.md", "/tmp/backup")
      
      





これらのオペレーターのうち、プレゼンテーションをバックアップするオペレーターと、先週のバージョンで上書きするオペレーターは誰ですか?ドキュメントを確認するまでわかりません。コードのレビュー中に、引数の順序が正しいかどうかは不明です。もう一度、ドキュメントを見てください。



1つの可能な解決策は正しい呼び出しに責任がある補助タイプを導入することですCopyFile







 type Source string func (src Source) CopyTo(dest string) error { return CopyFile(dest, string(src)) } func main() { var from Source = "presentation.md" from.CopyTo("/tmp/backup") }
      
      





ここではCopyFile



常に正しく呼び出されます-これは単体テストを使用して指定できます-プライベートにすることができ、これにより不正使用の可能性がさらに減少します。



協議会同じタイプの複数のパラメーターを持つAPIを正しく使用することは困難です。


6.2。 基本的なユースケース用のAPIを設計する



数年前に私が与えたプレゼンテーションを使用することに、機能オプションのデフォルトへの容易なAPIを作ること。



プレゼンテーションの本質は、主なユースケース用のAPIを開発する必要があるということでした。言い換えれば、APIは、ユーザーが興味のない追加のパラメーターを提供することをユーザーに要求するべきではありません。



6.2.1。パラメータとしてnilを使用することは推奨されません



まず、ユーザーに興味のないAPIパラメーターを提供するように強制するべきではないと言うことから始めました。これはまた、主なユースケース用のAPIを設計することも意味します(デフォルトオプション)。



net / httpパッケージの例を次に示します。



 package http // ListenAndServe listens on the TCP network address addr and then calls // Serve with handler to handle requests on incoming connections. // Accepted connections are configured to enable TCP keep-alives. // // The handler is typically nil, in which case the DefaultServeMux is used. // // ListenAndServe always returns a non-nil error. func ListenAndServe(addr string, handler Handler) error {
      
      





ListenAndServe



2つのパラメーターを受け入れます。着信接続をリッスンするためのTCPアドレスとhttp.Handler



、着信HTTPリクエストを処理するためのTCPアドレスですServe



2番目のパラメータをにすることができますnil



コメントでは、通常、呼び出し元のオブジェクト実際に渡され、暗黙的なパラメーターとしてnil



使用することhttp.DefaultServeMux



示していることに注意してください。



呼び出し元Serve



には、同じことを行う2つの方法があります。



 http.ListenAndServe("0.0.0.0:8080", nil) http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)
      
      





両方のオプションは同じことをします。



このアプリケーションnil



はウイルスのように広がりますパッケージにhttp



はヘルパーも含まれているhttp.Serve



ため、関数の構造を想像できますListenAndServe







 func ListenAndServe(addr string, handler Handler) error { l, err := net.Listen("tcp", addr) if err != nil { return err } defer l.Close() return Serve(l, handler) }
      
      





のでListenAndServe



、発信者が通過することを可能にするnil



二番目のパラメータのために、http.Serve



また、この動作をサポートしています。実際、http.Serve



「ハンドラが等しい場合はnil



、使用するDefaultServeMux



実装されているロジックにありますnil



1つのパラメーターを受け入れると、呼び出し側nil



は両方のパラメーターに渡すことができると考えるようになります。しかし、そのようなServe







 http.Serve(nil, nil)
      
      





恐ろしいパニックにつながります。



協議会同じ関数シグニチャパラメータに混在させないでくださいnil



とありませんnil





著者http.ListenAndServe



は、デフォルトの場合のAPIユーザーの生活を簡素化しようとしましたが、セキュリティに影響がありました。



存在する場合、nil



明示的使用と間接使用の間で行数に違いはありませんDefaultServeMux







  const root = http.Dir("/htdocs") http.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", nil)
      
      





と比較して



  const root = http.Dir("/htdocs") http.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)
      
      





1行を維持することは混乱の価値がありましたか?



  const root = http.Dir("/htdocs") mux := http.NewServeMux() mux.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", mux)
      
      





協議会ヘルパー関数がプログラマーをどれだけ節約するかを真剣に考えてください。明快さは簡潔さよりも優れています。


協議会テストのみが必要とするパラメーターを持つパブリックAPIは避けてください。テスト中にのみ値が異なるパラメーターを持つAPIをエクスポートしないでください。代わりに、そのようなパラメーターの転送を隠すラッパー関数をエクスポートし、テストでは、テストに必要な値を渡す同様の補助関数を使用します。


6.2.2。[] Tの代わりに可変長引数を使用します



多くの場合、関数またはメソッドは値のスライスを受け取ります。



 func ShutdownVMs(ids []string) error
      
      





これは単なる構成例ですが、これは非常に一般的です。問題は、これらの署名が複数のレコードで呼び出されることを前提としていることです。経験が示すように、それらはしばしば1つの引数のみで呼び出され、関数シグネチャの要件を満たすためにスライス内に「パック」する必要があります。



さらに、パラメーターids



はスライスであるため、空のスライスまたはゼロを関数に渡すことができ、コンパイラーは満足します。テストではこのようなケースをカバーする必要があるため、これによりテストの負担が増えます。



このようなAPIクラスの例を挙げるために、最近、少なくとも1つのパラメーターがゼロ以外の場合にいくつかの追加フィールドのインストールを必要とするロジックをリファクタリングしました。ロジックは次のようになりました。



 if svc.MaxConnections > 0 || svc.MaxPendingRequests > 0 || svc.MaxRequests > 0 || svc.MaxRetries > 0 { // apply the non zero parameters }
      
      





演算子がif



非常に長くなったため、検証ロジックを別の関数に引き込みたいと思いました。ここに私が思いついたものがあります:



 // anyPostive indicates if any value is greater than zero. func anyPositive(values ...int) bool { for _, v := range values { if v > 0 { return true } } return false }
      
      





これにより、室内ユニットが実行される条件を明確に述べることができました。



 if anyPositive(svc.MaxConnections, svc.MaxPendingRequests, svc.MaxRequests, svc.MaxRetries) { // apply the non zero parameters }
      
      





ただし、には問題がありanyPositive



、誰かが誤って次のように呼び出すことができます。



 if anyPositive() { ... }
      
      





その場合、anyPositive



を返しfalse



ます。これは最悪のオプションではありません。引数がない場合にanyPositive



返さtrue



れる場合さらに悪い



ただし、anyPositiveのシグネチャを変更して、呼び出し元に少なくとも1つの引数が渡されるようにする方がよいでしょう。これは、通常の引数と可変長引数(varargs)のパラメーターを組み合わせることで実行できます。



 // anyPostive indicates if any value is greater than zero. func anyPositive(first int, rest ...int) bool { if first > 0 { return true } for _, v := range rest { if v > 0 { return true } } return false }
      
      





これでanyPositive



、1つ未満の引数で呼び出すことはできません。



6.3。 関数に目的の動作を決定させます。



Document



ディスク上の構造を保存する関数を作成するタスクが与えられたとします



 // Save      f. func Save(f *os.File, doc *Document) error
      
      





ファイルにSave



書き込む関数を書くことができました。しかし、いくつかの問題があります。署名により、ネットワーク経由でデータを記録する可能性がなくなります。そのような要件が将来登場する場合、関数の署名を変更する必要があり、それはすべての呼び出しオブジェクトに影響します。また、ディスク上のファイルを直接操作するため、テストするのも面倒です。したがって、その動作を検証するために、テストは書き込み後にファイルの内容を読み取る必要があります。そして、それが一時フォルダーに書き込まれ、その後削除されることを確認する必要があります。また、たとえば、ディレクトリの読み取りや、パスがシンボリックリンクであるかどうかの確認など、に関連しない多くのメソッドを定義します。さて、署名の場合Document



*os.File







Save







Save







f







*os.File



Save



Save



関連する部分だけを説明しました*os.File







何ができますか?



 // Save      // ReadWriterCloser. func Save(rwc io.ReadWriteCloser, doc *Document) error
      
      





ヘルプを使用して、io.ReadWriteCloser



インターフェイスの分離の原則を適用しSave



、ファイルのより一般的なプロパティを説明するインターフェイスで再定義できます。



このような変更の後、インターフェイスを実装する型はすべてio.ReadWriteCloser



以前のに置き換えることができます*os.File







これにより、同時にスコープが拡張さSave



れ、どのタイプのメソッド*os.File



がその操作に関連するかが呼び出し側に明確になります。



そして、作者Save



はもはやこれらの無関係なメソッドを呼び出すことができません*os.File



なぜなら、彼はインターフェースの後ろに隠れているからio.ReadWriteCloser



です。



しかし、インターフェイス分離の原理をさらに拡張できます。



第一にSave



単一の責任の原則に従います。コンテンツをチェックするために彼が書いたばかりのファイルを読むことはありそうにありません-他のコードはこれを行うべきです。



 // Save      // WriteCloser. func Save(wc io.WriteCloser, doc *Document) error
      
      





したがって、インターフェイスの仕様を絞り込んで、Save



書き込みとクローズを行うことができます



第二に、スレッドを閉じるメカニズムy Save



は、ファイルを操作した当時の遺産です。問題は、どのような状況wc



で閉じられるかです。



かどうかはSave



、原因Close



無条件かどうか、成功の場合には



これは、ドキュメントが書き込まれた後にデータをストリームに追加したい場合があるため、呼び出し側に問題を提示します。



 // Save      // Writer. func Save(w io.Writer, doc *Document) error
      
      





最適なオプションはio.Writer



、保存のみを使用するようにSaveを再定義し、ストリームへのデータの書き込みを除き、他のすべての機能からオペレーターを保存することです。



インターフェイスの分離の原則を適用した後、機能は要件の点でより具体的(Save



書き込み可能なオブジェクトのみが必要)になり、機能の面ではより一般的になりましたio.Writer







7.エラー処理



私はいくつかのプレゼンテーションを行い、ブログでこのトピックについて多くのこと 書いたので、繰り返しません。



代わりに、エラー処理に関連する他の2つの領域について説明します。



7.1。 エラー自体を削除することにより、エラー処理の必要性を排除



エラー処理構文を改善するために多くの提案をしましたが、最良のオプションはそれらをまったく処理しないことです。



ご注意「エラー処理を削除する」とは言いません。処理にエラーがないようにコードを変更することをお勧めします。


John Osterhoutの最近のソフトウェア開発哲学の本は、私にこの提案をするよう促しましたチャプターの1つには「エラーを現実から排除する」というタイトルが付いています。このアドバイスを適用してみましょう。



7.1.1。 行数



ファイル内の行数をカウントする関数を作成します。



 func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err = br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, err } return lines, nil }
      
      





前のセクションのアドバイスに従って、CountLines



accepts io.Reader



でなく*os.File



;を受け入れます。io.Reader



カウントするコンテンツを提供するのは、呼び出し元のタスクです



を作成bufio.Reader



し、ループ内ReadString



メソッドを呼び出して、ファイルの最後に到達するまでカウンターを増やしてから、読み取った行数を返します。



少なくともこのようなコードを書きたいのですが、関数にはエラー処理が必要です。たとえば、次のような奇妙な構造があります。



  _, err = br.ReadString('\n') lines++ if err != nil { break }
      
      





エラーをチェックするに行数を増やします-これは奇妙に見えます。



このように記述する必要があるのReadString



は、改行文字より前にファイルの終わりが検出されるとエラーを返すためです。これは、ファイルの最後に新しい行がない場合に発生する可能性があります。



これを修正するには、行カウンターのロジックを変更し、ループを終了する必要があるかどうかを確認します。



ご注意 この論理はまだ完全ではありませんが、間違いを見つけることができますか?


ただし、エラーのチェックは完了していません。ファイルの終わりに達するReadString



と戻りio.EOF



ます。これは予想される状況です。そのため、ReadString



「停止、これ以上読むものはありません」と言う何らかの方法を実行する必要があります。したがって、呼び出し元のオブジェクトにエラーを返す前に、エラーがにCountLine



関連していないことを確認してからio.EOF



渡す必要があります。そうでない場合はnil



、すべてが正常であると返されます。



これは、エラー処理が関数を隠す方法に関するラスコックスの論文の良い例だと思います。改善されたバージョンを見てみましょう。



 func CountLines(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() }
      
      





この改善されたバージョンはbufio.Scanner



代わりにを使用しますbufio.Reader







内部でbufio.Scanner



はを使用しbufio.Reader



ますが、適切なレベルの抽象化を追加し、エラー処理の削除に役立ちます。



ご注意 bufio.Scanner



, .


スキャナが文字列を検出し、エラーを検出しなかった場合、メソッドsc.Scan()



は値を返しtrue



ます。したがって、ループボディはfor



、スキャナーバッファーにテキスト行がある場合にのみ呼び出されます。これCountLines



は、新しい行が存在しない場合やファイルが空の場合に新しいものがケースを処理することを意味します。



第二に、エラーが検出されたときsc.Scan



戻るためfalse



、サイクルfor



はファイルの終わりに達するかエラーが検出されると終了します。この型bufio.Scanner



は、最初に発生したエラーを記憶しており、メソッドsc.Err()



使用すると、ループを終了するとすぐにそのエラーを復元できます。



最後に、sc.Err()



処理を処理しio.EOF



nil



エラーなしでファイルの終わりに到達した場合に変換します。



協議会過度のエラー処理が発生した場合は、一部の操作をヘルパータイプに抽出してみてください。


7.1.2。 書き込み応答



2番目の例は、「Mistakes is Values」という投稿に触発されました



前に、ファイルを開く、書き込む、閉じる方法の例を見てきました。エラー処理はありますが、操作はヘルパーでカプセル化することができるので、それは、あまりない、などioutil.ReadFile



ioutil.WriteFile



ただし、低レベルのネットワークプロトコルを使用する場合は、I / Oプリミティブを使用して直接回答を作成する必要があります。この場合、エラー処理が邪魔になることがあります。HTTP応答を作成するHTTPサーバーのフラグメントを検討してください。



 type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprint(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err }
      
      





まず、ステータスバーを作成しfmt.Fprintf



、エラーを確認します。次に、各見出しに対して、エラーをチェックするたびにキーと見出し値を書き込みます。最後に、ヘッダーセクションを追加して\r\n



、エラーを確認し、応答本文をクライアントにコピーします。最後に、からのエラーをチェックする必要はありませんがio.Copy



、2つの戻り値からを返す唯一の値に変換する必要がありますWriteResponse







これは単調な作業です。しかし、小さなタイプのラッパーを適用することで、タスクを簡単にすることができますerrWriter







errWriter



は契約を満たしているio.Writer



ため、ラッパーとして使用できます。errWriter



エラーが検出されるまで、関数を介してレコードを渡します。この場合、エントリを拒否し、前のエラーを返します。



 type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{Writer: w} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprint(ew, "\r\n") io.Copy(ew, body) return ew.err }
      
      





に適用errWriter



するWriteResponse



と、コードの明瞭さが大幅に向上します。個々の操作ごとにエラーをチェックする必要はなくなりました。エラーメッセージは、フィールドチェックとして関数の最後に移動しew.err



、返されるio.Copy値の迷惑な変換を回避します。



7.2。 エラーを一度だけ処理する



最後に、エラーは1回だけ処理する必要があることに注意してください。処理とは、エラーの意味を確認し、単一の決定を下すことを意味します



 // WriteAll writes the contents of buf to the supplied writer. func WriteAll(w io.Writer, buf []byte) { w.Write(buf) }
      
      





1つ未満の決定を行う場合、エラーを無視します。ここにあるように、からのエラーはw.WriteAll



無視されます。



しかし、1つのミスに対応して複数の決定を下すことも間違っています。以下は、私がよく遭遇するコードです。



 func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println("unable to write:", err) // annotated error goes to log file return err // unannotated error returned to caller } return nil }
      
      





この例では、timeの間にエラーが発生するw.Write



と、行がログに書き込まれ、呼び出し元のオブジェクトに返されます。呼び出し元のオブジェクトは、おそらくログを記録してプログラムの最上位まで渡します。



ほとんどの場合、呼び出し元は同じことを行います。



 func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config: %v", err) return err } if err := WriteAll(w, buf); err != nil { log.Println("could not write config: %v", err) return err } return nil }
      
      





したがって、繰り返し行のスタックがログに作成されます。



 unable to write: io.EOF could not write config: io.EOF
      
      





しかし、プログラムの上部では、コンテキストなしで元のエラーが発生します。



 err := WriteConfig(f, &conf) fmt.Println(err) // io.EOF
      
      





このトピックをより詳細に分析したいのは、エラーを返すと同時に個人的な好みを記録するという問題を考慮していないためです。



 func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config: %v", err) // oops, forgot to return } if err := WriteAll(w, buf); err != nil { log.Println("could not write config: %v", err) return err } return nil }
      
      





プログラマーがエラーから戻ることを忘れるという問題にしばしば遭遇します。先に述べたように、Goのスタイルは境界演算子を使用し、関数の実行時に前提条件を確認し、早期に戻ることです。



この例では、作成者はエラーをチェックして登録しました、戻るのを忘れていました。このため、微妙な問題が発生します。



Goエラー処理コントラクトでは、エラーが存在する場合、他の戻り値の内容について仮定を立てることはできないとしています。 JSONマーシャリングが失敗したため、内容はbuf



不明です。何も含まれていない可能性がありますが、さらに悪いことに、半分書かれたJSONフラグメントが含まれている可能性があります。



プログラマはエラーをチェックして登録した後に戻るのを忘れたため、破損したバッファが転送されWriteAll



ます。操作は成功する可能性が高いため、構成ファイルは正しく書き込まれません。ただし、関数は正常に完了し、問題が発生したことを示す唯一の兆候は、構成レコードの障害ではなく、JSONマーシャリングが失敗したログの行です。



7.2.1。エラーへのコンテキストの追加



作成者がエラーメッセージにコンテキストを追加しようとしたため、エラーが発生しました。彼は、エラーの原因を示すマークを残そうとしました。



を介して同じことを行う別の方法を見てみましょうfmt.Errorf







 func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { return fmt.Errorf("could not marshal config: %v", err) } if err := WriteAll(w, buf); err != nil { return fmt.Errorf("could not write config: %v", err) } return nil } func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { return fmt.Errorf("write failed: %v", err) } return nil }
      
      





エラーレコードを1行で返すことと組み合わせると、戻ることを忘れて偶発的な継続を避けるのがより困難になります。



ファイルの書き込み中にI / Oエラーが発生した場合、メソッドError()



は次のようなものを生成します。



 could not write config: write failed: input/output error
      
      





7.2.2。github.com/pkg/errorsでのエラーラッピング



このパターンはエラーメッセージのfmt.Errorf



記録に適していますが、エラー種類はわき道にあります。疎結合プロジェクトでは、エラーを不透明な値として処理することが重要であるため、元のエラーの種類は、その値を処理するだけでよいかどうかは関係ないと主張しました。



  1. ゼロでないことを確認してください。

  2. 画面に表示するか、ログに記録します。


ただし、元のエラーを復元する必要がある場合があります。このようなエラーに注釈を付けるには、私のパッケージのようなものを使用できますerrors







 func ReadFile(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, errors.Wrap(err, "open failed") } defer f.Close() buf, err := ioutil.ReadAll(f) if err != nil { return nil, errors.Wrap(err, "read failed") } return buf, nil } func ReadConfig() ([]byte, error) { home := os.Getenv("HOME") config, err := ReadFile(filepath.Join(home, ".settings.xml")) return config, errors.WithMessage(err, "could not read config") } func main() { _, err := ReadConfig() if err != nil { fmt.Println(err) os.Exit(1) } }
      
      





これで、メッセージは素敵なK&Dスタイルのバグになります。



 could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
      
      





その値には元の理由へのリンクが含まれています。



 func main() { _, err := ReadConfig() if err != nil { fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err)) fmt.Printf("stack trace:\n%+v\n", err) os.Exit(1) } }
      
      





したがって、元のエラーを復元してスタックトレースを表示できます。



 original error: *os.PathError open /Users/dfc/.settings.xml: no such file or directory stack trace: open /Users/dfc/.settings.xml: no such file or directory open failed main.ReadFile /Users/dfc/devel/practical-go/src/errors/readfile2.go:16 main.ReadConfig /Users/dfc/devel/practical-go/src/errors/readfile2.go:29 main.main /Users/dfc/devel/practical-go/src/errors/readfile2.go:35 runtime.main /Users/dfc/go/src/runtime/proc.go:201 runtime.goexit /Users/dfc/go/src/runtime/asm_amd64.s:1333 could not read config
      
      





このパッケージをerrors



使用すると、人とマシンの両方にとって便利な形式でエラー値にコンテキストを追加できます。最近のプレゼンテーションで、Goの次のリリースで、そのようなラッパーが標準ライブラリに表示されることをお伝えしました。



8.並行性



Goは多くの場合、その同時実行機能のために選択されます。開発者はその効率(ハードウェアリソース)と生産性を向上させるために多くのことをしましたが、Goの並列処理機能を使用して、生産的でも信頼性のないコードを書くことができます。記事の最後で、Goの並行処理機能の落とし穴のいくつかを回避する方法に関するヒントをいくつか紹介します。



Goの最高レベルの同時実行性サポートは、チャネルと手順select



、およびgo



教科書や大学で囲theory理論を学んだ場合、並列処理セクションは常にコースの最後のセクションの1つであることにお気づきかもしれません。私たちの記事も同じです。Goプログラマーが習得すべき通常のスキルに追加するものとして、最後に並列処理について説明することにしました。



Goの主な特徴は単純で簡単な並列処理モデルであるため、ここには特定の二分法があります。製品として、私たちの言語は、このほぼ1つの機能を犠牲にしてそれ自体を販売しています。一方、並行性は実際にはそれほど使いやすいものではありません。さもなければ、著者は本の最後の章にせず、コードを後悔することはありませんでした。



このセクションでは、Go並行性関数の単純な使用の落とし穴のいくつかについて説明します。



8.1。常に何らかの仕事をしてください。



このプログラムの問題は何ですか?



 package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() for { } }
      
      





このプログラムは、私たちが意図したことを行います。単純なWebサーバーを提供します。同時に、CPU時間が無限ループに費やされます。for{}



最後の行でmain



はgorutin mainをブロックし、I / Oを実行せずに、ブロッキング、メッセージの送受信、またはシェダーとの何らかの接続を待機しません。



Goランタイムは通常シェデラーによって提供されるため、このプログラムはプロセッサー上で無意味にスピンし、アクティブなロック(ライブロック)になる可能性があります。



修正方法 1つのオプションがあります。



 package main import ( "fmt" "log" "net/http" "runtime" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() for { runtime.Gosched() } }
      
      





それは馬鹿げているように見えるかもしれませんが、これは現実の生活で私に出くわす一般的な解決策です。これは、根本的な問題の誤解の症状です。



Goの経験が少しある場合は、次のように書くことができます。



 package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() select {} }
      
      





空のステートメントはselect



永久にブロックされます。これは便利です。なぜなら、今はcallのためだけにプロセッサ全体をスピンしないからですruntime.GoSched()



ただし、原因ではなく症状のみを扱います。



もう1つの解決策を示したいと思いますが、それは既にあなたに起こっていることです。http.ListenAndServe



ゴルーチンで実行する代わりに、メインのゴルーチンの問題を残して、メインのゴルーチンで実行http.ListenAndServe



するだけです。



協議会関数を終了するmain.main



と、プログラムの実行中に実行される他のゴルーチンが何をするかに関係なく、Goプログラムは無条件に終了します。


 package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
      
      





したがって、これが私の最初のアドバイスです。ゴルーチンが別の結果を得るまで進歩できない場合、仕事を委任するよりも自分で行う方が簡単な場合がよくあります。



これにより、多くの場合、ゴルーチンからプロセス開始者に結果を戻すために必要な多くの状態追跡とチャネル操作が不要になります。



協議会多くのGoプログラマーは、特に最初はゴルーチンを乱用しています。人生の他のすべてと同様に、成功への鍵は節度です。


8.2。呼び出し元に並列処理を任せる



2つのAPIの違いは何ですか?



 // ListDirectory returns the contents of dir. func ListDirectory(dir string) ([]string, error)
      
      





 // ListDirectory returns a channel over which // directory entries will be published. When the list // of entries is exhausted, the channel will be closed. func ListDirectory(dir string) chan string
      
      





明らかな違いについて言及します。最初の例では、ディレクトリをスライスに読み込み、何か問題が発生した場合はスライス全体またはエラーを返します。これは同期的に発生し、呼び出し元ListDirectory



はすべてのディレクトリエントリが読み取られるまでブロックします。ディレクトリの大きさによっては、時間がかかり、場合によっては大量のメモリが必要になる場合があります。



2番目の例を考えてみましょう。古典的なGoプログラミングに少し似ていListDirectory



ますが、ここではディレクトリエントリが送信されるチャネルを返します。チャネルが閉じられると、これはカタログエントリがなくなったことを示しています。チャネルの充填はreturnの後に発生ListDirectory



するため、ゴルーチンがチャネルの充填を開始すると想定できます。



ご注意2番目のオプションでは、ゴルーチンを実際に使用する必要はありません。ブロックせずにすべてのディレクトリエントリを格納するのに十分なチャネルを選択し、入力して閉じてから、呼び出し元にチャネルを返すことができます。ただし、この場合、チャネル内のすべての結果をバッファリングするために大量のメモリを使用すると同じ問題が発生するため、これは考えられません。


ListDirectory



チャンネルバージョンには、さらに2つの問題があります。





どちらの場合でも、解決策はコールバックを使用することです。コールバックは、実行時に各ディレクトリエントリのコンテキストで呼び出される関数です。



 func ListDirectory(dir string, fn func(string))
      
      





当然のことながら、関数filepath.WalkDir



はそのように機能します。



協議会関数がゴルーチンを起動する場合、このルーチンを明示的に停止する方法を呼び出し元に提供する必要があります。多くの場合、呼び出し元で非同期実行モードを終了するのが最も簡単です。


8.3。いつ停止するかを知らずにゴルーチンを実行しない



前の例では、ゴルーチンは不必要に使用されていました。しかし、Goの主な強みの1つは、その一流の並行性機能です。実際、多くの場合、並行作業が非常に適切であり、ゴルーチンを使用する必要があります。



この単純なアプリケーションは、アプリケーショントラフィック用のポート8080とエンドポイントへのアクセス用のポート8001の2つの異なるポートでhttpトラフィックを処理します/debug/pprof







 package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) // debug http.ListenAndServe("0.0.0.0:8080", mux) // app traffic }
      
      





プログラムは複雑ではありませんが、実際のアプリケーションの基盤です。



現在の形式のアプリケーションには、成長するにつれて現れるいくつかの問題があるので、すぐにそれらのいくつかを見てみましょう。



 func serveApp() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) http.ListenAndServe("0.0.0.0:8080", mux) } func serveDebug() { http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) } func main() { go serveDebug() serveApp() }
      
      





ハンドラを破るserveApp



serveDebug



、別の関数に、我々はからそれらを分離していますmain.main



また、以前のアドバイスに従い、確認しましたserveApp



serveDebug



、呼び出し元の並列性を確保するためのタスクを残します。



しかし、そのようなプログラムのパフォーマンスにはいくつかの問題があります。終了serveApp



してからexitを実行するmain.main



と、プログラムは終了し、プロセスマネージャーによって再起動されます。



協議会Goの関数が呼び出しオブジェクトに並列性を残すように、アプリケーションは状態の監視を停止し、呼び出したプログラムを再起動する必要があります。アプリケーションを自分で再起動する責任を負わせないでください。このプロセスは、アプリケーションの外部から処理するのが最適です。


ただし、serveDebug



別のゴルーチン開始され、そのリリースの場合、ゴルーチンは終了しますが、プログラムの残りは継続します。開発者/debug



、ハンドラーが長時間動作しなくなったため、アプリケーションの統計情報を取得できないという事実を嫌います



私たちは停止した場合、必ずアプリケーションが閉じられるようにする必要があり任意のそれを提供gorutinaを。



 func serveApp() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil { log.Fatal(err) } } func serveDebug() { if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil { log.Fatal(err) } } func main() { go serveDebug() go serveApp() select {} }
      
      





serverApp



、彼らはserveDebug



からエラーチェックListenAndServe



し、必要に応じてそれらを呼び出しますlog.Fatal



両方のハンドラーはゴルーチンで動作するため、メインルーチンを作成しselect{}



ます。



このアプローチにはいくつかの問題があります。



  1. ListenAndServe



    エラー返された場合nil



    、呼び出しは行われずlog.Fatal



    、このポートのHTTPサービスはアプリケーションを停止せずに終了します。

  2. log.Fatal



    os.Exit



    プログラムを無条件に終了する呼び出し遅延呼び出しは機能せず、他のゴルーチンは閉鎖の通知を受けず、プログラムは単に停止します。これにより、これらの関数のテストを書くことが難しくなります。


協議会log.Fatal



関数main.main



またはでのみ使用しますinit





実際、ゴルーチンの作成者にエラーが発生した場合は、そのエラーを伝えて、彼女がプロセスを停止して完全に完了した理由を見つけられるようにします。



 func serveApp() error { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) return http.ListenAndServe("0.0.0.0:8080", mux) } func serveDebug() error { return http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) } func main() { done := make(chan error, 2) go func() { done <- serveDebug() }() go func() { done <- serveApp() }() for i := 0; i < cap(done); i++ { if err := <-done; err != nil { fmt.Println("error: %v", err) } } }
      
      





Goroutineの戻りステータスは、チャネルを介して取得できます。チャネルサイズは制御したいゴルーチンの数と等しいため、チャネルへの送信done



はブロックされません。ゴルーチンのシャットダウンがブロックされ、リークが発生するためです。



チャネルdone



を安全に閉じることができないため、for range



すべてのゴルーチンが報告されるまで、チャネルサイクルのイディオムを使用できません。代わりに、実行中のすべてのゴルーチンを1サイクルで実行します。これは、チャネルの容量に等しくなります。



これで、すべてのゴルーチンをきれいに終了し、発生したすべてのエラーを修正する方法ができました。最初のゴルーチンから他の全員に作業を完了するための信号を送信するだけです。



への訴えhttp.Server



完了についてなので、このロジックをヘルパー関数でラップしました。ヘルパーserve



は、アドレスとhttp.Handler



、同様http.ListenAndServe



stop



、メソッドの実行に使用するチャネル受け入れますShutdown







 func serve(addr string, handler http.Handler, stop <-chan struct{}) error { s := http.Server{ Addr: addr, Handler: handler, } go func() { <-stop // wait for stop signal s.Shutdown(context.Background()) }() return s.ListenAndServe() } func serveApp(stop <-chan struct{}) error { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) return serve("0.0.0.0:8080", mux, stop) } func serveDebug(stop <-chan struct{}) error { return serve("127.0.0.1:8001", http.DefaultServeMux, stop) } func main() { done := make(chan error, 2) stop := make(chan struct{}) go func() { done <- serveDebug(stop) }() go func() { done <- serveApp(stop) }() var stopped bool for i := 0; i < cap(done); i++ { if err := <-done; err != nil { fmt.Println("error: %v", err) } if !stopped { stopped = true close(stop) } } }
      
      





チャンネルの各値に対して、チャンネルdone



を閉じますstop



。これにより、このチャンネルの各ゴルチンが独自に閉じますhttp.Server



これにより、残りのgoroutinesがすべて返されListenAndServe



ます。実行中のgorutinがすべて停止main.main



すると、終了し、プロセスが完全に停止します。



協議会このようなロジックを自分で記述することは、繰り返しの作業であり、間違いのリスクです。ほとんどの作業を行うこのパッケージのようなものを見てください。



All Articles