Go Wiki: Goコードレビューコメント

このページでは、Goコードのレビュー中によく行われるコメントを収集しており、詳細な説明を簡略な表現で参照できるようにしています。これは一般的なスタイル問題の羅列であり、包括的なスタイルガイドではありません。

Effective Go の補足としてご覧ください。

テストに関する追加コメントは Go Test Comments にあります。

Googleはより詳細なGo Style Guideを公開しています。

このページを編集する前に、**軽微な変更であっても、変更について議論してください**。多くの人が意見を持っており、ここは編集戦争の場ではありません。

Gofmt

ほとんどの機械的なスタイル問題を自動的に修正するには、コードに対してgofmtを実行してください。Goのコードのほとんどはgofmtを使用しています。このドキュメントの残りの部分は、機械的ではないスタイルポイントを扱います。

別の選択肢として、gofmtのスーパーセットであり、必要に応じてインポート行を追加(および削除)するgoimportsを使用することもできます。

コメント文

https://go.dokyumento.jp/doc/effective_go#commentary を参照してください。宣言を説明するコメントは、少し冗長に思えても、完全な文であるべきです。このアプローチにより、godocドキュメントとして抽出されたときに適切にフォーマットされます。コメントは記述されるものの名前で始まり、ピリオドで終わるべきです。

