最初に、プログラミング言語のベストプラクティスの意味について同意する必要があります。 ここでは、Goテクニカルマネージャー、ラスコックスの言葉を思い出すことができます。
時間の要因や他のプログラマーを追加すると、ソフトウェアエンジニアリングがプログラミングに起こります。
したがって、ラスはプログラミングの概念とソフトウェアエンジニアリングを区別します。 前者の場合はプログラムを自分で作成し、後者の場合は他のプログラマーが時間をかけて作業する製品を作成します。 エンジニアが出入りします。 チームは成長または縮小します。 新機能が追加され、バグが修正されました。 これがソフトウェア開発の性質です。
内容
1.基本原則
私はあなたの中でGoの最初のユーザーの一人かもしれませんが、これは私の個人的な意見ではありません。 これらの基本原則は、Go自体の根底にあります。
- シンプルさ
- 読みやすさ
- 生産性
ご注意 「パフォーマンス」または「同時実行性」については言及していません。 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は、扱いにくいワンライナーやプログラムの最小行数に対して最適化されていません。 ディスク上のソースコードのサイズも、エディターでプログラムを入力するのに必要な時間も最適化しません。
「良い名前はいい冗談のようなものです。 説明する必要がある場合、それはもはや面白くありません。」 - デイブチェイニー
最大の明瞭さの鍵は、プログラムを識別するために選択する名前です。 良い名前にはどのような特質がありますか?
- 良い名前は簡潔です。 それは最短である必要はありませんが、過剰を含んでいません。 信号対雑音比が高い。
- 良い名前は説明的です。 内容ではなく、変数または定数の使用について説明しています。 適切な名前は、実装ではなく、関数の結果またはメソッドの動作を説明します。 内容ではなくパッケージの目的。 名前が識別するものをより正確に記述しているほど、優れています。
- 適切な名前は予測可能です。 1つの名前で、オブジェクトの使用方法を理解する必要があります。 名前は説明的なものにする必要がありますが、伝統に従うことも重要です。 これは、Goプログラマーが「イディオマティック」と言うときの意味です。
これらのプロパティのそれぞれをさらに詳しく検討してみましょう。
2.2。 IDの長さ
Goのスタイルは、短い変数名で批判されることがあります。 Rob Pikeが言ったように 、「Goプログラマーは正しい長さの識別子を必要としています 。」
Andrew Gerrandは、重要性を示すより長い識別子を提供しています。
「名前の宣言からオブジェクトの使用までの距離が長いほど、名前は長くなるはずです」 -Andrew Gerrand
したがって、いくつかの推奨事項を作成できます。
- 短い変数名は、宣言と最後の使用との距離が短い場合に適しています。
- 長い変数名は、それ自体を正当化する必要があります。 長くなればなるほど、より重要になります。 詳細なタイトルには、ページ上の重みに関連するシグナルがほとんど含まれていません。
- 変数名にタイプ名を含めないでください。
- 定数名は、値の使用方法ではなく、内部値を記述する必要があります。
- ループと分岐には1文字の変数を、パラメーターと戻り値には別々の単語を、パッケージレベルでは関数と宣言に複数の単語を使用します。
- メソッド、インターフェース、およびパッケージには単一の単語を優先します。
- パッケージ名は、呼び出し元が参照に使用する名前の一部であることに注意してください。
例を考えてみましょう。
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つの異なる方法があります。
var x int = 1
var x = 1
var x int; x = 1
var x = int(1)
x := 1
まだすべてを覚えていないに違いない。 Go開発者はおそらくこれを間違いと見なしますが、何も変更するには遅すぎます。 この選択で、均一なスタイルを確保する方法は?
私は、自分自身で可能な限り使用しようとする変数を宣言するスタイルを提案したいと思います。
- 初期化せずに変数を宣言するときは、
var
使用します 。
var players int // 0 var things []Thing // an empty slice of Things var thing Thing // empty Thing struct json.Unmarshall(reader, &thing)
var
は、この変数が指定された型のnull値として意図的に宣言されていることを示すヒントとして機能します。 これは、短い宣言構文ではなく、var
使用してパッケージレベルで変数を宣言するという要件と一致していますが、後でパッケージレベルの変数を使用しないでください。 - 初期化で宣言するときは、
:=
使用します 。 これにより、:=
の左側の変数:=
意図的に初期化されていることが読者に明確になります。
理由を説明するために、前の例を見てみましょうが、今回は各変数を特別に初期化します。
var players int = 0 var things []Thing = nil var thing *Thing = new(Thing) json.Unmarshall(reader, thing)
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
構文を使用します。
- 変数を宣言して明示的に初期化するときは、
:=
使用し:=
。
ヒント 複雑なものを明示的に指摘します。
var length uint32 = 0x80
ここで、特定の数値型を必要とするライブラリでlength
を使用できます。このオプションは、長さ型が短い宣言でよりもuint32として具体的に選択されることをより明確に示します。
length := uint32(0x80)
最初の例では、明示的に初期化したvar宣言を使用して意図的にルールを破ります。 標準からの逸脱により、読者は異常なことが起こっていることを理解できます。
2.6。 チームのために働く
ソフトウェア開発の本質は、読みやすくサポートされたコードを作成することだとすでに述べました。 あなたのキャリアのほとんどは、おそらく共同プロジェクトで働くでしょう。 この状況での私のアドバイスは、チームが採用したスタイルに従うことです。
ファイルの途中でスタイルを変更するのは面倒です。 個人の好みを損ねるものの、一貫性は重要です。 私の経験則では、コードが
gofmt
に適合する場合、問題は通常議論する価値はありません。
ヒント コードベース全体で名前を変更する場合は、これを他の変更と混ぜないでください。 誰かがgit bisectを使用している場合、彼は別の変更されたコードを見つけるために何千もの名前変更を歩き回るのを好みません。
3.コメント
より重要なポイントに移る前に、コメントするのに数分かかりたいと思います。
「良いコードには多くのコメントがあり、悪いコードには多くのコメントが必要です。」 -実用プログラマー、デイブ・トーマスとアンドリュー・ハント
コメントは、プログラムを読みやすくするために非常に重要です。 各コメントは、次の3つのうち1つだけを実行する必要があります。
- コードの機能を説明します。
- 彼がそれをどのように行うかを説明してください。
- 理由を説明してください 。
最初の形式は、公開キャラクターに関するコメントに最適です。
// 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つのパッケージの名前が同じであることが判明した場合、ほとんどの場合:
- パッケージ名が一般的すぎます。
- . , .
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つの優れた方法があります。
- インターフェイスを使用して、関数またはメソッドに必要な動作を記述します。
- グローバル状態を避けてください。
Goでは、関数またはメソッドのスコープ内およびパッケージのスコープ内で変数を宣言できます。大文字の識別子を持つ変数が公開されている場合、そのスコープは実際にはプログラム全体でグローバルです。パッケージはいつでもこの変数のタイプと内容を参照します。
グローバル変数はプログラム内の各関数の目に見えないパラメーターになるため、可変グローバル状態はプログラムの独立した部分間の密接な関係を提供します!グローバル変数に依存する関数は、この変数の型が変更されると違反する可能性があります。グローバル変数の状態に依存する関数は、プログラムの別の部分がこの変数を変更すると違反する可能性があります。
グローバル変数が作成する接続を減らす方法:
- 対応する変数をフィールドとして、それらを必要とする構造に移動します。
- インターフェイスを使用して、動作とこの動作の実装との関係を減らします。
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
を複数に分割するタイミングを知る方法は?行き過ぎて、ファイルのマージについて考える必要がある場合、どのようにわかりますか?
私が使用する推奨事項は次のとおりです。
- 1つのファイルで各パッケージを開始します
.go
。このファイルにディレクトリと同じ名前を付けます。たとえば、パッケージhttp
はhttp.go
ディレクトリ内のファイルにある必要がありますhttp
。
- パッケージが大きくなると、さまざまな機能をいくつかのファイルに分割できます。たとえば、ファイル
messages.go
にはタイプRequest
とResponse
、ファイルclient.go
タイプClient
、ファイルserver.go
タイプサーバーが含まれます。
- , . , .
- . ,
messages.go
HTTP- ,http.go
,client.go
server.go
— HTTP .
. .
ご注意 Go . ( — Go). .
5.1.2.
このツール
go
は、
testing
2つの場所でパッケージをサポートします。パッケージ
http2
がある場合は、ファイル
http2_test.go
を記述してパッケージ宣言を使用できます
http2
。これは、コードをコンパイルし
http2_test.go
、としてそれはパッケージの一部です
http2
。口語音声では、このようなテストは内部と呼ばれます。
このツール
go
は、testで終わる特別なパッケージ宣言もサポートしています
http_test
。これにより、テストファイルはコードと同じパッケージに存在できますが、そのようなテストがコンパイルされると、それらはパッケージのコードの一部ではなく、独自のパッケージに存在します。これにより、別のパッケージがコードを呼び出しているかのようにテストを作成できます。このようなテストは外部と呼ばれます。
ユニットユニットテストには内部テストを使用することをお勧めします。これにより、各機能またはメソッドを直接テストでき、外部テストの官僚主義を回避できます。
ただし、テスト関数()の例を外部テストファイルに配置する必要があります
Example
。これにより、godocで表示したときに、サンプルが適切なパッケージプレフィックスを受け取り、簡単にコピーできるようになります。
. , .
, , Gogo
. ,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
記録に適していますが、エラーの種類はわき道にあります。疎結合プロジェクトでは、エラーを不透明な値として処理することが重要であるため、元のエラーの種類は、その値を処理するだけでよいかどうかは関係ないと主張しました。
- ゼロでないことを確認してください。
- 画面に表示するか、ログに記録します。
ただし、元のエラーを復元する必要がある場合があります。このようなエラーに注釈を付けるには、私のパッケージのようなものを使用できます
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つの問題があります。
- 処理する要素がもうないことを示す信号として閉じたチャネルを使用する
ListDirectory
と、エラーが原因で要素の不完全なセットを呼び出し側に通知できません。呼び出し元には、空のディレクトリとエラーの違いを伝える方法がありません。どちらの場合も、チャネルはすぐに閉じられるようです。
- 呼び出し元は、チャネルが閉じられたときにチャネルからの読み取りを継続する必要があります。これは、チャネルを満たすゴルーチンが機能しなくなったことを理解する唯一の方法だからです。これは使用上の重大な制限
ListDirectory
です。発信者は、必要なデータをすべて受信したとしても、チャネルからの読み取りに時間を費やします。これはおそらく、中規模および大規模ディレクトリのメモリ使用量の点でより効率的ですが、この方法は元のスライスベースの方法よりも高速ではありません。
どちらの場合でも、解決策はコールバックを使用することです。コールバックは、実行時に各ディレクトリエントリのコンテキストで呼び出される関数です。
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{}
ます。
このアプローチにはいくつかの問題があります。
ListenAndServe
エラーで返された場合nil
、呼び出しは行われずlog.Fatal
、このポートのHTTPサービスはアプリケーションを停止せずに終了します。
-
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
すると、終了し、プロセスが完全に停止します。
協議会。このようなロジックを自分で記述することは、繰り返しの作業であり、間違いのリスクです。ほとんどの作業を行うこのパッケージのようなものを見てください。