背景
同僚達とGoのドキュメントを読んでこうって取り組みを開始しました。
毎週決められた日時で担当者が発表…とはいえ私の理解・予習・復習の為にも、雑に和約っぽいもの(意訳)と個人の感想を書き連ねていきます。
内容の正しさは保証しないので、誤り見つけましたらご指摘ください。
ちょいちょい更新する予定です。
今回読むもの
本題
【Gofmt】
gofmt をコード上で実行すると、自動的に多くの機械的システムの問題を修正します。
ほとんど全てのGoコードがgofmtを使用しております。
このドキュメントの残りの部分は機械的でないスタイルを対象としおります。
gofmtの上位互換であるgoimportsを代わりに使用することもできます。
goimportsは必要に応じてimport行を追加・削除をすることができます。
つまり
VSCodeなりなんだりのGoをサポートしているエディタでgofmt、goimportsのモジュールを使用すれば、自動で修正・importしてくれるよってお話。便利なので忘れずにやらねば。
【Comment Sentences】
参考)https://golang.org/doc/effective_go.html#commentary.
(関数・定数の)宣言時のコメント文は、多少冗長に感じたとしても、完全な文章でなければいけません。
このアプローチによってgodocドキュメントに展開したときのフォーマットが良くなります。
コメントは宣言されているものの名前で始まり、ピリオドで終わる必要があります。
つまり
宣言に対するコメントは、名称で始めて文章にする。(ピリオドで終わらせる)
godoc -http=:8080 でgodocドキュメントを確認してみるか…
【Contexts】
context.Context型の値には、APIとプロセスの境界を跨いで、セキュリティ証明書・追跡情報・期限・キャンセルシグナルが含まれております。
Goプログラムは明示的に、RPCやHTTPリクエストからリクエストの発信まで、コールチェイン全体に沿ってContextsを渡します。
Contextを使用するほとんどの関数は、Contextを最初の引数として受け取らなくてはいけません。
リクエスト固有でない関数は、context.Background()を使用することができます。しかし必要ないと思っていても、Contextを渡すようにしましょう。
Contextは渡すのがデフォルトです。
Contextを渡すのが間違いであることが明確な時だけcontext.Background()を使うようにしましょう。
構造体にContextメンバを追加しないでください。
代わりにContextが必要な各メソッドにctxパラメータを追加してください。
署名が標準ライブラリやサードパーティ・ライブラリのインターフェイスに一致しなくてはいけないメソッドは例外です。
カスタムContext型を作成したり、関数シグネチャでContext以外のインターフェイスを使用したりしないでください。
アプリケーションデータを渡したい場合は、パラメータ、レシーバ、グローバル変数に入れてください。
もし実際に存在するならば、Contextの値として持たせてください。
Contextsは不変なので、同じctxを、同じ期限・キャンセルシグナル・資格情報・親トレースを持つ多数のcallに渡すことができます。
つまり
Contextは渡すように。メソッドの引数として渡すように。
渡されるメソッドは、引数の最初がctxになるように設計する。
【Copying】
予期せぬエイリアスを避ける為に、他のパッケージから構造体をコピーする時は気をつけてください。
例えば、bytes.Bufferの構造体は[]byteを含んでおります。
Bufferをコピーすると、コピーしたスライスは元の配列のエイリアスになってしまい、後続のメソッドで呼び出した際に驚くような結果になるかもしれません。
一般的に、T型のメソッドが*Tのポインタ型に関連づいている場合、値をコピーしてはいけません。
【Crypto Rand】
鍵を生成する時はmath/randパッケージを使用してはいけません。使い捨ての1回限りであってもです。
seedを持たないジェネレーターは完全に予測可能です。
time.Nanoseconds()のseedを持つものでも、ほんの少し予測ができます。
代わりに、crypto/randのリーダーを用いて、テキストが必要な場合は16進数かbase64を用いましょう。
つまり
鍵生成はcrypto/randを用いる
【Declaring Empty Slices】
空のスライスを宣言する時はt := []string{}ではなくvar t []stringを使いましょう。
var t []stringではnilのスライス、t := []string{}ではnilではない長さゼロのスライスになります。
どちらもlenもcapもゼロで、機能的には同じですが、nilスライスの方が望ましいです。
JSONオブジェクトにエンコードする際など、nilではない長さゼロのスライスが好まれることがあることも覚えておきましょう。
([]string{}は空配列にエンコードされますが、nilのスライスはnullにエンコードされます)
インターフェースを設計する際、nilのスライスとnilではない長さ0のスライスに区別を設けることは避けてください。
微妙なプログラミングのエラーを引き起こす恐れがあります。
Goにおけるnilのyより詳しい情報はこちらをみてください
つまり
空のスライスの宣言はvar t []stringを使う方が良い
nilと、nilでは無いが長さ0のスライスとを区別するようなインターフェースは設計しない
【Doc Comments】
トップレベルのエクスポートされる自明でない型や関数名称にはdocコメントを書いてください。
コメントの規約についての詳細はこちらを参照してください。
【Don't Panic】
こちらを参照してください。
標準のエラーハンドリングでpanicは使用せず、errorと複数の返り値を用いるようにしてください。
【Error Strings】
エラー文言は他の文脈に続いて出力される為、頭文字や固有名詞で始まらない限り、大文字を用いたり、句点で終わるべきではありません。
つまりlog.Printf("Reading %s: %v", filename, err)がメッセージの途中で不自然な大文字を使わずにフォーマットされるように、fmt.Errorf("Something bad")ではなくfmt.Errorf("something bad")でコーディングするようにしてください。
これは暗黙的なライン指向で他のメッセージ内に結合されないロギングには適用されません。
つまり
単体で出力されるようなログを除いたエラー文言では、大文字始まりや句読点終わりのメッセージは使用しない
【Examples】
新しくパッケージを追加する際、実行できる、または完全なコールシーケンスを示す簡単なテストといった、使用例を含めてください。
詳しくはこちら
【Goroutine Lifetimes】
goroutinesを使用する際は、ライフサイクルを明確にしてください。
goroutinesはチャンネルの送受信をブロックすることで「リーク」することがあります。
ブロックされたチャンネルが到達不可能であっても、ガベージコレクタはgoroutinesを終了させません。
goroutineがリークしていない時でさえ、必要ないにもかかわらず放置していると些細で診断が困難な問題を引き起こす恐れがあります。
閉じたチャンネルでの送信はpanicになります。
使用中の入力を「結果が不要になった後」に編集すると、データの競合を起こし得ます。
goroutineを放置していると予期せぬメモリ使用量になる恐れがあります。
並行コードはgoroutineのライフタイムが明らかになるように、シンプルに記載してください。
それが不可能な時は、いつ・どういった時にgoroutineが終了するのかをドキュメントに記しておいてください。
つまり
goroutineはライフタイムを明確にする
それが無理な時は、いつ・どんな時に終了するのかドキュメントにまとめておく
【Handle Errors】
こちらを参照ください。
errを_で無視しないでください。
関数がエラーを返す場合、必ずチェックして関数が成功していることを確認して下さい。
エラーハンドルとしては、errを返却して下さい。または完全に例外的な状況であればpanicを使用して下さい。
つまり
エラーチェックはマスト
【Imports】
名前の衝突を避けるために、importをリネームするのは避けて下さい。
良いパッケージ名はリネームの必要がありません。
import文は空行で区切られた塊で書いて下さい。
標準ライブラリはいつも最初の塊に記載して下さい。
goimportsを用いると自動で実装してくれます。
つまり
importはリネームしないように
goimportsを使ってimport文が秩序だった記載になるようにしよう
【Import Blank】
import _ "pkg"の構文を用いてのインポートは、プログラムのmainパッケージか、それらのテストのみにしましょう。
【Import Dot】
.インポートは循環依存性のあるテストの際に便利です。
package foo_test
import (
"bar/testutil" // also imports "foo"
. "foo"
)
このような場合、bar/testutilでfooパッケージが使用されているため、テストファイルはfooパッケージに含めることはできません。
そのためimport .を使用して、fooパッケージの一部であるかのように見せます。
このような場合を除いて、import .を使用するのはやめて下さい。
Quuxのような名称が現在のパッケージまたはインポートされたパッケージの最上位の識別子であるかどうかが判断できなくなり、プログラムの可読性が悪くなります。
つまり
import .はなるべく使用しない
使用するとしたらテストコードくらい
【In-Band Errors】
C言語や同様の言語では、関数が-1やnullなどの値を返すことで、エラーや返り値の異常を通知するのが一般的です。
Go言語では返り値を複数にすることで、より良い解決策を提供しております。
【Indent Error Flow】
通常のコードはインデントを最小にし、エラーハンドリングのインデントは最初に行うようにしてください。
これにより、通常のコードを素早く読むことができるため、コードの可読性が向上します。
例えば
if err != nil {
// error handling
} else {
// normal code
}
ではなく、以下のように書いてください
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
もし、ifの条件文中に初期化文があった場合は、以下のように書くのではなく、
if x, err := f(); err != nil {
// error handling
return
} else {
// use x
}
このように書いてください↓
xx, err := f()
if err != nil {
// error handling
return
}
// use x
つまり
エラーハンドリングはインデントを下げて行い、通常コードはインデントを一番浅くなるように書く
【Initialisms】
URLやNATOと言った頭字語(略語)は、一貫性を持っています。
例えば、URLの表記はurlやURLとし、Urlのように書かないようにしましょう。
例えを追加しますと、ServeHTTPと書いて、 ServeHttpと書かないようにしましょう。
複数の頭字語を持つ変数ではxmlHTTPRequestやXMLHTTPRequestと記載するようにしましょう。
このルールはIDにも適用されます。IDはidentifierの略です。
そのためappIdではなくappIDと書くようにしてください。
Protocol buffers コンパイラによって生成されたコードはこのルールから除外されます。
人の書いたコードは、機械によって書かれたコードよりも高い水準で保持されます。
つまり
頭字語を変数名やメソッド名などに使用するときは全て大文字または全て小文字にする
【Interfaces】
Goのインターフェースは一般的に、インターフェースの値ではなく、型の値を用いるパッケージに属します。
実装するパッケージは、具体的な(ポインターや構造体の)型を返却するべきです。
そうすることによって、大規模なリファクタ無しで新しいメソッドを追加することができます。
Mockのためのインターフェースを実装するべきではありません。
代わりに、実際に公開されているAPIを使用してテストできるように、APIを実装してください。
使用される前にインターフェースを定義してはいけません。
現実的な使用例無しに、インターフェースの必要性を見極めるのは困難で、どんなメソッドが含まれている必要があるのかを見極めるのが困難であることは言わずもがなです。
【Line Length】
Goコードには厳格な長さの規定はありませんが不快な長さは避けてください。
同様に、長い方が読みやすい際は、行を短くする為に改行を加えるような行為はやめてください。(ex.反復的な文章)
関数の呼び出しや関数の宣言の途中で多かれ少なかれ例外はあるものの、人々が不自然に改行を行っている時、適度なパラメータ数で適度な長さの変数名であれば、改行は必要ないでしょう。
長い行は長い名称が使用されることが多く、その名称を取り除くことが助けになるでしょう。
言い換えれば、基本的には、行の長さではなく書いている内容に基づいて改行を行ってください。
行が長すぎと感じたら、名称や意味を変更して、程よい長さにしてください。
これは実は、関数の長さに対するアドバイスと同じです。
「関数はN行以内」といったルールはありませんが、確かに関数が長すぎる・小さすぎるといった問題はあり、その解決策は行数を数えることではなく境界を変更することです。
つまり
Goでは1行の長さ、関数の行数に関して特に決まりはないけど、極端に長い・短いは避けましょう
意味が崩れないように長い変数名を使用しつつ、行が長くなってしまったら適宜変数名を変更する
【Mixed Caps】
参照: https://golang.org/doc/effective_go.html#mixed-caps
他の言語の規則に違反している場合でも、Goにおける命名では_は使用しないでMixedCapsまたはmixedCapsを採用して下さい。
例えば、exportされない定数ではmixedCaps(先頭小文字のキャメル式)を採用して下さい。
こっち(Initialisms)も見てね
つまり
Goで命名をする際には、基本的にはキャメル式で記載する。
公開されない場合は先頭を小文字にする。
【Named Result Parameters】
Named Result Parametersがgodocでどのように見えるか考えてみましょう。
func (n *Node) Parent1() (node *Node) {}
func (n *Node) Parent2() (node *Node, err error) {}
微妙…以下を使うほうが良いです。
func (n *Node) Parent1() *Node {}
func (n *Node) Parent2() (*Node, error) {}
一方で、複数の同じ型の返り値を返す関数、または文脈から返り値の意味が明確でない場合など、返り値に命名をしておくと役に立つ場合があるかもしれません。
関数内でvarを宣言しないようにするためだけに、返り値に名前を付けないでください。
func (f *Foo) Location() (float64, float64, error)
↑ よりも ↓ の方がわかりやすいです
// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)
named return valueは短い関数で使用しましょう。
長くなってきたら返り値を明示してください。
named return valueが利用できるからという理由だけで採用するのはいけません。
関数中の1・2行を短くすることよりも、明確なドキュメントの方がより重要です。
最後に、defer close を用いてパラメータを変更させたい場合、Naked Returnsが必要になります。
その場合は常に使用して良いです。
つまり
named returnは複数の同じ型の返り値がある関数や短い関数の時に使う
【Naked Returns】
↑ Named Result Parametersに同じ
【Package Comments】
パッケージコメントは、godocによって提示されているコメントと同様に、package節に隣接する形で空行なしで記載してください。
mainパッケージのコメントの場合、バイナリ名に続く形でコメントを記載してください。
パッケージのコメントは大文字始まりの一般的な英文で書いてください。
コメントの最初がバイナリ名であったとしても、大文字で始めてください。
詳しくはこちら https://golang.org/doc/effective_go.html#commentary
つまり
パッケージのコメントは密に
英文になるように記載する
【Package Names】
パッケージ名への参照はパッケージ名を指定して行われおり、識別子を省略することができます。
例えばchubbyパッケージでは、ChubbyFile型は必要ありません。その名称を使用してしまうとクライアントはchubby.ChubbyFileのように指定することになってしまいます。
File型を実装することによって、クライアントがchubby.Fileとして指定できるようになります。
util、common、misc、api、types、interfacesなどの意味のないパッケージ名は避けてください。
参照
https://golang.org/doc/effective_go.html#package-names
https://blog.golang.org/package-names
つまり
パッケージ名はクライアント側での見え方を考慮してつけよう