// Request represents a request to run a command.
type Request struct { ...

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) { ...

など。

コンテキスト

context.Context型の値は、APIおよびプロセス境界を越えて、セキュリティ資格情報、トレース情報、期限、およびキャンセルシグナルを運びます。Goプログラムは、受信RPCおよびHTTPリクエストから送信リクエストまで、関数呼び出しチェーン全体でContextを明示的に渡します。

Contextを使用するほとんどの関数は、それを最初のパラメータとして受け入れるべきです。

func F(ctx context.Context, /* other arguments */) {}

リクエストに固有ではない関数はcontext.Background()を使用するかもしれませんが、必要ないと思われる場合でもContextを渡す傾向があります。デフォルトのケースはContextを渡すことです。代替が間違いであると確信できる明確な理由がある場合にのみ、直接context.Background()を使用してください。

構造体型にContextメンバーを追加しないでください。代わりに、Contextを渡す必要があるその型の各メソッドにctxパラメータを追加してください。唯一の例外は、標準ライブラリまたはサードパーティライブラリのインターフェースにシグネチャが一致する必要があるメソッドです。

カスタムContext型を作成したり、関数シグネチャでContext以外のインターフェースを使用したりしないでください。

渡す必要があるアプリケーションデータがある場合は、パラメータ、レシーバ、グローバル、または本当にそこにあるべき場合はContext値に入れてください。

Contextはイミュータブルであるため、同じ期限、キャンセルシグナル、資格情報、親トレースなどを共有する複数の呼び出しに同じctxを渡しても問題ありません。

コピー

予期せぬエイリアシングを避けるため、別のパッケージから構造体をコピーする際には注意が必要です。たとえば、bytes.Buffer型には`[]byte`スライスが含まれています。`Buffer`をコピーすると、コピー内のスライスが元の配列のエイリアスになる可能性があり、その後のメソッド呼び出しで予期せぬ効果が生じることがあります。

一般に、メソッドがポインタ型`*T`に関連付けられている場合、型`T`の値をコピーしないでください。

Crypto Rand

キー(使い捨てのものであっても)を生成するために、パッケージmath/randmath/rand/v2を使用しないでください。Time.Nanoseconds()でシードした場合、エントロピーのビットはごくわずかです。代わりに、crypto/rand.Readerを使用してください。テキストが必要な場合は、crypto/rand.Textを使用するか、代わりにランダムバイトをencoding/hexまたはencoding/base64でエンコードしてください。

import (
    "crypto/rand"
    "fmt"
)

func Key() string {
  return rand.Text()
}

空のスライスの宣言

空のスライスを宣言する場合、以下を推奨します。

var t []string

よりも

t := []string{}

前者はnilスライス値を宣言し、後者はnilではないが長さがゼロのスライスです。これらは機能的には同等で、`len`と`cap`はどちらもゼロですが、nilスライスが推奨されるスタイルです。

nilではないが長さがゼロのスライスが推奨される限定的な状況があることに注意してください。例えばJSONオブジェクトのエンコード時などです(`nil`スライスは`null`にエンコードされ、`[]string{}`はJSON配列`[]`にエンコードされます)。

インターフェースを設計する際には、nilスライスとnilではない、長さゼロのスライスを区別しないようにしてください。これは微妙なプログラミングエラーにつながる可能性があります。

Goでのnilに関する詳細な議論については、Francesc Campoy氏の講演Understanding Nilをご覧ください。

Docコメント

すべてのトップレベルの公開名にはドキュメントコメントが必要であり、自明でない非公開型または関数宣言にも同様に必要です。コメント規則の詳細については、https://go.dokyumento.jp/doc/effective_go#commentary を参照してください。

パニックしない

https://go.dokyumento.jp/doc/effective_go#errors を参照してください。通常のエラー処理にpanicを使用しないでください。errorと複数の戻り値を使用してください。

エラー文字列

エラー文字列は、通常他のコンテキストの後に印刷されるため、大文字で始まったり(固有名詞や頭字語で始まる場合を除く)句読点で終わったりすべきではありません。つまり、`fmt.Errorf("Something bad")`ではなく`fmt.Errorf("something bad")`を使用してください。これにより、`log.Printf("Reading %s: %v", filename, err)`がメッセージの途中で不要な大文字なしでフォーマットされます。これは、暗黙的に行指向であり、他のメッセージ内に結合されないロギングには適用されません。

新しいパッケージを追加する場合は、意図された使用例(実行可能なExample、または完全な呼び出しシーケンスを示す簡単なテスト)を含めてください。

テスト可能なExample()関数について詳しく読む。

ゴルーチンのライフタイム

ゴルーチンを生成するときは、いつ、またはそれが終了するかを明確にしてください。

ゴルーチンはチャネルの送信または受信でブロックすることでリークする可能性があります。ガベージコレクターは、ブロックされているチャネルが到達不能であってもゴルーチンを終了させません。

たとえゴルーチンがリークしなくても、不要になったときにそれらを飛行中に放置すると、他の微妙で診断が難しい問題を引き起こす可能性があります。クローズされたチャネルへの送信はパニックを引き起こします。「結果が不要になった後」にまだ使用中の入力を変更すると、データ競合につながる可能性があります。そして、ゴルーチンを任意に長時間飛行中に放置すると、予測不能なメモリ使用量につながる可能性があります。

ゴルーチンのライフタイムが明確になるように、並行コードを十分にシンプルに保つように努めてください。それが現実的でない場合は、ゴルーチンがいつ、なぜ終了するのかを文書化してください。

エラーを処理する

https://go.dokyumento.jp/doc/effective_go#errors を参照してください。アンダースコア変数 (_) を使用してエラーを破棄しないでください。関数がエラーを返す場合は、関数が成功したことを確認するためにチェックしてください。エラーを処理するか、返すか、本当に例外的な状況ではパニックしてください。

インポート

名前の衝突を避ける場合を除き、インポートの名前変更は避けてください。適切なパッケージ名は名前変更を必要としないはずです。衝突が発生した場合は、最もローカルな、またはプロジェクト固有のインポートの名前を変更することを優先してください。

インポートは、空白行で区切られたグループで編成されます。標準ライブラリパッケージは常に最初のグループに入ります。

package main

import (
    "fmt"
    "hash/adler32"
    "os"

    "github.com/foo/bar"
    "rsc.io/goversion/version"
)

goimports がこれを自動的に行います。

インポートブランク

副作用のみのためにインポートされるパッケージ(`import _ "pkg"`構文を使用)は、プログラムのメインパッケージ内、またはそれらを必要とするテスト内でのみインポートされるべきです。

インポートドット

インポート`.`形式は、循環依存のためにテストされるパッケージの一部にできないテストで役立ちます。

package foo_test

import (
    "bar/testutil" // also imports "foo"
    . "foo"
)

この場合、テストファイルはbar/testutilを使用しているため、パッケージfoo内に存在できません。bar/testutilはfooをインポートしています。そのため、`import .`形式を使用して、ファイルがfooパッケージの一部であるかのように見せかけます。この1つのケースを除いて、プログラムで`import .`を使用しないでください。Quuxのような名前が現在のパッケージのトップレベル識別子なのか、インポートされたパッケージの識別子なのかが不明確になり、プログラムの読み取りが非常に困難になります。

帯域内エラー

Cや類似の言語では、関数がエラーや結果の欠落を示すために-1やnullのような値を返すのが一般的です。

// Lookup returns the value for key or "" if there is no mapping for key.
func Lookup(key string) string

// Failing to check for an in-band error value can lead to bugs:
Parse(Lookup(key))  // returns "parse failure for value" instead of "no value for key"

Goの複数の戻り値のサポートは、より良い解決策を提供します。クライアントに帯域内エラー値をチェックさせる代わりに、関数は他の戻り値が有効かどうかを示す追加の値を返す必要があります。この戻り値はエラーであるか、説明が不要な場合はブール値である場合があります。これは最後の戻り値であるべきです。

// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)

これにより、呼び出し元が結果を誤って使用するのを防ぎます。

Parse(Lookup(key))  // compile-time error

そして、より堅牢で読みやすいコードを奨励します。

value, ok := Lookup(key)
if !ok {
    return fmt.Errorf("no value for %q", key)
}
return Parse(value)

このルールはエクスポートされた関数に適用されますが、エクスポートされていない関数にも役立ちます。

nil、""、0、-1のような戻り値は、それが関数の有効な結果である場合、つまり呼び出し元が他の値と区別して処理する必要がない場合に適切です。

"strings"パッケージの関数など、一部の標準ライブラリ関数は帯域内エラー値を返します。これは、プログラマにより多くの注意を要求する代わりに、文字列操作コードを大幅に簡素化します。一般的に、Goコードはエラーに対して追加の値を返す必要があります。

エラーフローをインデントする

通常のコードパスを最小限のインデントに保ち、エラー処理を最初に処理してインデントするようにしてください。これにより、通常のパスを視覚的に素早くスキャンできるため、コードの読みやすさが向上します。たとえば、次のように書かないでください。

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
}

