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

このページでは、Goコードのレビュー中に寄せられる一般的なコメントをまとめています。これにより、詳細な説明を省略形で参照できます。これは、包括的なスタイルガイドではなく、一般的なスタイルの問題のリストです。

Effective Goの補足として、このページをご覧ください。

テストに関連する追加のコメントは、Goテストコメントにあります。

Googleは、より詳細なGoスタイルガイドを公開しています。

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

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リクエストから送信リクエストまで、関数呼び出しチェーン全体にコンテキストを明示的に渡します。

コンテキストを使用するほとんどの関数は、それを最初のパラメータとして受け入れる必要があります。

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

リクエスト固有でない関数はcontext.Background()を使用できますが、必要ないと思ってもコンテキストを渡すようにしてください。 デフォルトのケースはコンテキストを渡すことです。 context.Background()を直接使用するのは、代替案が間違いである正当な理由がある場合のみです。

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

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

渡すアプリケーションデータがある場合は、パラメータ、レシーバ、グローバル変数、または本当に属している場合はコンテキスト値に配置します。

コンテキストは不変であるため、同じデッドライン、キャンセルシグナル、資格情報、ペアレントトレースなどを共有する複数の呼び出しに同じctxを渡しても問題ありません。

コピー

予期しないエイリアシングを避けるため、別のパッケージから構造体をコピーする場合は注意してください。 たとえば、bytes.Buffer型には`[]byte`スライスが含まれています。 `Buffer`をコピーすると、コピー内のスライスが元の配列のエイリアスになる可能性があり、後続のメソッド呼び出しが予期しない影響を与える可能性があります。

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

暗号乱数

使い捨てのキーであっても、パッケージ`math / rand`を使用してキーを生成しないでください。 シードされていない場合、ジェネレータは完全に予測可能です。 `time.Nanoseconds()`でシードされた場合、エントロピーはわずかです。 代わりに、`crypto / rand`のReaderを使用し、テキストが必要な場合は、16進数またはbase64で出力します。

import (
    "crypto/rand"
    // "encoding/base64"
    // "encoding/hex"
    "fmt"
)

func Key() string {
    buf := make([]byte, 16)
    _, err := rand.Read(buf)
    if err != nil {
        panic(err)  // out of randomness, should never happen
    }
    return fmt.Sprintf("%x", buf)
    // or hex.EncodeToString(buf)
    // or base64.StdEncoding.EncodeToString(buf)
}

空のスライスの宣言

空のスライスを宣言する場合は、次のようにします。

var t []string

ではなく、

t := []string{}

を使用してください。 前者はnilスライス値を宣言しますが、後者はnil以外ですが長さはゼロです。 それらは機能的には同等です(`len`と`cap`はどちらもゼロです)が、nilスライスが推奨されるスタイルです。

JSONオブジェクトをエンコードする場合など、nil以外で長さがゼロのスライスが推奨される状況は限られています(`nil`スライスは`null`にエンコードされますが、`[] string {}`はJSON配列`[]`にエンコードされます)。

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

Goでのnilについての詳細は、Francesc Campoyの講演Understanding Nilをご覧ください。

ドキュメントコメント

すべてトップレベルのエクスポートされた名前には、ドキュメントコメントが必要です。また、重要でないエクスポートされていない型または関数宣言にもドキュメントコメントが必要です。 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()関数の詳細をご覧ください。

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

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

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

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

ゴルーチンのライフタイムが明らかになるほど、並行コードをシンプルに保つようにしてください。 それが不可能な場合は、ゴルーチンがいつ、なぜ終了するかを文書化してください。

エラー処理

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"`を使用)は、プログラムのメインパッケージ、またはそれらを必要とするテストでのみインポートする必要があります。

ドットインポート

import . 形式は、循環依存関係のためにテスト対象のパッケージの一部にすることができないテストで役立ちます

package foo_test

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

この場合、テストファイルはfooをインポートするbar / testutilを使用するため、パッケージfooに含めることができません。 したがって、ファイルは実際にはパッケージfooの一部ではありませんが、そうであるかのように見せかけるために「import。」形式を使用します。 この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」を使用します。

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

protocol buffer コンパイラによって生成されたコードはこのルールの対象外です。人間が書いたコードは、機械が書いたコードよりも高い基準が求められます。

インターフェース

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

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 行より長くしない」というルールはありませんが、長すぎる関数や、反復的な小さな関数といったものは間違いなく存在し、解決策は関数の境界を変更することであり、行数を数え始めることではありません。

大文字小文字の混在

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

頭字語 も参照してください。

名前付き戻り値パラメータ

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 つのパラメーターを返す場合、または結果の意味がコンテキストから明確でない場合は、名前を追加すると便利な場合があります。関数内で var を宣言しないようにするために、結果パラメーターに名前を付けないでください。これは、実装の簡潔さをわずかに向上させる一方で、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)

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

最後に、場合によっては、遅延クロージャで変更するために、結果パラメーターに名前を付ける必要があります。これは常に問題ありません。

裸のreturn

引数のない 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 は必要ありません。代わりに、クライアントが chubby.File と記述する型 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 では、メソッドのレシーバーは単なる別のパラメーターであるため、それに応じて名前を付ける必要があります。名前はメソッド引数ほど説明的である必要はありません。その役割は明白であり、文書化の目的を果たさないためです。型のほぼすべてのメソッドのほぼすべての行に表示されるため、非常に短くすることができます。慣れ親しんでいる場合は簡潔にすることができます。また、一貫性を保ってください。1 つのメソッドでレシーバーを「c」と呼ぶ場合は、別のメソッドで「cl」と呼んではいけません。

レシーバータイプ

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

同期関数

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

同期関数は、ゴルーチンを呼び出し内にローカライズし、ライフタイムについて推論し、リークやデータレースを回避しやすくします。また、テストも容易です。呼び出し側は、ポーリングや同期を必要とせずに、入力を渡して出力を確認できます。

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

役立つテストの失敗

テストは、何が間違っていたか、どのような入力だったか、実際に何が得られたか、何が期待されていたかを説明する有用なメッセージとともに失敗する必要があります。多くの 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 はそうではありません。

入力が多すぎると思われる場合は、テーブル駆動テスト を記述することをお勧めします。

異なる入力でテストヘルパーを使用する場合に、失敗したテストを明確にするもう 1 つの一般的な手法は、各呼び出し側を異なる 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 文字(ir)にすることができます。より珍しいものやグローバル変数には、より説明的な名前が必要です。

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


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