コーディングスタイルは私達のコードを統治する規則です。 これらのスタイルは、gofmt がやってくれることから少しだけ発展したものです。
このガイドのゴールはUber社内でのGoのコードでやるべき、もしくはやるべからずを説明し、コードの複雑さを管理することです。 これらのルールはコードを管理しやすくし、かつエンジニアがGoの言語機能をより生産的に利用できるようにします。
このガイドは元々同僚がGoを使ってより開発しやすくするためにPrashant VaranasiとSimon Newtonによって作成されました。 長年にわたって多くのフィードバックを受けて修正されています。
このドキュメントはUber社内で使われる規則を文書化したものです。 多くは以下のリソースでも見ることができるような一般的なものです。
- Effective Go
- The Go common mistakes guide
全てのコードは golint
や go vet
を通してエラーが出ない状態にするべきです。
エディタに以下の設定を導入することを推奨しています。
- 保存するごとに
goimports
を実行する golint
とgo vet
を実行してエラーがないかチェックする
Goのエディタのサポートについては以下の資料を参考にしてください。 https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
インタフェースをポインタとして渡す必要はほぼありません。 インタフェースは値として渡すべきです。 ただインタフェースを実装している要素はポインタでも大丈夫です。
インタフェースには2種類あります。
- 型付けされた情報へのポインタ。これは type と考えることができます。
- データポインタ。格納されたデータがポインタならそのまま使えます。格納されたデータが値ならその値のポインタになります。
もしインタフェースのメソッドがそのインタフェースを満たした型のデータをいじりたいなら、インタフェースの裏側の型はポインタである必要があります。
コンパイル時にインタフェースが適切に実装されているかチェックしましょう。 これは以下のことを指します。
- 公開された型がAPIとして適切に要求されたインタフェースを実装しているか
- 公開されてるかどうかに関わらず、ある型の集合が同じインタフェースを実装しているか
- その他にインタフェースを実装しなければ利用できなくなるケース
Bad | Good |
---|---|
|
|
var _ http.Handler = (*Handler)(nil)
という式は *Handler
型が http.Handler
インタフェースを実装していなければ、コンパイルエラーになります。
代入式の右辺はゼロ値にするべきです。
ポインタ型やスライス、マップなどは nil
ですし、構造体ならその型の空の構造体にします。
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
レシーバーが値のメソッドはレシーバーがポインタでも呼び出すことができますが、逆はできません。
type S struct {
data string
}
func (s S) Read() string {
return s.data
}
func (s *S) Write(str string) {
s.data = str
}
sVals := map[int]S{1: {"A"}}
// You can only call Read using a value
sVals[1].Read()
// This will not compile:
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// You can call both Read and Write using a pointer
sPtrs[1].Read()
sPtrs[1].Write("test")
同じように、メソッドのレシーバーが値型でも、ポインタがインタフェースを満たしているとみなされます。
type F interface {
f()
}
type S1 struct{}
func (s S1) f() {}
type S2 struct{}
func (s *S2) f() {}
s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}
var i F
i = s1Val
i = s1Ptr
i = s2Ptr
// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
// i = s2Val
Effective Go の Pointers vs Valuesを見るとよいでしょう。
sync.Mutex
や sync.RWMutex
はゼロ値でも有効です。ポインタで扱う必要はありません。
Bad | Good |
---|---|
|
|
もし構造体のポインタを使う場合、mutexはポインタでないフィールドにする必要があります。 外部に公開されてない構造体なら、mutexを埋め込みで使うこともできます。
|
|
インターナルな型やmutexのインタフェースを実装している必要がある場合には埋め込みを使う | 公開されている型にはプライベートなフィールドを使う |
スライスやマップは内部でデータへのポインタが含まれています。なのでコピーする際には注意してください。
引数として受け取ってフィールドに保存したスライスは、他の箇所でデータが書き換わる可能性があることを覚えておいてください。
Bad | Good |
---|---|
|
|
同じように、公開せずに内部に保持しているスライスやマップが変更されることもあります。
Bad | Good |
---|---|
|
|
ファイルや mutex のロックなどをクリーンアップするために defer を使おう
Bad | Good |
---|---|
|
|
defer のオーバーヘッドは非常に小さいです。 関数の実行時間がナノ秒のオーダーである場合には避ける必要があります。 defer を使ってほんの少しの実行コストを払えば可読性がとてもあがります。 これはシンプルなメモリアクセス以上の計算が必要な大きなメソッドに特に当てはまります。
channel のサイズは普段は1もしくはバッファなしのものにするべきです。 デフォルトでは channel はバッファなしでサイズが0になっています。 それより大きいサイズにする場合はよく考える必要があります。 どのようにしてサイズを決定するのか、チャネルがいっぱいになり処理がブロックされたときにどのような挙動をするかよく考える必要があります。
Bad | Good |
---|---|
|
|
Go で enum を導入するときの標準的な方法は、型を定義して const
のグループを作り、初期値を iota
にすることです。
変数のデフォルト値はゼロ値です。なので通常はゼロ値ではない値から enum を始めるべきでしょう。
Bad | Good |
---|---|
|
|
ただゼロ値を使うことに意味があるケースもあります。 例えばゼロ値をデフォルトの挙動として扱いたい場合です。
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
時間を正しく扱うのは非常に困難です。 時間に対する誤解には次のようなものがあります。
- 1日は24時間である
- 1時間は60分である
- 1週間は7日である
- 1年は365日である
- などなど
例えば、1番について考えると、単純に24時間を足すだけでは正しくカレンダー上次の日になるとは限りません。
そのため、時間を扱う場合は常にtimeパッケージを使いましょう。 なぜならこのパッケージで前述の誤解を安全に処理することができるからです。
時刻を扱うときはtime.Time型を使いましょう。 また、時刻を比較したり、足し引きする際にもtime.Time型のメソッドを使いましょう
Bad | Good |
---|---|
|
|
期間を扱うときにはtime.Duration型を使いましょう。
Bad | Good |
---|---|
|
|
時刻に24時間を足す例に戻ります。
もしカレンダー上で次の日の同じ時刻にしたい場合は time.AddDate
メソッドを使います。
もしその時刻から正確に24時間後にしたい場合はtime.Add
メソッドを使います。
newDay := t.AddDate(0 /* years */, 0, /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)
できるなら外部システムとのやり取りにも time.Time
型や time.Duration
型を使うようにしましょう。
- Command-line flags:
flag
パッケージはtime.ParseDuration
を使うことでtime.Duration
型をサポートできます - JSON:
encoding/json
パッケージはUnmarshal
メソッドによってRFC 3339フォーマットの時刻をtime.Time
型にエンコーディングできます - SQL:
database/sql
パッケージではもしドライバーがサポートしていればDATETIME
やTIMESTAMP
型のカラムをtime.Time
型にすることができます - YAML:
gopkg.in/yaml.v2
パッケージはtime.ParseDuration
によってRFC 3339フォーマットの時刻をtime.Time
型にエンコーディングできます
もし time.Duration
が使えないなら、int
型や float64
型を使ってフィールド名に単位をもたせるようにしましょう。
次の表のようにします。
Bad | Good |
---|---|
|
|
もしこれらのインタラクションで time.Time
を使えない場合には、RFC 3339フォーマットの string
型を使うようにしましょう。
このフォーマットはtime.UnmarshalTextメソッドの中でも使われますし、time.Parse
や time.Format
関数でも time.RFC3339
と組み合わせて使えます。
エラーを定義する方法にはいくつかの種類があります。 ユースケースに合った最適なものを選ぶために以下のことを考慮しましょう。
- 呼び出し側は自身でエラーをハンドリングするためにエラーを検知する必要がありますか?
その場合、上位のエラー変数または自前の型を定義することで
errors.Is
とerrors.As
関数を利用できるようにサポートしなければなりません。 - エラーメッセージは静的な文字列ですか?またはコンテキストを持つ情報が必要な動的な文字列ですか?
前者ならば
errors.New
が利用できます。後者ならばfmt.Errorf
または自前のエラー型を利用しなければなりません。 - 下流のエラーを更に上流に返していますか?もしそうならばError Wrappingのセクションを参照してください。
エラーを検知? | エラーメッセージ | アドバイス |
---|---|---|
いいえ | 静的 | errors.New |
いいえ | 動的 | fmt.Errorf |
はい | 静的 | errors.New を使ったパッケージ変数(var で定義) |
はい | 動的 | 自前のエラー型 |
例えば、静的文字列のエラーならば errors.New
を利用しましょう。
呼び出し側がエラーを検知しハンドリングする必要がある場合は、そのエラーをパッケージ変数としerrors.Is
で検知できるようにしましょう。
No error matching | Error matching |
---|---|
|
|
動的文字列のエラーの場合、呼び出し側がエラー検知する必要がないならば fmt.Errorf
を使い、検知する必要があるならば自前のerror
インターフェースを実装する型を使いましょう。
No error matching | Error matching |
---|---|
|
|
自前のエラー型を公開する場合、それもパッケージの公開APIの一部になることに留意しましょう。
// package foo
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func IsNotFoundError(err error) bool {
_, ok := err.(errNotFound)
return ok
}
func Open(file string) error {
return errNotFound{file: file}
}
// package bar
if err := foo.Open("foo"); err != nil {
if foo.IsNotFoundError(err) {
// handle
} else {
panic("unknown error")
}
}
エラーを伝搬させるためには以下の3つの方法が主流です。
- 受けたエラーをそのまま返す。
fmt.Errorf
に%w
を付けてコンテキストを追加する。fmt.Errorf
に%v
を付けてコンテキストを追加する。
追加するコンテキストが無いならばエラーをそのまま返しましょう。これによりオリジナルのエラー型とメッセージが保たれます。これは下流のエラーメッセージにエラーがどこから来たか追うための十分な情報がある場合に適しています。
別の方法として、"connection refused"のような曖昧なエラーではなく、"call service foo: connection refused"のようなより有益なエラーを得られるように、可能な限りエラーメッセージにコンテキストを追加することもできます。
エラーにコンテキストを追加するにはfmt.Errorf
を使いましょう。このとき、呼び出し側がエラー元の原因を抽出し検知できるようにするべきかどうかに基づき%w
または%v
を選ぶことになります。
- 呼び出し側が原因のエラー元を把握する必要がある場合は
%w
を使いましょう。これはほとんどのラップされたエラーにとって良いデフォルトの振る舞いになりますが、呼び出し側がそれに依存しだすかもしないことを考慮しましょう。そのため、ラップされたエラーが既知の変数(var)か型(type)であるケースでは、関数の責務としてその振る舞いのコードドキュメント記載とテストをしましょう。 - 原因のエラー元をあえて曖昧にする場合
%v
を使いましょう。呼び出し側はエラー検知をすることができなくなりますが、将来必要なときに%w
を使うように変更できます。
返されたエラーにコンテキストを追加する場合、"failed to"のようなエラーがスタックに蓄積されるあたって明白な表現は避け、コンテキストを簡潔に保つようにしてください。
Bad | Good |
---|---|
|
|
|
|
しかし、エラーメッセージが他のシステムに送られる場合は"err"タグを付けたり"Failed"プレフィックスをつけたりすることでエラーメッセージであることを明確にする必要があります。
Don't just check errors, handle them gracefullyの記事も参照してください。
グローバル変数として使われるエラー値においてそれがパブリックかプライベートかによってErr
またはerr
のプレフィックスを付けましょう。
この助言はPrefix Unexported Globals with _の助言よりも優先されます。
var (
// 以下の2つのエラーはパブリックであるため
// このパッケージの利用者はこれらのエラーを
// errors.Isで検知することができる。
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// このエラーはパッケージのパブリックAPIに
// させたくないのでプライベートにしている。
// errors.Isでパッケージ内にてこのエラーを
// 使うことができる。
errNotFound = errors.New("not found")
)
カスタムエラー型の場合、Error
を末尾に付けるようにしましょう。
// 同様にこのエラーはパブリックであるため
// このパッケージの利用者はこれらのエラーを
// errors.Asで検知することができる。
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// このエラーはパッケージのパブリックAPIに
// させたくないのでプライベートにしている。
// errors.Asでパッケージ内にてこのエラーを
// 使うことができる。
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
呼び出し側が呼び出し先からエラーを受け取ったとき、エラーについてどれほど知っているかで様々な方法があります。
主にこれらですが、他にも色々あります。
- もし、呼び出し先が特定のエラーを定義しているなら、
errors.Is
やerrors.As
を使って処理を分岐させます。 - もし復帰可能なエラーなら、エラーをログに出力し、安全に処理を継続しましょう
- もしドメインで定義された条件を満たさないエラーなら、事前に定義したエラーを返しましょう
- エラーを返すときは、Wrap するか、一つ一つ忠実に返しましょう。
呼び出し元がどのようにエラーを扱うかに関わらず、通常はどのエラーも一度だけ処理するべきです。例えば、呼び出し元の更に呼び出し元も同様にエラーを処理するので、呼び出し元はエラーをログに記録してから返すべきではありません。
次の具体例を考えます。
Description | Code |
---|---|
悪い例: エラーをログに書き出してから返す。 より上位の呼び出し先も同様にエラーをログに出力する可能性があるので、アプリケーションログに多くのノイズが混ざりこのログの価値が薄まります。 |
|
良い例: エラーをラップして返す。 より上位の呼び出し側がエラーを処理します。 |
|
良い例: エラーをログに出力し、処理を継続する。 もしその処理が絶対必要でないなら、品質は下がりますが復旧して処理を続けることができます。 |
|
良い例: エラーマッチさせて処理を継続する。 もし呼び出し元がエラーを定義していて、そのエラーが復旧可能なら、エラーを確認して処理を継続しましょう。 その他のエラーだった場合はエラーをラップして返しましょう。 ラップされたエラーは上位の呼び出し元で処理させましょう。 |
|
型アサーションで1つの戻り値を受け取る場合、その型でなかったらパニックを起こします。 型アサーションではその型に変換できたかを示すbool値も同時に返ってくるので、それで事前にチェックしましょう。
Bad | Good |
---|---|
|
|
プロダクションで動くコードはパニックを避けなければいけません。 パニックは連鎖的障害の主な原因です。 もしエラーが起きた場合、関数はエラーを返して、呼び出し元がどのようにエラーをハンドリングするか決めさせる必要があります。
Bad | Good |
---|---|
|
|
panic
とrecover
はエラーハンドリングではありません。
プログラムはnil参照などの回復不可能な状況が発生したとき以外は出すべきではありません。
ただプログラムの初期化時は例外です。
プログラムが開始するときに異常が起きた場合にはpanicを起こしてもよいでしょう。
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
またテストでは、テストが失敗したことを示すためにはpanic
ではなくて t.Fatal
や t.FailNow
を使うようにしましょう。
Bad | Good |
---|---|
|
|
sync/atomicパッケージによるアトミック操作はint32
やint64
といった基本的な型を対象としているため、アトミックに操作すべき変数に対する読み出し・変更操作にアトミック操作を用いるということ(つまりsync/atomicパッケージの関数を使うこと自体)を容易に忘却させます。例ではint32
の変数に普通の読み出し操作を行ってしまっていますが、これはコンパイラの型チェック機構を素通ししてしまっているため潜在的に競合条件のあるコードをコンパイルできてしまっています。
go.uber.org/atomicは実際のデータの型を基底型として隠蔽することによりこれらのアトミック操作に対して型安全性を付与できます。これによって読み出し操作を行う方法はアトミックな操作に限定され、普通の読み出し操作はコンパイラの型チェックの機構によってコンパイル時にはじくことが可能となります。
またsync/atomic
パッケージに加えて便利なatomic.Bool
型も提供しています。
Bad | Good |
---|---|
|
|
グローバル変数を変更するのは避けましょう。 代わりに依存関係の注入を使って構造体に持たせるようにしましょう。 関数ポインタを他の値と同じように構造体にもたせます。
Bad | Good |
---|---|
|
|
|
|
型の埋め込みは実装の詳細を漏らし、型のブラッシュアップを阻害し、ドキュメントが曖昧になってしまいます。
共通の AbstractList
を使って様々なリスト型を実装すると仮定します。
この場合に AbstractList
を埋め込むことでリスト操作を実装するのはやめましょう。
代わりにメソッドを再度定義してその中で AbstractList
のメソッドを実装するようにしましょう。
type AbstractList struct {}
// Add adds an entity to the list.
func (l *AbstractList) Add(e Entity) {
// ...
}
// Remove removes an entity from the list.
func (l *AbstractList) Remove(e Entity) {
// ...
}
Bad | Good |
---|---|
|
|
Go では継承が無い代わりに埋め込みを使えます。 外部の型は暗黙的に埋め込まれた型のメソッドを実装しています。 これらのメソッドはデフォルトでは埋め込まれた型のインスタンスのメソッドになります。
構造体は埋め込んだ型と同じ名前のフィールドを作成します。 なので埋め込んだ型が公開されていたら、そのフィールドも公開されます。 後方互換性を保つために、外側の型は埋め込んだ型を保持する必要があります。
埋め込みが必要な場面は殆どありません。 多くは面倒なメソッドの移譲を書かずに済ませるために使われます。
構造体の代わりに AbstractList インタフェースを埋め込むこともできます。 これだと開発者に将来的な自由度をもたせることができます。 しかし、抽象的な実装に依存して実装の詳細が漏れるという問題は解決されません。
Bad | Good |
---|---|
|
|
構造体の埋め込みでもインタフェースの埋め込みでも、将来的な型の変更に制限がかかります。
- 埋め込まれたインタフェースにメソッドを追加することは破壊的変更になります
- 埋め込まれた構造体からメソッドを削除すると破壊的変更になります
- 埋め込まれた型を削除することは破壊的変更になります
- 埋め込まれた型を同じインタフェースを実装した別の型に差し替える場合も破壊的変更になります
埋め込みの代わりに同じメソッドを書くのは面倒ですが、その分実装の詳細を外側から隠すことができます。 実装の詳細をそのメソッドが持つことで内部での変更がしやすくなります。 実装がすぐに見えるので、Listの詳細をさらに見に行く必要がなくなります。
Go の言語仕様ではいくつかのビルトイン、または定義済み識別子があります。これらは Go のプログラム内で識別子として使うべきではありません。
状況にもよりますが、これらの識別子を再利用すると、元の識別子がレキシカルスコープ内で隠蔽されるか、元のコードを混乱させます。 コンパイラがエラーを出して気づく場合もありますが、最悪の場合は grep などでは発見困難な潜在的バグを起こす可能性があります。
Bad | Good |
---|---|
|
|
|
|
宣言済み識別子名をローカルの識別子に使ってもコンパイラはエラーを出さないことに注意してください。ですが go vet
などのツールはこれらのシャドウイングを正しく見つけることができます。
できるなら init()
を使うのは避けましょう。次のケースの場合は避けようがなかったり、推奨されます。
- 実行環境や呼び出し方に関係なく、決定的である場合
- 他の
init()
関数の実行順や副作用に影響されない場合。init()
の順序はよく知られていますが、コードが変更され、init()
関数間の関係によってはコードが脆弱になり、エラーが発生しやすくなります - グローバルな情報や、環境変数、ワーキングディレクトリ、プログラムの引数や入力にアクセスしたり、操作しない場合
- ファイルシステム、ネットワーク、システムコールの操作をしない場合
コードがこれらの要件を満たせない場合、 main()
関数か、プログラムのライフサイクルの一部でヘルパー関数として呼び出すか、main()
関数で直接呼び出す必要があります。
特にライブラリなど他のプログラムで使われることを想定したコードの場合は特に決定的であることに注意し、 "init magic" を引き起こさないように注意しましょう。
Bad | Good |
---|---|
|
|
|
|
これらを考慮すると、次のような状況では init()
が望ましかったり必要になる可能性があります。
- ただの代入では表現できない複雑な式
database/sql
の登録や、エンコーディングタイプの登録など、プラグイン的に使うフック- Google Cloud Functions などの決定的事前処理の最適化
Go のプログラムでは即時終了するために os.Exit
や log.Fatal*
を使います。panic()
を使うのは良い方法ではありません don't panicを読んでください。
os.Exit
や log.Fatal*
を読んでいいのは main()
関数だけです。他の関数ではエラーを返して失敗を通知しましょう。
Bad | Good |
---|---|
|
|
根拠: exit
する関数が複数あるプログラムにはいくつかの問題があります。
- 明確でない制御フロー: どの関数もプログラムを強制終了できるので、制御フローを推論するのが難しくなります。
- テストが難しくなる: 強制終了するプログラムはテストで呼び出されたときも終了します。これはその関数をテストするのも難しくなりますし、
go test
でテストされるはずだった他のテストがスキップされる危険もあります。 - 後処理のスキップ: プログラムが強制終了されたとき、
defer
で終了時に実行する予定だった関数がスキップされます。これは重要な後処理をスキップする危険があります。
可能なら、 os.Exit
か log.Fatal
を main()
関数で 一度だけ 呼ぶのが好ましいです。もしプログラムを停止する失敗のシナリオがいくつかある場合、そのロジックは別の関数にしてエラーを返しましょう。
こうすると、 main()
関数を短くすることができますし、重要なビジネスロジックを分離してテストしやすくすることができます。
Bad | Good |
---|---|
|
|
JSON や YAML あるいは他のタグを使ったフィールド名をサポートするフォーマットに変換する場合、関連するタグを使ってアノテーションをつけましょう。
Bad | Good |
---|---|
|
|
根拠: 構造体をシリアライズした形式は異なるシステムをつなぐ約束事です。 フィールド名を含む構造体のシリアライズした形式が変わってしまうと、この約束事が破れてしまいます。 タグを使ってフィールド名を指定するとこの約束事がより厳密になり、リファクタリングやフィールドのリネームで不意に壊れてしまうことを防ぐことができます。
ゴルーチンは軽量ですが、コストはかかります。少なくとも、スタックのメモリとスケジュールされたCPUを使います。 典型的な使い方をする限りコストは小さいですが、ライフタイムを考えずに大量に作り出すと大きなパフォーマンス問題を引き起こします。 ライフタイムが管理されてないゴルーチンは、ガベージコレクションの邪魔になったり、使用されなくなったリソースを保持し続けるなどの問題も引き起こす可能性があります。
なので、絶対に本番コードでゴルーチンをリークさせないようにしましょう。 go.uber.org/goleak でゴルーチンを使うところでリークさせてないかテストしましょう。
一般的に、全てのゴルーチンはどちらかの方法を持っている必要があります。
- 停止する時間が予測できる
- 停止すべきゴルーチンに通知する方法がある
どちらのケースでも、処理をブロックしてゴルーチンの終了を待つコードも無ければいけません。
例:
Bad | Good |
---|---|
|
|
ゴルーチンを止める方法は無い。プログラムが終了するまで残り続ける |
ゴルーチンは |
システムによって起動されたゴルーチンが与えられた場合、ゴルーチンの終了を待つ方法を用意する必要があります。次の2つがよく使われる方法です。
sync.WaitGroup
を使います。終了を待つべきゴルーチンが複数ある場合、こちらを使う
var wg sync.WaitGroup
for i := 0; i < N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ...
}()
}
// 全ての終了を待つ
wg.Wait()
- 別の
chan struct{}
を作り、ゴルーチンが終了したときclose
します。待つべきゴルーチンが1つだけのときはこちらを使いましょう
done := make(chan struct{})
go func() {
defer close(done)
// ...
}()
// ここでゴルーチンの終了を待つ
<-done
init()
関数ではゴルーチンを起動するのを避けましょう。 Avoid init も参照してください。
もしパッケージがバックグラウンドで動くゴルーチンを作る必要があるなら、ゴルーチンのライフタイムを管理するオブジェクトを作りそれを公開しましょう。
そのオブジェクトは Close
、Stop
、Shutdown
など、バックグラウンドのゴルーチンを停止し、その終了を待つメソッドを提供しましょう。
Bad | Good |
---|---|
|
|
ユーザーがこのパッケージを公開すると、ゴルーチンを無条件に作成し、止める方法はありません。 |
ユーザーがリクエストすると、ワーカーを起動します。 Wait for goroutines to exit でも話したように、ワーカーが複数ゴルーチンを使う場合は |
パフォーマンスガイドラインは特によく実行される箇所にのみ適用されます。
数字と文字列を単に変換する場合、fmt
パッケージよりも strconv
パッケージのほうが高速に実行されます。
Bad | Good |
---|---|
|
|
|
|
固定の文字列からバイト列を何度も生成するのは避けましょう。 代わりに変数に格納してそれを使うようにしましょう。
Bad | Good |
---|---|
|
|
|
|
スライスやmapの容量のヒントが事前にある場合は初期化時に次のように設定しましょう。
make(map[T1]T2, hint)
make()
の引数にキャパシティを渡すと、初期化時に適切なサイズにしようとします。
なので要素をマップに追加する際にアロケーションの回数を減らすことができます。
ただし、キャパシティのヒントは必ずしも保証されるものではありません。
もし事前にキャパシティを渡していても、要素の追加時にアロケーションが発生する場合もあります。
Bad | Good |
---|---|
|
|
|
|
横にスクロールしたり、たくさん首をふるような長過ぎるコードは避けましょう。
横幅は99文字を推奨しています。書く側はこれを超えると改行したほうが良いですが、絶対ではありません。 コードが超えても問題ありません。
このガイドラインの一部は客観的に評価することができます。 ですが状況や文脈に依存する主観的なものもあります。
ですが何よりも重要なことは一貫性を保つことです。
一貫性のあるコードは保守しやすく、説明しやすく、読むときのオーバーヘッドも減らせます。 更に新しい規則やバグへの修正が非常に簡単になります。
逆に、1つのコードベース内に複数の異なったりバッティングしているスタイルがあると、メンテナンスのオーバーヘッドや、不確実なコード、認知的不協和が発生します。 これらの全てが開発速度の低下、苦痛なコードレビューを誘発し、更にバグを発生させます。
このガイドラインにある項目を自分たちのコードに適用する場合、パッケージもしくは更に大きな単位で適用することを勧めます。 サブパッケージレベルで適用することは同じコードベースに複数のスタイルを当てはめることになるため、先程述べた悪いパターンに当てはまっています。
Go は似たような宣言をグループにまとめることができます。
Bad | Good |
---|---|
|
|
これはパッケージ定数やパッケージ変数、型定義などにも利用できます。
Bad | Good |
---|---|
|
|
関係が近いものだけをグループ化しましょう。 関係ないものまでグループ化するのは避けましょう。
Bad | Good |
---|---|
|
|
グループ化を使う場所に制限は無いので、関数内でも使うことができます。
Bad | Good |
---|---|
|
|
import のグループは次の2つに分けるべきです。
- 標準パッケージ
- それ以外
goimports がデフォルトで適用してくれます。
Bad | Good |
---|---|
|
|
パッケージ名をつける場合、以下のルールに従いましょう。
- 全て小文字で大文字やアンダースコアを使わない
- ほとんどの呼び出し側が名前付きインポートをする必要がないようにする
- 短く簡潔にすること。全ての呼び出し側で識別されることを意識してください
- 複数形にしないこと。
net/urls
ではなくnet/url
です - "common"、"util"、"shared"、"lib"などを使わないこと。これらはなんの情報もない名前です。
Package NamesやStyle guideline for Go packagesも参考にしてください。
関数名にはGoコミュニティの規則であるMixedCapsに従います。
例外はテスト関数です。
TestMyFunction_WhatIsBeingTested
のようにテストの目的を示すためにアンダースコアを使って分割します。
インポートエイリアスはパッケージ名とパッケージパスの末尾が一致していない場合に利用します。
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
他にもインポートするパッケージの名前がバッティングした場合には使います。 それ以外の場合は使わないようにしましょう。
Bad | Good |
---|---|
|
|
- 関数は呼び出される順番におおまかにソートされるべきです
- 関数はレシーバーごとにまとめられているべきです。
なので、struct
、const
、var
の次にパッケージ外に公開されている関数が来るべきです。
newXYZ()
やNewXYZ()
は型が定義されたあと、他のメソッドの前に定義されている必要があります。
関数はレシーバーごとにまとめられているので、ユーティリティな関数はファイルの最後の方に出てくるはずです。
Bad | Good |
---|---|
|
|
エラーや特殊ケースなどは早めにハンドリングしてreturn
したりループ内ではcontinue
やbreak
してネストが浅いコードを目指しましょう。
ネストが深いコードを減らしていきましょう。
Bad | Good |
---|---|
|
|
if-else のどちらでも変数に代入する場合、条件に一致した場合に上書きするようにしましょう。
Bad | Good |
---|---|
|
|
パッケージ変数で式と同じ型なら型名を指定しないようにしましょう。
Bad | Good |
---|---|
|
|
式の型と合わない場合は明示するようにしましょう。
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F は myError 型を返すが私達は error 型が欲しい
公開されてないtop-levelのvar
やconst
の名前には最初にアンダースコアをつけることでより内部向けてあることが明確になります。
ただerr
で始まる変数名は例外です。
理由としてtop-levelの変数のスコープはそのパッケージ全体です。一般的な名前を使うと別のファイルで間違った値を使ってしまうことになります。
Bad | Good |
---|---|
|
|
埋め込まれた型は構造体の定義の最初に置くべきです。 また、通常のフィールドと区別するために1行開ける必要があります。
Bad | Good |
---|---|
|
|
埋め込みは適切な方法で機能を追加、拡張できる具体的なメリットがある場合に使いましょう。 ユーザーに悪影響を与えずに行う必要があります。Avoid Embedding Types in Public Structs も参照しましょう。
例外: Mutex は埋め込むべきではありません。公開されないフィールドとして使いましょう。Zero-value Mutexes are Valid も参照しましょう。
埋め込みは次のことをすべきではありません。:
- 見た目や、手軽さを重視して使うこと
- 埋め込まれた型が使いにくくなること
- 埋め込まれた型のゼロ値に影響すること。もし埋め込まれた型が便利なゼロ値を持っているなら、埋め込まれた後にもそれが維持されるようにしなければいけません。
- 埋め込まれた副作用として関係ないメソッドやフィールドが公開されてしまうこと
- 公開してない型を公開してしまうこと
- 埋め込まれた型のコピーに影響が出ること
- 埋め込まれた型の API や、意味上の型が変わってしまうこと
- 非標準の形式で埋め込むこと
- 埋め込まれる型の実装の詳細を公開すること
- 型の内部を操作できるようにすること
- ユーザーが意図しない方法で内部関数の挙動を変えること
簡単にまとめると、きちんと意識して埋め込みましょうということになります。 使うべきかチェックする簡単な方法は、「埋め込みたい型の公開されているメソッドやフィールドは全て埋め込まれる型に直接追加する必要があるか?」です。 答えが、「いくつかはある」あるいは「ない」の場合は埋め込みは使わず、フィールドを使いましょう。
Bad | Good |
---|---|
|
|
|
|
|
|
変数が明示的に設定される場合、:=
演算子を利用しましょう。
Bad | Good |
---|---|
|
|
しかし空のスライスを宣言する場合はvar
キーワードを利用したほうがよいでしょう。参考資料: Declearing Empty Slices
Bad | Good |
---|---|
|
|
nil
は長さ0のスライスとして有効です。
つまり以下のものが有効です。
-
長さ0のスライスを返す代わりに
nil
を返すBad Good if x == "" { return []int{} }
if x == "" { return nil }
-
スライスが空かチェックするためには
nil
かチェックするのではなくlen(s) == 0
でチェックするBad Good func isEmpty(s []string) bool { return s == nil }
func isEmpty(s []string) bool { return len(s) == 0 }
-
varで宣言しただけのゼロ値が有効
Bad Good nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
できる限り変数のスコープを減らしましょう。ただネストを浅くすることとバランスを考えてください。
Bad | Good |
---|---|
|
|
もし関数の戻り値をifの外で利用する場合、あまりスコープを縮めようとしなくてもよいでしょう。
Bad | Good |
---|---|
|
|
値をそのまま関数の引数に入れることは可読性を損ないます。 もし分かりづらいならC言語スタイルのコメントで読みやすくしましょう。
Bad | Good |
---|---|
|
|
よりよいのはただのbool
を自作の型で置き換えることです。こうすると型安全ですし可読性も上がります。
更に将来的にtrue/false以外の状態も利用可能に修正することもできます。
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady = iota + 1
StatusDone
// Maybe we will have a StatusInProgress in the future.
)
func printInfo(name string, region Region, status Status)
Goは複数行や引用符のためにRaw string literal
をサポートしています。
これらをうまく使って手動でエスケープした読みづらい文字列を避けてください。
Bad | Good |
---|---|
|
|
構造体を初期化する際にはフィールド名を書くようにしましょう。
go vet
でこのルールは指摘されます。
Bad | Good |
---|---|
|
|
例外としてフィールド数が3以下のテストケースなら省略してもよいです。
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
フィールド名を使って構造体を初期化するときは、意味のあるコンテキストを提供しない場合はフィールド名を省略しましょう。Goが自動的に型に応じたゼロ値を設定してくれます
Bad | Good |
---|---|
|
|
省略されたフィールドはデフォルトのゼロ値持つことは読み手の負荷を下げることができます。デフォルト値ではない値だけが指定されているからです。
ゼロ値をあえてセットする意味がある場合もあります。例えば、Test Tables で指定するテストケースではゼロ値でも設定することは役に立ちます。
全てのフィールドを省略して宣言するときは、 var
を使って構造体の宣言をしましょう。
Bad | Good |
---|---|
|
|
map initializationでも似たようなことをしていますが、この方法を使うと、ゼロ値だけの構造体の宣言と、フィールドに値を指定する宣言を区別することができます。そしてこの方法はdeclare empty slicesと同じ理由でおすすめの方法です。
構造体の初期化と同じように構造体のポインタを初期化するときはnew(T)
ではなく、&T{}
を使いましょう。
Bad | Good |
---|---|
|
|
空のマップを作る場合は make(...)
を使い、コード内で実際にデータを入れます。
こうすることで、変数宣言と視覚的に区別でき、後でサイズヒントをつけやすくなります。
Bad | Good |
---|---|
|
|
変数宣言と初期化が視覚的に似ている |
変数宣言と初期化が視覚的に区別しやすい |
可能なら make()
でマップを初期化する際にキャパシティのヒントを渡しましょう。
詳細はPrefer Specifying Map Capacity Hintsを参照してください。
一方で、マップが予め決まった要素だけを保つ場合にはリテラルを使って初期化するほうがよいでしょう。
Bad | Good |
---|---|
|
|
大まかな原則は初期化時に決まった要素を追加するならマップリテラルを使い、それ以外なら make()
(とあるならキャパシティのヒント)を使いましょう。
フォーマット用の文字列をPrintf
スタイルの外で定義する場合はconst
を使いましょう。
こうすることでgo vet
などの静的解析ツールでチェックしやすくなります。
Bad | Good |
---|---|
|
|
Printf
スタイルの関数を使う場合、go vet
がフォーマットをチェックできるか確認しましょう。
これは可能であればPrintf
スタイルの関数名を使う必要があることを示しています。
go vet
はデフォルトでこれらの関数をチェックします。
事前に定義された関数名を使わない場合、関数名の最後をf
にしましょう。
例えばWrap
ではなくWrapf
にします。
go vet
は特定のPrintf
スタイルのチェックができるようになっていますが、末尾がf
である必要があります。
$ go vet -printfuncs=wrapf,statusf
go vet: Printf family checkを更に参照してください。
サブテストを利用したテーブルドリブンテストでコアのテストロジックを繰り返すときにコードの重複を避けるようにしましょう。
Bad | Good |
---|---|
|
|
テストテーブルを使うと、エラーメッセージへの情報の追加やテストケースの追加も簡単ですし、コードも少なくなります。
テストケースのルールはテストケースの構造体のスライス名が tests
、ループ内のそれぞれのテストケースの変数名が tt
とします。
更に入力値と出力値をわかりやすくするためにgive
やwant
などのプレフィックスをつけることを推奨しています。
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
テーブルテストで実施するテストの中で条件付きのアサーションや分岐ロジックがあると、可読性が低くなり保守がとても難しくなります。
テーブルテストでは for
の中で、複雑なコードや条件分岐を入れるべきではありません。
テストが失敗してデバッグする必要があるとき、大きくて複雑なテーブルテストは可読性と保守性を大きく損ないます。
そのようなテーブルテストはいくつかのテーブルテストに分割するか、そもそも別のテスト関数に分けるのも良いでしょう。
いくつかの方針を紹介します。
- 振る舞いが小さくなるように意識する
- 条件付きのアサーションを避けて、テストの深さを最小にする
- テーブルのフィールドが全てのテストで使われているか確認する
- 全てのロジックが全てのテストケースで実行されるか確認する
ここでいう「テストの深さ」とは、「そのテストで、前のアサーションを保持する必要がある連続したアサーションの数」といえます。 循環複雑度に近いです。より薄いテストはよりアサーション間の関係が薄く、より重要な点はそれらのアサーションは条件付きになる可能性が低くなることです。
具体的に言うと、次のような状況だとテストを読むのが難しくなります。
- テーブルのフィールドによって条件分岐が複数ある。(
shouldError
やexpectCall
などのフィールドがあると注意です ) - 特定のモックの期待値のためにたくさんの
if
がある。(shouldCallFoo
などのフィールドがあると怪しいです ) - テーブルの中に関数がある。(フィールドの中に
setupMocks func(*FooMock)
がある )
しかし、変更された入力に基づいてのみ変化する動作をテストする場合、比較可能なユニットを別々のテストに分割して比較しにくくするのではなく、すべての入力に対してどのように動作が変化するかをよりよく説明するために、同様のケースをまとめてテーブルテストとすることが望ましい場合があります。
テスト本体が短くてわかりやすければ、成功ケースと失敗ケースの分岐経路を1つにして、shouldErr
のようなフィールドでエラーを期待することもできます。
Bad | Good |
---|---|
|
|
このコードの複雑さは変更したり、理解したり、このテストが正しいのかを証明するのが困難になります。
厳密なガイドラインはありませんが、システムへの複数の入力がある場合は、可読性と保守性を常に考慮して、個別のテストかテーブルテストか決定しましょう。
並列テストなどの特殊なループ(例えば、ループの一部としてゴルーチンを起動し参照を保持するもの)では、ループのスコープ内の変数が正しい値を保持しているか注意しましょう。
tests := []struct{
give string
// ...
}{
// ...
}
for _, tt := range tests {
tt := tt // for t.Parallel
t.Run(tt.give, func(t *testing.T) {
t.Parallel()
// ...
})
}
この例では、 t.Parallel()
を下で読んでいるので、 tt
という変数をループの中で再度宣言する必要があります。
もしやらないと、ほとんどのテストで tt
変数が期待しない値になったり、テスト中に値が変更されてしまいます。
Functional Option パターンは不透明なOption型を使って内部の構造体に情報を渡すパターンです。 可変長引数を受け取り、それらを順に内部のオプションに渡します。
コンストラクタや、公開されたAPIで3つ以上の多くの引数が必要な場合、このパターンを使うと良いでしょう。
Bad | Good |
---|---|
|
|
キャッシュやロガーはデフォルトを使う場合でも常に指定する必要があります
|
オプションは必要なら提供されます
|
私達が紹介する方法は非公開のメソッドを持った Option
インタフェースを使う方法です。
指定されたオプションは非公開の options
構造体に保持されます。
type options struct {
cache bool
logger *zap.Logger
}
type Option interface {
apply(*options)
}
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
func WithCache(c bool) Option {
return cacheOption(c)
}
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
// Open creates a connection.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
// ...
}
このパターンを実装するためにクロージャを使っていますが、この方法は開発者により柔軟性をもたせ、デバッグやテストをしやすくなると考えています。
特に、テストやモックなどで比較する際にクロージャを使うことで比較しやすくなります。
更に、options
に他のインタフェースを実装させる事もできます。
fmt.Stringer
インタフェースを実装すると、設定を人間にわかりやすく表示させることも可能です。
更に以下の資料が参考になります。
どんなおすすめの linter のセットよりも重要なのは、コードベース全体で一貫した linter を使うことです。
私達は最小限の linter として以下のものをおすすめしています。これだけあれば、一般的な問題を発見でき、不必要に厳しすぎることもなく、良い品質を確立することができるからです。
- errcheck はエラーが正しく処理されているかを担保します
- goimports はライブラリのインポートの管理もするフォーマッタです
- golint は一般的なスタイルのミスを見つけます
- govet は一般的なミスを見つけます
- staticcheck は多くの静的チェックを行います
私達は、大きなコードベースでのパフォーマンスと、多くの linter を一度に設定できる点から、golangci-lint をおすすめしています。 このガイドのリポジトリにはおすすめの .golangci.yaml 設定ファイルがあります。
golangci-lint は多くのlinterが使えます。前述した linter は基本のセットですが、チームが必要に応じて linter を追加することを推奨しています。