これは、短い変数宣言を自身の行に移動する必要があるかもしれません。

x, err := f()
if err != nil {
    // error handling
    return
}
// use x

頭字語

名前の中の頭字語や略語(例:「URL」や「NATO」)は、一貫した大文字・小文字表記を使用します。例えば、「URL」は「URL」または「url」(「urlPony」や「URLPony」のように)と表記され、「Url」とは決して表記されません。例として:ServeHTTPではなくServeHttp。複数の頭字語の「単語」を持つ識別子の場合、「xmlHTTPRequest」または「XMLHTTPRequest」のように使用します。

このルールは、「identifier」(ほとんどの場合、「ego」、「superego」のような「id」ではない)の略である「ID」にも適用されます。したがって、「appId」ではなく「appID」と記述します。

プロトコルバッファコンパイラによって生成されたコードはこのルールの対象外です。人間が書いたコードは、機械が書いたコードよりも高い水準が求められます。

インターフェース

Goのインターフェースは一般に、インターフェース型の値を使用するパッケージに属し、それらの値を実装するパッケージには属しません。実装パッケージは具体的な(通常はポインタまたは構造体)型を返す必要があります。そうすることで、広範なリファクタリングを必要とせずに、新しいメソッドを実装に追加できます。

「モック」のためにAPIの実装側でインターフェースを定義しないでください。代わりに、実際のAPIの公開APIを使用してテストできるようにAPIを設計してください。

インターフェースを使用する前に定義しないでください。実際の使用例がなければ、インターフェースが必要かどうか、ましてやどのようなメソッドを含むべきかを見極めるのは難しすぎます。

package consumer  // consumer.go

type Thinger interface { Thing() bool }

func Foo(t Thinger) string { … }
package consumer // consumer_test.go

type fakeThinger struct{ … }
func (t fakeThinger) Thing() bool { … }
…
if Foo(fakeThinger{…}) == "x" { … }
// DO NOT DO IT!!!
package producer

type Thinger interface { Thing() bool }

type defaultThinger struct{ … }
func (t defaultThinger) Thing() bool { … }

func NewThinger() Thinger { return defaultThinger{ … } }

代わりに具象型を返し、コンシューマがプロデューサの実装をモックできるようにする。

package producer

type Thinger struct{ … }
func (t Thinger) Thing() bool { … }

func NewThinger() Thinger { return Thinger{ … } }

行の長さ

Goコードに厳密な行長制限はありませんが、不快に長い行は避けてください。同様に、反復的であるなど、長い方が読みやすい場合は、行を短くするために改行を追加しないでください。

たいていの場合、人々が「不自然に」(関数呼び出しや関数宣言の途中などで、多少の例外はあるものの)行を折り返すのは、妥当な数のパラメータと妥当に短い変数名を使用していれば、折り返しは不要だったでしょう。長い行は長い名前と共に行われるようで、長い名前をなくすことが大いに役立ちます。

言い換えれば、行の長さのせいで改行するのではなく、書いているものの意味論のために改行しなさい(一般的なルールとして)。もしそれが長すぎる行を生み出すと感じるなら、名前や意味論を変更すれば、おそらく良い結果が得られるでしょう。

これは、実際には関数の長さに関するアドバイスとまったく同じです。「N行を超える関数は絶対に持たない」というルールはありませんが、長すぎる関数や、小さすぎて反復的すぎる関数というものは確かに存在し、その解決策は関数の境界を変更することであり、行数を数え始めることではありません。

Mixed Caps

https://go.dokyumento.jp/doc/effective_go#mixed-caps を参照してください。これは、他の言語の慣例を破る場合でも適用されます。たとえば、エクスポートされていない定数は`maxLength`であり、`MaxLength`や`MAX_LENGTH`ではありません。

Initialismsも参照してください。

名前付き結果パラメータ

godocでどのように表示されるかを考慮してください。以下のような名前付き結果パラメータは

func (n *Node) Parent1() (node *Node) {}
func (n *Node) Parent2() (node *Node, err error) {}

godocでは繰り返しになるので、次のようにする方が良いでしょう。

func (n *Node) Parent1() *Node {}
func (n *Node) Parent2() (*Node, error) {}

一方、関数が同じ型の2つまたは3つのパラメータを返す場合、あるいは結果の意味がコンテキストから明確でない場合、一部のコンテキストでは名前を追加することが役立つかもしれません。関数内で変数を宣言するのを避けるためだけに結果パラメータに名前を付けないでください。それは、不必要なAPIの冗長性というコストを払って、わずかな実装の簡潔さを交換することになります。

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)

関数が数行しかない場合は、裸の戻り値は問題ありません。中規模の関数になったら、戻り値を明示的にしてください。推論:裸の戻り値を使用できるようにするためだけに、結果パラメータに名前を付ける価値はありません。ドキュメントの明確さは、関数内の1行または2行を節約するよりも常に重要です。

最後に、deferredクロージャで変更するために、結果パラメータに名前を付ける必要がある場合があります。それは常にOKです。

裸のreturn

引数なしの`return`文は、名前付きの戻り値を返します。これは「裸の」戻り値として知られています。

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

名前付き結果パラメータを参照してください。

パッケージコメント

パッケージコメントは、godocで表示されるすべてのコメントと同様に、空白行なしでパッケージ句に隣接して表示されなければなりません。

// Package math provides basic constants and mathematical functions.
package math
/*
Package template implements data-driven templates for generating textual
output such as HTML.
....
*/
package template

"package main"コメントの場合、バイナリ名の後に他のスタイルのコメントでも問題ありません(最初にくる場合は大文字にしてもよい)。例えば、`seedgen`ディレクトリにある`package main`の場合、次のように書くことができます。

// Binary seedgen ...
package main

または

// Command seedgen ...
package main

または

// Program seedgen ...
package main

または

// The seedgen command ...
package main

または

// The seedgen program ...
package main

または

// Seedgen ..
package main

これらは例であり、これらの妥当なバリアントも許容されます。

文を小文字で始めることは、パッケージコメントとして許容される選択肢ではないことに注意してください。これらは公開されており、文の最初の単語を大文字にすることを含め、適切な英語で記述されるべきです。バイナリ名が最初の単語である場合、コマンドライン呼び出しのスペルと厳密には一致しなくても、大文字にすることが必須です。

コメント規則の詳細については、https://go.dokyumento.jp/doc/effective_go#commentary を参照してください。

パッケージ名

パッケージ内の名前への参照はすべてパッケージ名を使用して行われるため、識別子からその名前を省略できます。例えば、chubbyパッケージ内にいる場合、クライアントが`chubby.ChubbyFile`と書くようなChubbyFile型は必要ありません。代わりに、型を`File`と命名し、クライアントは`chubby.File`と書くでしょう。util、common、misc、api、types、interfacesのような無意味なパッケージ名は避けてください。詳細については、https://go.dokyumento.jp/doc/effective_go#package-names および https://go.dokyumento.jp/blog/package-names を参照してください。

値を渡す

数バイトを節約するためだけに、関数引数としてポインタを渡さないでください。関数が引数`x`を全体を通して`*x`としてのみ参照する場合、その引数はポインタであるべきではありません。これの一般的な例には、文字列へのポインタ(`*string`)やインターフェース値へのポインタ(`*io.Reader`)を渡すことが含まれます。どちらの場合も、値自体は固定サイズであり、直接渡すことができます。このアドバイスは、大きな構造体や、成長する可能性のある小さな構造体には適用されません。

レシーバ名

メソッドのレシーバー名は、そのアイデンティティを反映したものであるべきです。多くの場合、その型の1文字または2文字の略語で十分です(「Client」の「c」や「cl」など)。「me」、「this」、「self」などの一般的な名前は使用しないでください。これらはオブジェクト指向言語に典型的な識別子で、メソッドに特別な意味を与えます。Goでは、メソッドのレシーバーは単なる別のパラメータであり、したがってそれに応じて命名されるべきです。その役割は明白であり、文書化の目的はないため、メソッド引数ほど記述的である必要はありません。その型を持つすべてのメソッドのほとんどすべての行に表示されるため、非常に短くすることができます。また、一貫性も重要です。あるメソッドでレシーバーを「c」と呼んだら、別のメソッドで「cl」と呼んではいけません。

レシーバ型

メソッドで値レシーバとポインタレシーバのどちらを使用するかを選択するのは、特にGo初心者にとって難しい場合があります。迷った場合はポインタを使用しますが、値レシーバが理にかなっている場合もあります。通常は効率上の理由で、例えば小さな不変の構造体や基本的な型の値などが該当します。いくつかの役立つガイドラインを以下に示します。

  • レシーバがマップ、関数、またはチャネルである場合、それらのポインタを使用しないでください。レシーバがスライスであり、メソッドがスライスを再スライスしたり再割り当てしたりしない場合、そのポインタを使用しないでください。
  • メソッドがレシーバを変更する必要がある場合、レシーバはポインタである必要があります。
  • レシーバが`sync.Mutex`または同様の同期フィールドを含む構造体である場合、コピーを避けるためにレシーバはポインタである必要があります。
  • レシーバが大きな構造体または配列である場合、ポインタレシーバの方が効率的です。どのくらいの大きさが「大きい」のでしょうか?そのすべての要素をメソッドへの引数として渡すのと同等だと仮定してください。それが大きすぎると感じるなら、レシーバとしても大きすぎます。
  • 関数またはメソッドが、同時に、またはこのメソッドから呼び出されたときに、レシーバを変更する可能性はありますか?値型は、メソッドが呼び出されたときにレシーバのコピーを作成するため、外部の更新はこのレシーバに適用されません。変更が元のレシーバで可視である必要がある場合、レシーバはポインタである必要があります。
  • レシーバが構造体、配列、またはスライスであり、その要素のいずれかが変更される可能性のある何かへのポインタである場合、読者への意図をより明確にするため、ポインタレシーバを推奨します。
  • レシーバが小さな配列または構造体で、本質的に値型(例えばtime.Time型のようなもの)であり、変更可能なフィールドやポインタがなく、またはintやstringのような単純な基本型である場合、値レシーバが理にかなっています。値レシーバは、生成されるガベージの量を減らすことができます。値が値メソッドに渡されると、ヒープに割り当てる代わりにスタック上のコピーを使用できます。(コンパイラはこの割り当てを避けるために賢く振る舞おうとしますが、常に成功するわけではありません。)プロファイリングをまず行わずにこの理由で値レシーバ型を選択しないでください。
  • レシーバ型を混在させないでください。利用可能なすべてのメソッドに対してポインタ型または構造体型のいずれかを選択してください。
  • 最後に、迷ったときはポインタレシーバを使用してください。

同期関数

結果を直接返すか、返す前にコールバックやチャネル操作を完了する同期関数を、非同期関数よりも優先してください。

同期関数は、ゴルーチンを呼び出し内に局所化し、ライフタイムの推論を容易にし、リークやデータ競合を回避します。また、テストも簡単です。呼び出し元は、ポーリングや同期の必要なく、入力を渡し、出力を確認できます。

呼び出し元がより多くの並行性が必要な場合、別のゴルーチンから関数を呼び出すことで簡単に追加できます。しかし、呼び出し元側で不要な並行性を削除するのは非常に困難であり、時には不可能です。

役立つテスト失敗

テストは、何が間違っていたのか、どのような入力で、実際に何が得られ、何が期待されたのかを伝える役立つメッセージとともに失敗する必要があります。多くのassertFooヘルパーを書きたくなるかもしれませんが、ヘルパーが役立つエラーメッセージを生成するようにしてください。失敗したテストをデバッグする人があなたではない、あなたのチームではないと仮定してください。典型的なGoのテストは次のように失敗します。

if got != tt.want {
    t.Errorf("Foo(%q) = %d; want %d", tt.in, got, tt.want) // or Fatalf, if test can't test anything more past this point
}

ここでの順序は actual != expected であり、メッセージもその順序を使用していることに注意してください。一部のテストフレームワークは、0 != x, "expected 0, got x" などと、これらを逆にして記述することを推奨していますが、Goはそうではありません。

入力が非常に多いと感じる場合は、テーブル駆動テストを作成することをお勧めします。

異なる入力でテストヘルパーを使用する際に、失敗したテストを曖昧にしないためのもう一つの一般的なテクニックは、各呼び出し元を異なるTestFoo関数でラップすることです。そうすることで、テストはその名前で失敗します。

func TestSingleValue(t *testing.T) { testHelper(t, []int{80}) }
func TestNoValues(t *testing.T)    { testHelper(t, []int{}) }

いずれにせよ、将来あなたのコードをデバッグする人に対して、役立つメッセージで失敗させる責任はあなたにあります。

変数名

Goの変数名は、長いよりも短い方が良いです。これは、スコープが限定されたローカル変数に特に当てはまります。`lineCount`よりも`c`を優先します。`sliceIndex`よりも`i`を優先します。

基本的なルール:名前が宣言から遠くで使用されるほど、名前はより記述的である必要があります。メソッドのレシーバーの場合、1文字または2文字で十分です。ループインデックスやリーダーなどの一般的な変数は1文字(`i`、`r`)で構いません。より珍しいものやグローバル変数は、より記述的な名前が必要です。

Google Goスタイルガイドのより詳細な議論も参照してください。


このコンテンツはGo Wikiの一部です。