はじめに
みなさん、こんにちは!
Go大好きエンジニアのくろたくです。
ちょっと前になりますが、GoogleからGoのスタイルガイドが発表されましたね🙌
A Go style guide from Google is dropped!
— inanc (@inancgumus) November 19, 2022
So many gems here. I read it all in one sitting.
It’s still in progress. Looking forward to the Go tips section.https://t.co/grav7zkJFJ#golang
今更ながら、アドカレの機会を利用してスタイルガイドを読んでアウトプットしようと考えました。
とはいえ、スタイルガイドの内容全てを読んで一気にアウトプットするとなると相当な時間と労力がかかります。そこで、この記事は1度アウトプットしたら終わりというものにするのではなく、書き足していくこと前提で書こうと決めました。
アップデートを見逃さないように、LGTM、ストックしておくことをオススメします👍
Go Style Guildeを読んでみようかなと思ってもらえるきっかけになれたら嬉しいです!
それでは、CyberAgent AI tech studio | Go Advent Calendar 2022の18日目の記事スタートです🎉
自分に合ったスタイルガイドを見つけよう!
Go Style Guideのトップページには、3種類の文書が存在することと、それぞれの文書にどんなことが書かれているかがまとめられています。
| 文書 | 対象読者 | 書かれている内容 |
|---|---|---|
| Style Guide | 全ての人 | スタイルの原則(明瞭性、単純性、簡潔性、保守性、一貫性)について。それぞれの説明と事例を交えた深堀り。 |
| Style Decisions | Goを書く全ての開発者(特にコードレビューを担当する人) | スタイルの原則を達成するためにGoにおいて意識するべきコードの書き方をまとめたもの。 |
| Best Practices | 興味のある人 | Goで開発する際に出くわすことの多いコード例を取り上げ、各ケースごとにどのようにスタイルガイドを適用させていくべきか書かれている。 |
※この記事では、2番目のStyle Decisionsの内容をまとめています。
※優先順位については筆者の独断と偏見で、重要だと思ったところから順に書いています。
Naming
Underscores - アンダースコア(_)について
下記の例外を除いては基本的に使ってはだめ。
- 自動生成されたコードのパッケージ名
- テストコード(
*_test.go)内の関数名 - OSやcgo(GoからC言語のコードを呼び出すための仕組み)を制御するような低レイヤーライブラリにおける、識別子を再利用する場合
Package names - package名について
小文字のアルファベットと数字のみで構成されている必要がある。(例:k8s, oauth2 のように数字を含む名前も許可されます)
例:
tabwriter → ⭕️
tabWriter → ❌
TabWriter → ❌
tab_writer → ❌
よく使われるローカル変数名は避ける。
例:
count → usercount
情報量の多いパッケージ名は避ける。
例:
util, utility, common, helper, model, testhelper
▼ Best practicesドキュメントより参照したutilパッケージの例
// Good:
db := spannertest.NewDatabaseFromFile(...)
// Bad:
db := test.NewDatabaseFromFile(...)
Receiver names - Receiver名について
Receiver名は、
- 短く(だいたい1〜2文字くらいの長さが目安)
- 型名の略称
- レシーバーごとに名前が違うのはNG
であるべき。
| 悪い例 | 良い例 |
|---|---|
func (tray Tray) |
func (t Tray) |
func (info *ResearchInfo) |
func (ri *ResearchInfo) |
func (this *ReportWriter) |
func (w *ReportWriter) |
func (self *Scanner) |
func (s *Scanner) |
Constant names - 定数名について
定数もGoの基本原則とされているMixedCaps(単語の区切りをアンダースコアではなく大文字で表すGoの命名規約)に従って、キャメルケースで書く。
大文字から始めるか小文字から始めるかで他のパッケージから使われるグローバル定数か、パッケージ内のみで利用できるローカル変数かを区別する。
// Good:
const MaxPacketSize = 512
const (
ExecuteBit = 1 << iota
WriteBit
ReadBit
)
// Bad:
const MAX_PACKET_SIZE = 512
const kMaxBufferSize = 1024
const KMaxUsersPergroup = 500
定数には、値自体ではなく、役割に基づいて名前をつけること。
もしも、定義しようとしている定数がその値とは別の意味や役割を持たないのであれば、定数として定義する必要はない。
// Bad:
const Twelve = 12
const (
UserNameColumn = "username"
GroupColumn = "group"
)
Initialisms - 頭字語(URLやID)について
URL や ID などのように、各単語の頭文字からなる略である場合は、それらを同じケース(大文字なら全て大文字)で表す必要がある。
例:
❌ URL → Url
❌ appID → appId
下記を参考にするとわかりやすいと思います。
| Initialism(s) | Scope | Correct | Incorrect |
|---|---|---|---|
| XML API | Exported | XMLAPI | XmlApi, XMLApi, XmlAPI, XMLapi |
| XML API | Unexported | xmlAPI | xmlapi, xmlApi |
| iOS | Exported | IOS | Ios, IoS |
| iOS | Unexported | iOS | ios |
| gRPC | Exported | GRPC | Grpc |
| gRPC | Unexported | gRPC | grpc |
| DDoS | Exported | DDoS | DDOS, Ddos |
| DDoS | Unexported | ddos | dDoS, dDOS |
| ID | Exported | ID | Id |
| ID | Unexported | id | iD |
| DB | Exported | DB | Db |
| DB | Unexported | db | dB |
Getters
関数名に基本概念としてGetという単語が含まれていない限りGetを付与するべきでない。
※基本概念としてGetという単語が含まれている例: HTTP GET
// Bad
func GetCounts() int
// Good
func Counts() int
関数が複雑な計算の実行やリモートコールを伴う場合は、Getの代わりにComputeやFetchなどの別の単語を使用する。目的としては、関数呼び出しに時間がかかり、ブロックや失敗する可能性があることを関数の使用者に伝える目的がある。
Variable names - 変数名について
変数名の長さは、対象とするスコープの大きさに比例し、そのスコープ内で使用される回数に反比例するはず。
つまり、スコープが広くなるほど、変数名は長くなり、使用頻度が低ければ変数名は短くなるということ。
ファイルスコープで宣言された変数は複数の単語を必要とするかもしれないが、単一のインナーブロックにスコープされた変数はコードを明確に保ち、余計な情報を省略するために1単語または、1~2文字で済むかもしれない。
▼ ファイルスコープの例
var Users = []*User{
{/* フィールドは省略 */},
}
▼ 単一のインナーブロックスコープの例
for _, u := range users {
fmt.Println(u)
}
スコープの範囲について、目安は下記。
- 小範囲:1~7行程度の小さな操作を1~2回行うもの
- 中程度のスコープ:8~15行程度の小さな操作を数回、または大きな操作を1回行うもの
- 大きなスコープ:1つまたはいくつかの大きな操作が行われるもので、例えば15~25行のもの
- 非常に大きな範囲:1ページ以上にまたがるもの(例えば25行以上)
他の判断軸として、概念の具体性がある。
例えば、使用するデータベースが1つだけだと仮定すると、dbのような短い変数名は、スコープが非常に大きい場合でも明確なままである可能性が高い。
このように、変数名が短くとも、その変数が表す概念が一意に定まる場合であれば短い変数名も許容される。
ローカル変数の名前は、その値がどこで作られたかよりも、その変数が何を含んでいて、現在の文脈でどのように使われているかを反映するべき。という前提を置きつつ、下記に一般的なアプローチを示す。
- 一語の変数名はファーストチョイスとして良い場合が多い(例:
count,options) - 似たような変数の違いについて、単語を追加することで曖昧さをなくすことができる(例:
userCount,projectCount) - 短い変数名が良いからといって、単純に文字を削除して略語化することは避けること。(NG:
Sandbox→sbx) - 変数名には型に関する情報を省くこと
- ❌
numUsers,usersInt→ ⭕️userCount - ❌
userSlice→ ⭕️users - 例外として、入力値としてはstring型で受け取った
ageをintへパースする場合などについては、inputの変数名としてageStringなどを使うことは許容される
- ❌
- 周囲の文脈から明らかな単語は省略する。
- 例えば、UserCountメソッドの実装では、userCountというローカル変数はおそらく冗長である。
1文字の変数名について、下記のような場合に使うと良い。
- メソッドのレシーバー変数には、1文字または2文字の名前を付けるのが好ましい。
- 一般的な型のために馴染みのある変数名を使う場合
-
rはio.Readerや*http.Requestを表します。 -
wはio.Writerまたはhttp.ResponseWriterです。
-
- 整数型のループ変数では、特にインデックス(iなど)や座標(xやyなど)には一文字の識別子を使うことができる
- スコープが短い場合は、
for _, n := range nodes { ... }のように省略形もループ識別子として使用できる。
Repetition - 変数の冗長化に対する対策
変数名が長くなりすぎないように、不必要に文脈や型名を繰り返すのは避けましょう。
Package vs. exported symbol name
- ❌
widget.NewWidget→ ⭕️widget.New - ❌
widget.NewWidgetWithName→ ⭕️widget.NewWithName - ❌
db.LoadFromDatabase→ ⭕️db.Load - ❌
goatteleportutil.CountGoatsTeleported→ ⭕️gtutil.CountGoatsTeleportedorgoatteleport.Count - ❌
myteampb.MyTeamMethodRequest→ ⭕️mtpb.MyTeamMethodRequestormyteampb.MethodRequest
Variable name vs. type
- ❌
var numUsers int→ ⭕️var users int - ❌
var nameString string→ ⭕️var name string - ❌
var primaryProject int→ ⭕️var primary *Project
パースすることが前提になっているユーザーからの入力値などの変数名では型名を変数名に入れて良いこととする。
// Good
limitStr := r.FormValue("limit")
limit, err := strconv.Atoi(limitStr)
External context vs. local names
名前に周囲の文脈の情報を含めると、多くの場合余計なノイズになる。
パッケージ名、メソッド名、型名、関数名、インポートパス、そしてファイル名さえも変数名からは省略するべきでしょう。
▼ In package "ads/targeting/revenue/reporting"
// Bad:
type AdsTargetingRevenueReport struct{}
func (p *Project) ProjectName() string
// Good:
type Report struct{}
func (p *Project) Name() string
▼ In package "sqldb"
// Bad:
type DBConnection struct{}
// Good:
type Connection struct{}
▼ In package "ads/targeting/revenue/reporting"
// Bad:
func Process(in *pb.FooProto) *Report {
adsTargetingID := in.GetAdsTargetingID()
}
// Good:
func Process(in *pb.FooProto) *Report {
id := in.GetAdsTargetingID()
}
文脈や用法から明らかな名称についてもできるだけ省略するようにしよう。
// Bad:
func (db *DB) UserCount() (userCount int, err error) {
var userCountInt64 int64
if dbLoadError := db.LoadFromDatabase("count(distinct users)", &userCountInt64); dbLoadError != nil {
return 0, fmt.Errorf("failed to load user count: %s", dbLoadError)
}
userCount = int(userCountInt64)
return userCount, nil
}
// Good:
func (db *DB) UserCount() (int, error) {
var count int64
if err := db.Load("count(distinct users)", &count); err != nil {
return 0, fmt.Errorf("failed to load user count: %s", err)
}
return int(count), nil
}
Commentary
Comment line length - コメントの文字数について
コメントは、狭い画面でも読みやすいようにすることが大切です。
目安としては80文字程度で改行するようにしましょう。ただし、これは厳密なルールではなく、あくまで目安です。
// Good:
// これはコメントの段落です。
// Godocでは各行の長さは問題になりませんが、
// 折り返し方を工夫すると狭い画面でも読みやすくなります。
//
// 長いURLは途中で改行しなくても大丈夫です:
// https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/
//
// 改行が多すぎて読みにくくなる場合は、
// 判断して長い行のままにした方が良いこともあります。
URLなどの長い文字列を途中で改行する必要はありません。むしろ、改行が多すぎると読みにくくなることもあるので、判断に迷ったら読みやすさを優先しましょう。
なお、標準ライブラリでは句読点やセマンティックな意味の区切りに基づいて自然に改行するスタイルが多く採用されており、その場合は1行が60〜70文字程度になることもあります。厳密な文字数にこだわるよりも、意味の切れ目で改行する方が読みやすいコメントになります。
コードレビュー時に、小さな画面で読んだ時にジグザグに折り返されてしまうような長いコメントは避けるようにしましょう。
Doc comments - ドキュメント用コメントについて
トップレベルのエクスポートされた名前にはすべてドキュメントコメントが必要です。また、エクスポートされていない型や関数であっても、振る舞いが自明でないものにはドキュメントコメントを付けましょう。
ドキュメントコメントは完全な文で書き、対象の名前から始めるのがルールです。冠詞(A、An、The)を名前の前に付けて、自然に読めるようにすることもできます。
// Good:
// Request はコマンドを実行するためのリクエストを表します。
type Request struct { ...
// Encode は req の JSON エンコード結果を w に書き込みます。
func Encode(w io.Writer, req *Request) { ...
ドキュメントコメントはGodocやIDEのドキュメントに表示されるため、パッケージの利用者を意識して書きましょう。
構造体のフィールドについては、グループごとにコメントを付けたり、行末コメントで簡潔に説明することができます。
// Good:
// Options はグループ管理サービスの設定を保持します。
type Options struct {
// 基本設定:
Name string
Group *FooGroup
// 依存関係:
DB *sql.DB
// カスタマイズ:
LargeGroupThreshold int // 省略可; デフォルト: 10
MinimumMembers int // 省略可; デフォルト: 2
}
💡Tips: エクスポートされていないコードであっても、エクスポートされたコードと同じ慣習(名前から始める)に従うことをオススメします。将来エクスポートする際に修正の手間が省けます。
Comment sentences - コメント文について
完全な文として書くコメントは、英語の標準的な大文字・句読点のルールに従いましょう。文の断片(フラグメント)の場合はこれらの規約に従う必要はありません。
ドキュメントコメントは常に完全な文で書き、大文字で始め、句読点で終えましょう。一方、構造体のフィールドに付ける行末コメントなどは、フレーズ(断片)の形式で書いても問題ありません。
// Good:
// Server はシェイクスピア全集から名言を提供するサービスを処理します。
type Server struct {
// BaseDir はシェイクスピア作品が格納される
// ベースディレクトリのパスを指します。
//
// ディレクトリ構造は以下を想定しています:
// {BaseDir}/manifest.json
// {BaseDir}/{name}/{name}-part{number}.txt
BaseDir string
WelcomeMessage string // ユーザーログイン時に表示される
ProtocolVersion string // 受信リクエストの検証に使用
PageLength int // 印刷時の1ページあたりの行数(省略可; デフォルト: 20)
}
上記の例では、BaseDirフィールドのコメントは完全な文で書かれているのに対し、WelcomeMessageやProtocolVersion、PageLengthの行末コメントは断片的な表現で書かれています。フィールド名が主語の役割を果たすため、行末コメントは断片で書いても十分に伝わります。
Examples - 例について
パッケージは、実行可能なExample(テスト例)を提供することで、意図された使い方を明確にドキュメント化するべきです。Exampleはプロダクションコードではなく、テストファイル(*_test.go)内に記述します。Godoc上に表示されるため、利用者にとって非常に参考になります。
実行可能なExampleが書けない場合は、コメント内にコード例を記述しましょう。標準的なフォーマット規約に従い、利用者が典型的な使い方を理解できるようにすることが大切です。
Named result parameters - 名前付き結果パラメータについて
関数のシグネチャがGodoc上でどのように表示されるかを意識しましょう。通常は、関数名と返り値の型だけで十分に意味が伝わります。
// Good:
func (n *Node) Parent1() *Node
func (n *Node) Parent2() (*Node, error)
同じ型の返り値が2つ以上ある場合は、名前を付けることでそれぞれの役割を明確にできます。
// Good:
func (n *Node) Children() (left, right *Node, err error)
また、呼び出し側で特定のアクションが必要な場合にも、名前付き結果パラメータは有効です。
// Good:
// WithTimeout は、現在時刻から最大 d(Duration)後にキャンセルされる
// コンテキストを返します。
//
// リソースリークを防ぐため、呼び出し元はコンテキストが
// 不要になったタイミングで返却されたcancel関数を必ず呼び出してください。
func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func())
一方で、型と意味が自明な場合に不要な名前を付けると、かえって冗長になってしまいます。
// Bad:
func (n *Node) Parent1() (node *Node)
func (n *Node) Parent2() (node *Node, err error)
naked return(名前付き返り値を使い、値を指定せずreturnだけで返す書き方)を有効にするためだけに名前を付けるのは避けましょう。コードの明確さは常に簡潔さに優先します。
ただし、deferクロージャ(関数の終了時に実行される無名関数)内で返り値を変更する必要がある場合は、名前付き結果パラメータを使うのは問題ありません。
Package comments - パッケージの説明文について
パッケージコメントは、package宣言の直前に空行なしで記述する必要があります。
// Good:
// Package math は基本的な定数と数学関数を提供します。
//
// このパッケージはアーキテクチャ間でビット同一の結果を保証しません。
package math
パッケージごとに1つだけパッケージコメントを書きましょう。複数のファイルで構成されるパッケージでも、パッケージコメントを書くファイルは1つだけにします。
また、複数行のコメントには/* */形式のブロックコメントも使用できます。これは、ドキュメント内にサンプルコマンドやテンプレートなど、ソースからそのままコピー&ペーストして使いたいセクションが含まれる場合に特に便利です。
mainパッケージの場合は、パッケージ名の代わりにBUILDファイル(Bazelビルドシステムのビルド設定ファイル)で指定されたバイナリ名を使います。コメントの書き出しには以下のようなバリエーションが認められています。
// Binary seed_generator ...// Command seed_generator ...// Program seed_generator ...// The seed_generator command ...// The seed_generator program ...// Seed_generator ...
// Good:
// seed_generator コマンドは、JSONスタディ設定ファイル群から
// Finch シードファイルを生成するユーティリティです。
package main
💡Tips: パッケージコメントが非常に長くなる場合は、doc.goファイルにパッケージコメントとpackage句だけを記述するのも良いでしょう。
Imports
Import renaming - インポートのエイリアス化について
インポートしたパッケージは通常、リネームすべきではありません。ただし、名前の衝突やわかりにくいパッケージ名の場合は、リネームが正当化されます。
リネームする場合のローカル名は、パッケージの命名規則に従い、アンダースコアや大文字は使わないようにしましょう。同じパッケージに対しては、コードベース全体で同じローカル名を使うよう一貫性を持たせましょう。
名前の衝突が起きた場合は、最もローカルな(プロジェクト固有の)インポートをリネームします。
// Good:
import (
foosvcpb "path/to/package/foo_service_go_proto"
)
生成されたProtocol Buffer(Googleが開発した、構造化データをバイト列にシリアライズするための仕組み)パッケージをリネームする際は、パッケージ名からアンダースコアを除去し、ローカル名にはpbサフィックスを付ける慣習があります。上記の例では、foo_service_go_protoから_go_protoサフィックスを除き、foosvcにpbを付けてfoosvcpbとしています。
utilやv1のような情報量の少ないパッケージ名の場合も、控えめにリネームできます。可能であれば、パッケージ自体をより適切な名前にリファクタリングしましょう。
// Good:
import (
core "github.com/kubernetes/api/core/v1"
meta "github.com/kubernetes/apimachinery/pkg/apis/meta/v1beta1"
)
インポートしたパッケージ名が、よく使うローカル変数名(urlやsshなど)と衝突する場合は、pkgサフィックスを付けてリネームします。
// Good:
import (
urlpkg "golang.org/x/net/url"
)
Import grouping - インポートのグループ化について
インポートは以下の順序でグループ分けし、各グループの間には空行を入れましょう。
- 標準ライブラリ
- プロジェクトパッケージ・ベンダーパッケージ
- Protocol Bufferのインポート
- サイドエフェクトのみのインポート(
import _)
// Good:
package main
import (
"fmt"
"hash/adler32"
"os"
"github.com/dsnet/compress/flate"
"golang.org/x/text/encoding"
"google.golang.org/protobuf/proto"
foopb "myproj/foo/proto/proto"
_ "myproj/rpc/protocols/dial"
_ "myproj/security/auth/authhooks"
)
Import "blank"(import _) - blankインポートについて
サイドエフェクト(副作用)、つまりパッケージのinit()関数を実行させる目的(データベースドライバの登録など)のためだけにインポートするパッケージ(import _ "package")は、mainパッケージまたはそれを必要とするテストファイルでのみ使用できます。
ライブラリパッケージではblankインポートを避けましょう。 サイドエフェクトのインポートをmainパッケージに限定することで、依存関係の管理がしやすくなり、異なるインポートに依存するテスト間のコンフリクトも防げます。
例外として認められるケースは以下の2つです。
- nogo(Bazelビルドシステムで使用される静的解析ツール)スタティックチェッカーのdisallowed importsチェックを回避するためのblankインポート
-
//go:embedコンパイラディレクティブ(ファイルやディレクトリの内容をGoバイナリに埋め込むための仕組み)を使用するファイルにおけるembedパッケージのblankインポート
Import "dot"(import .) - dotインポートについて
import .は、別のパッケージからエクスポートされた識別子を、修飾なしで現在のパッケージに取り込む機能です。この機能は使わないでください。 どの機能がどのパッケージから来ているかがわかりにくくなり、可読性が低下します。
// Bad:
package foo_test
import (
"bar/testutil" // "foo" もインポートされる
. "foo"
)
var myThing = Bar() // Bar は foo パッケージで定義されているため修飾不要
// Good:
package foo_test
import (
"bar/testutil" // "foo" もインポートされる
"foo"
)
var myThing = foo.Bar()
上のBadの例では、Bar()がどこから来ているのか一目ではわかりません。Goodの例のようにfoo.Bar()と書くことで、この関数がfooパッケージに属していることが明確になります。
Errors
Returning errors - エラーのreturnについて
関数が失敗する可能性を含んでいる場合、返り値にerrorを指定する。
慣習上、errorは最後の返り値に指定されることが多いので、それに従う。
// Good:
func Good() error { /* ... */ }
errorの返り値としてnilを返すことは、失敗する可能性のある処理に成功したことを知らせる一般的な方法である。
もしエラーが発生してerr != nilの状態になった場合は、呼び出し側でエラー以外の戻り値を未指定として扱う必要がある。
// Good:
func GoodLookup() (*Result, error) {
res, err := doSomeThing()
if err != nil {
return nil, err
}
return res, nil
}
エラーを返す関数は、基本的にerror型を利用するようにしましょう。
// Bad:
func Bad() *os.PathError { /*...*/ }
Error strings - エラー文言について
エラー文言は、固有名詞や頭字語で始まらない限りは大文字から始めてはいけない。
また、文章のように句読点で終わらせてはいけない。
// Bad:
err := fmt.Errorf("Something bad happened.")
// Good:
err := fmt.Errorf("something bad happened")
一方、エラーメッセージ(ロギング、テスト失敗、APIレスポンス、その他のUI上で表示されるメッセージなど)では、大文字から始まる必要がある。
// Good:
log.Infof("Operation aborted: %v", err)
log.Errorf("Operation aborted: %v", err)
t.Errorf("Op(%q) failed unexpectedly; err=%v", args, err)
Handle errors - エラーハンドリングについて
エラーに遭遇したコードは、エラーをどのように処理するか意図的に選択する必要がある。
通常、_変数を利用してエラーを無視することは得策とは言えない。
関数がエラーを返した場合は、以下のいずれかを実行する必要がある。
- エラーを直ちに処理して対処する。
- エラーを呼び出し元に返す。
- 例外的な状況においては、
log.Fatalまたはpanicを呼び出す。
ライブラリによりエラーが起きることがないことを保証されている場合( (*bytes.Buffer).Writeなど)の、エラーを無視できる稀な状況では、付随するコメントでなぜそれが安全なのかを説明する必要がある。
// Good:
var b *bytes.Buffer
n, _ := b.Write(p) // non-nilのエラーを返すことは絶対にない
In-band errors - 特別な戻り値によるエラー通知について
CやC++などの言語では、関数が-1、null、空文字列といった特別な値を返してエラーや結果の欠如を知らせることが一般的です。これは「イン・バンドエラー(in-band error)」、つまり通常の戻り値と同じ経路で特別な値を使ってエラーを表現する手法です。
// Bad:
// Lookup はkeyに対応する値を返します。マッピングが存在しない場合は-1を返します。
func Lookup(key string) int
イン・バンドエラー値のチェックを忘れると、バグを引き起こしたり、エラーの原因を誤った関数に帰属させてしまう可能性があります。
// Bad:
// 以下の行はParseの入力値が不正だったかのようなエラーを返しますが、
// 本当の原因はmissingKeyに対応するマッピングが存在しないことです。
return Parse(Lookup(missingKey))
Goの多値返却(multiple return values)はこの問題に対するより良い解決策を提供してくれます。イン・バンドエラー値のチェックをクライアントに要求するのではなく、他の返り値が有効かどうかを示す追加の値を返すようにしましょう。この返り値は、説明が不要な場合はerror型またはbool型とし、最後の返り値に配置します。
// Good:
// Lookup はkeyに対応する値を返します。マッピングが存在しない場合はok=falseを返します。
func Lookup(key string) (value string, ok bool)
このAPIにより、呼び出し側がParse(Lookup(key))のような誤ったコードを書くことがコンパイル時にエラーとして検出されます。Lookup(key)の返り値が2つあるためです。
このようにエラーを返すことで、より堅牢で明示的なエラーハンドリングが促されます。
// Good:
value, ok := Lookup(key)
if !ok {
return fmt.Errorf("no value for %q", key)
}
return Parse(value)
一般的に、Goコードはエラーに対して追加の返り値を返すようにしましょう。
Indent error flow - エラー処理と正常処理の書き方について
関数を呼び出した後は、必ずエラー処理をしてからコードを進めるようにすること。
これにより、読み手が正常なパスを素早く見つける手助けになる。
エラーが起きない場合の正常なパスで実行されるコードは、ifブロックの後に表示されるべきで、else節のなかでインデントされるべきではない。
// Good:
if err != nil {
// エラー処理
return // または continue など
}
// 正常処理
// Bad:
if err != nil {
// エラー処理
} else {
// インデントのせいで異常に見える正常処理
}
関数の返り値を後の処理でも利用したい場合は、if文のブロックスコープに変数を閉じ込めてしまう書き方は避けるべきでしょう。
// Good:
x, err := f()
if err != nil {
// エラー処理
return
}
// xを使う処理が
// 複数行にわたって続く
// Bad:
if x, err := f(); err != nil {
// エラー処理
return
} else {
// xを使う処理が
// 複数行にわたって続く
}
Language
Literal formatting - リテラル構文について
Goには強力な複合リテラル構文があり、深くネストした複雑な値を1つの式で表現することができる。
可能な限りフィールドごとに値を構築するのではなく、リテラル構文を使用しましょう。
リテラルをより読みやすくするための追加ルールを下記に示す。
Field names
構造体への代入をする時は、フィールド名を指定して代入しよう。
// Good:
good := otherpkg.Type{A: 42}
// Bad:
// https://pkg.go.dev/encoding/csv#Reader
r := csv.Reader{',', '#', 4, false, false, false, false}
Matching braces
構造体の括弧{}のインデントは揃えよう。
// Good:
good := []*Type{{Key: "value"}}
// Good:
good := []*Type{
{Key: "multi"},
{Key: "line"},
}
// Bad:
bad := []*Type{
{Key: "multi"},
{Key: "line"}}
// Bad:
bad := []*Type{
{
Key: "value"},
}
Cuddled braces
スライスや配列の構造体で中括弧の間の括弧を削除する場合は下記の場合のみに限る。
- インデントが揃っている場合
- 内部の値(フィールド)もリテラルやプロトビルダー(Protocol Bufferのメッセージを構築するためのビルダーパターン)であること(変数や他の式ではないこと)
// Good:
good := []*Type{
{ // 密着させない
Field: "value",
},
{
Field: "value",
},
}
// Good:
good := []*Type{{ // 正しく密着させた場合
Field: "value",
}, {
Field: "value",
}}
// Good:
good := []*Type{
first, // 密着できない
{Field: "second"},
}
// Good:
okay := []*pb.Type{pb.Type_builder{
Field: "first", // Protoビルダーは縦方向のスペース節約のため密着可
}.Build(), pb.Type_builder{
Field: "second",
}.Build()}
// Bad:
bad := []*Type{
first,
{
Field: "second",
}}
Repeated type names
スライスやマップの構造体定義において、繰り返される型名宣言は省略しよう。
型名を明示的に示した方が良い場合は、プロジェクトでは一般的ではない複雑な型を扱うときや、繰り返される型名が離れた行にあり、読み手に文脈を思い出させる必要がある時のみ。
// Good:
good := []*Type{
{A: 42},
{A: 43},
}
// Bad:
repetitive := []*Type{
&Type{A: 42},
&Type{A: 43},
}
// Good:
good := map[Type1]*Type2{
{A: 1}: {B: 2},
{A: 3}: {B: 4},
}
// Bad:
repetitive := map[Type1]*Type2{
Type1{A: 1}: &Type2{B: 2},
Type1{A: 3}: &Type2{B: 4},
}
💡Tips: 上記のルールに従って、繰り返される型名を削除したい場合は、gofmt -sを実行すると良いでしょう。
Zero-value fields
ゼロ値を代入したいフィールドは、構造体リテラルの明確性が損なわれない場合においては省略することができる。
ゼロ値フィールドの省略により、代入されるフィールドのみに注目が集まるようになる。
// Bad:
import (
"github.com/golang/leveldb"
"github.com/golang/leveldb/db"
)
ldb := leveldb.Open("/my/table", &db.Options{
BlockSize: 1<<16,
ErrorIfDBExists: true,
// 以下のフィールドはすべてゼロ値です。
BlockRestartInterval: 0,
Comparer: nil,
Compression: nil,
FileSystem: nil,
FilterPolicy: nil,
MaxOpenFiles: 0,
WriteBufferSize: 0,
VerifyChecksums: false,
})
// Good:
import (
"github.com/golang/leveldb"
"github.com/golang/leveldb/db"
)
ldb := leveldb.Open("/my/table", &db.Options{
BlockSize: 1<<16,
ErrorIfDBExists: true,
})
Nil slices - nil値を返すsliceについて
nilと空のスライスの間に機能的な違いは存在しない。
lenやcapのような組み込み関数はnilスライスでも期待通りに動作する。
// Good:
import "fmt"
var s []int // nil(ゼロ値)
fmt.Println(s) // []
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
for range s {} // 何もしない(no-op)
s = append(s, 42)
fmt.Println(s) // [42]
空のスライスをローカル変数として宣言する場合(特に、戻り値になる予定の場合)は、呼び出し側によるバグのリスクを減らすためにnilの初期化を優先するようにすること。
// Good:
var t []string
// Bad:
t := []string{}
また、nil か空スライスかを呼び出し側が区別する必要があるようなAPIの設計も避けましょう。
nilなのか、空のスライスなのかをクライアントに判別させるようなAPIは実装しないこと。
// Good:
// Ping は対象ホストにpingを送信します。
// 正常に応答したホストの一覧を返します。
func Ping(hosts []string) ([]string, error) { ... }
// Bad:
// Ping は対象ホストにpingを送信し、正常に応答したホストの一覧を返します。
// 入力が空の場合は空リストを返します。
// nilはシステムエラーが発生したことを意味します。
func Ping(hosts []string) []string { ... }
スライスのチェックには、len()を使って比較すること。
空っぽかどうかを== nilでチェックすることは避けましょう。
// Good:
// describeInts はsが空でない場合に、prefixと共にsの内容を表示します。
func describeInts(prefix string, s []int) {
if len(s) == 0 {
return
}
fmt.Println(prefix, s)
}
// Bad:
func maybeInts() []int { /* ... */ }
// describeInts はprefixと共にsの内容を表示します。nilを渡すと完全にスキップします。
func describeInts(prefix string, s []int) {
// 'empty'ケース(nilまたは[]int{})でmaybeInts()が返す値によって
// この関数の振る舞いが意図せず変わってしまいます。
if s == nil {
return
}
fmt.Println(prefix, s)
}
describeInts("Here are some ints:", maybeInts())
【FYI】
なぜ、空スライスかどうかのチェックを== nilで行ってはいけないかについては、下記の記事が参考になると思います。
[Go]なぜsliceの空チェックで「nil」ではなく「長さ」でチェックするのか
Indentation confusion - インデントが招く混乱について
ifやforなどのコードブロック内の記述と条件式などの行が同一にならないようにしよう。
// Bad:
if longCondition1 && longCondition2 &&
// 条件3と4がifブロック内のコードと同じインデントになっています。
longCondition3 && longCondition4 {
log.Info("all conditions met")
}
Function formatting - 関数のフォーマットについて
関数についても、Indentation confusionで述べたように、引数が長い場合であっても1行で記述した方が良いでしょう。
インデントが変わることにより、ブロック内の処理と区別することが難しくなる。
また、関数の引数に長いインラインコメントを付けることは避けるべきです。代わりに、オプション構造体(Option struct)を使うか、関数のドキュメントコメントに詳細を記述するようにしましょう。
// Bad:
func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string,
foo4, foo5, foo6 int) {
foo7 := bar(foo1)
// ...
}
大量の引数を関数に指定したい場合に使える、引数を短くするテクニックは、Best practicesを参考にすると良いでしょう。
以下、Best practicesより引用した2つの回避策
Option structure
引数を1つの構造体にまとめてしまえば良いよね!という発想。
// Bad:
func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) {
// ...
}
// Good:
type ReplicationOptions struct {
Config *replicator.Config
PrimaryRegions []string
ReadonlyRegions []string
ReplicateExisting bool
OverwritePolicies bool
ReplicationInterval time.Duration
CopyWorkers int
HealthWatcher health.Watcher
}
func EnableReplication(ctx context.Context, opts ReplicationOptions) {
// ...
}
Variadic options
いわゆる可変長引数の実装。
注意書きでも書かれているが、コードの記述量は増えてしまうため、可変長引数を導入するメリットがオーバーヘッドを上回る場合のみ効果的とされている。
// Bad:
func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) {
...
}
// Good:
type replicationOptions struct {
readonlyCells []string
replicateExisting bool
overwritePolicies bool
replicationInterval time.Duration
copyWorkers int
healthWatcher health.Watcher
}
// ReplicationOption は EnableReplication の設定を行います。
type ReplicationOption func(*replicationOptions)
// ReadonlyCells は読み取り専用レプリカを追加で保持する
// セルを追加します。
//
// このオプションを複数回指定すると、追加の
// 読み取り専用セルが加算されます。
//
// デフォルト: なし
func ReadonlyCells(cells ...string) ReplicationOption {
return func(opts *replicationOptions) {
opts.readonlyCells = append(opts.readonlyCells, cells...)
}
}
// ReplicateExisting はプライマリセルに既存のファイルを
// レプリケートするかどうかを制御します。falseの場合、
// 新しく追加されたファイルのみがレプリケーション対象になります。
//
// このオプションを再度指定すると、以前の値を上書きします。
//
// デフォルト: false
func ReplicateExisting(enabled bool) ReplicationOption {
return func(opts *replicationOptions) {
opts.replicateExisting = enabled
}
}
// ... その他のオプション ...
// DefaultReplicationOptions は EnableReplication に渡されるオプションを
// 適用する前のデフォルト値を制御します。
var DefaultReplicationOptions = []ReplicationOption{
OverwritePolicies(true),
ReplicationInterval(12 * time.Hour),
CopyWorkers(10),
}
func EnableReplication(ctx context.Context, config *placer.Config, primaryCells []string, opts ...ReplicationOption) {
var options replicationOptions
for _, opt := range DefaultReplicationOptions {
opt(&options)
}
for _, opt := range opts {
opt(&options)
}
}
// 呼び出し側
func foo(ctx context.Context) {
// 複雑な呼び出し:
storage.EnableReplication(ctx, config, []string{"po", "is", "ea"},
storage.ReadonlyCells("ix", "gg"),
storage.OverwritePolicies(true),
storage.ReplicationInterval(1*time.Hour),
storage.CopyWorkers(100),
storage.HealthWatcher(watcher),
)
// シンプルな呼び出し:
storage.EnableReplication(ctx, config, []string{"po", "is", "ea"})
}
Conditionals and loops - ifとforについて
if文の条件式に改行は入れるべきではない。(Indentation confusionでも述べた通り。)
// Bad:
// 2行目の条件式がifブロック内のコードと同じインデントになっており、
// インデントの混乱を招いています。
if db.CurrentStatusIs(db.InTransaction) &&
db.ValuesEqual(db.TransactionKey(), row.Key()) {
return db.Errorf(db.TransactionError, "query failed: row (%v): key does not match transaction key", row)
}
より短いコードで書かなければならないという制約がない場合であれば、下記のようにBool値の取得処理を切り出せば良い。
// Good:
inTransaction := db.CurrentStatusIs(db.InTransaction)
keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key())
if inTransaction && keysMatch {
return db.Error(db.TransactionError, "query failed: row (%v): key does not match transaction key", row)
}
また、事前にデータを抽出しておくことで条件式を短くすることも可能だったりする。
// Bad:
if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) {
// ...
}
// Good:
uid := user.GetUniqueUserID()
if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) {
// ...
}
ifのコードブロックの可読性を上げるために、{}のインデントを揃えたりはきちんと対応しましょう。
// Good:
if err := db.RunInTransaction(func(tx *db.TX) error {
return tx.Execute(userUpdate, x, y, z)
}); err != nil {
return fmt.Errorf("user update failed: %s", err)
}
// Good:
if _, err := client.Update(ctx, &upb.UserUpdateRequest{
ID: userID,
User: user,
}); err != nil {
return fmt.Errorf("user update failed: %s", err)
}
for文については、なるべく改行を入れないようにしましょう。
// Good:
for i, max := 0, collection.Size(); i < max && !collection.HasPendingWriters(); i++ {
// ...
}
switch文についても、改行を入れないようにしましょう。
// Good:
switch good := db.TransactionStatus(); good {
case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting:
// ...
case db.TransactionCommitted, db.NoTransaction:
// ...
default:
// ...
}
// Bad:
switch bad := db.TransactionStatus(); bad {
case db.TransactionStarting,
db.TransactionActive,
db.TransactionWaiting:
// ...
case db.TransactionCommitted,
db.NoTransaction:
// ...
default:
// ...
}
case条件が長すぎる場合は、全ての条件式を改行した上で、コードブロック内の処理は1行空けてから記述すると良いでしょう。
// Good:
switch db.TransactionStatus() {
case
db.TransactionStarting,
db.TransactionActive,
db.TransactionWaiting,
db.TransactionCommitted:
// ...
case db.NoTransaction:
// ...
default:
// ...
}
if文の条件式については、比較演算子(==や<=)の左辺に変数を持ってきましょう。
// Good:
if result == "foo" {
// ...
}
// Bad:
if "foo" == result {
// ...
}
Copying - 値のコピーについて
予期せぬエイリアスを生んだり、バグの原因になったりするため、他のパッケージから構造体をコピーする際は注意が必要。
-
sync.Mutexのような同期オブジェクトはコピーNG -
bytes.Buffer型も、コピーするとコピー元と同じメモリ領域を参照するスライスが生まれてしまい、一方の変更がもう一方にも影響する副作用をもたらす可能性がある。
// Bad:
mu := sync.Mutex{}
mu2 := mu
// Good:
// ポインタを経由して共有することでコピーを回避する
mu := &sync.Mutex{}
// mu を複数の場所から参照する場合はポインタのまま渡す
// Bad:
b1 := bytes.Buffer{}
b2 := b1
レシーバーに指定する構造体のフィールドに、コピーされては困るフィールドが含まれている場合、一般的にポインタレシーバーにするべきである。
// Good:
type Record struct {
buf bytes.Buffer
// 他のフィールドは省略
}
func New() *Record {...}
func (r *Record) Process(...) {...}
func Consumer(r *Record) {...}
下記のように、値レシーバーにしてしまうと、r.bufフィールドがコピーされる結果になってしまう。
// Bad:
type Record struct {
buf bytes.Buffer
// 他のフィールドは省略
}
func (r Record) Process(...) {...} // r.buf のコピーが作られてしまう
func Consumer(r Record) {...} // r.buf のコピーが作られてしまう
Don't panic - パニックはNG
panic()を使わず、通常のエラーハンドリングをするか、return errしましょう。
mainパッケージ内で、プログラムを強制終了させなければならないような、状況になった場合(configが間違っている場合など)は、log.Exit()を使ってプログラムを終了させましょう。
Must functions - 失敗時にプログラムを停止させる関数について
失敗したらプログラム自体を終了させた方が良いような、セットアップヘルパー関数には、mustという単語をつけましょう。また、このような関数はユーザーの入力値エラーなどをハンドリングするようなケースではなく、必ずプログラムが立ち上がるタイミングで実行されるようにしましょう。
// Good:
func MustParse(version string) *Version {
v, err := Parse(version)
if err != nil {
log.Fatalf("MustParse(%q) = _, %v", version, err)
}
return v
}
// パッケージレベルの「定数」。`Parse`を使いたい場合は`init`で値を設定する必要があります。
var DefaultVersion = MustParse("1.2.3")
テーブル駆動テスト(テストケースをスライスにまとめ、ループで回すパターン)のように、正解値である構造体を作る際などでも活用することが可能である。
// Good:
func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any {
t.Helper()
any, err := anypb.New(m)
if err != nil {
t.Fatalf("MustMarshalAny(t, m) = %v; want %v", err, nil)
}
return any
}
func TestCreateObject(t *testing.T) {
tests := []struct{
desc string
data *anypb.Any
}{
{
desc: "my test case",
// テーブル駆動テストのケース内で直接値を生成しています。
data: mustMarshalAny(t, mypb.Object{}),
},
// ...
}
// ...
}
プログラムを終了させるような可能性のある関数は、基本的に下記の条件に当てはまるような関数では導入しない方が良い。
- エラーを確実に捕捉するのが難しい
- エラーをチェックすべきコンテキスト(例:リクエストハンドラの実装箇所など)
- 通常のエラーハンドリングが可能な場所
// Bad:
func Version(o *servicepb.Object) (*version.Version, error) {
// Must関数を使わず、エラーを返すべきです。
v := version.MustParse(o.GetVersionString())
return dealiasVersion(v)
}
Goroutine lifetimes - Goroutineのライフタイムについて
goroutineを生成する際は、そのgoroutineがいつ・どのように終了するかを明確にする必要があります。
goroutineは、チャネルの送受信でブロックされることによりリークする可能性があります。ガベージコレクタは、他のgoroutineがそのチャネルへの参照を保持していなくても、チャネルでブロックされているgoroutineを終了させることはありません。
goroutineがリークしない場合でも、不要になったgoroutineをそのまま放置すると、微妙で診断しにくい問題を引き起こす可能性があります。
- クローズ済みのチャネルに送信するとpanicが発生する。
- 「結果が不要になった後」に使用中の入力を変更すると、データ競合につながる。
- goroutineを任意の期間放置すると、予測不能なメモリ使用量を招く。
並行処理コードは、goroutineのライフタイムが明白になるように書くべきです。通常、同期関連のコードを関数スコープ内に閉じ込め、ロジックを同期関数に切り出すことでこれを実現します。それでも並行性が明白でない場合は、goroutineがいつ・なぜ終了するのかをドキュメント化することが重要です。
context.Contextを適切に使用したコードでは、goroutineのライフタイムが明確になることが多いです。
// Good:
func (w *Worker) Run(ctx context.Context) error {
var wg sync.WaitGroup
for item := range w.q {
// processはコンテキストがキャンセルされた時点で必ず終了します。
wg.Add(1)
go func() {
defer wg.Done()
process(ctx, item)
}()
}
wg.Wait() // 生成したgoroutineがこの関数より長く生存しないようにします。
return nil
}
// Bad:
func (w *Worker) Run() {
for item := range w.q {
// goroutineがいつ終了するか不明確で、リークやデータ競合の原因になりうる
go process(item)
}
}
Interfaces - Interfaceについて
Goのインターフェースは一般的に、インターフェースを実装するパッケージではなく、インターフェース型の値を利用するパッケージに属するべきです。
実装パッケージは具象型(通常はポインタまたは構造体)を返すべきです。これにより、大規模なリファクタリングを必要とせずに実装に新しいメソッドを追加できます。
// Good:
package consumer // consumer.go
type Thinger interface { Thing() bool }
func Foo(t Thinger) string { ... }
// Good:
package consumer // consumer_test.go
type fakeThinger struct{ ... }
func (t fakeThinger) Thing() bool { ... }
...
if Foo(fakeThinger{...}) == "x" { ... }
一方、下記のようにプロデューサー(実装側)パッケージでインターフェースを定義してエクスポートするパターンは推奨されません。
// Bad:
package producer
type Thinger interface { Thing() bool }
type defaultThinger struct{ ... }
func (t defaultThinger) Thing() bool { ... }
func NewThinger() Thinger { return defaultThinger{ ... } }
代わりに、プロデューサーパッケージは具象型を返すようにしましょう。
// Good:
package producer
type Thinger struct{ ... }
func (t Thinger) Thing() bool { ... }
func NewThinger() Thinger { return Thinger{ ... } }
また、使用前にインターフェースを定義しないことが重要です。現実的な使用例がなければ、インターフェースが本当に必要かどうか、また含めるべきメソッドが何かを判断することはできません。
その他のインターフェースに関するルールは以下の通りです。
- テスト用のフェイク実装(テストダブル)を、インターフェースを利用するAPIからエクスポートしてはいけません。テストヘルパーは、テスト対象のコードと同じパッケージに含めるか、別の
testutilパッケージに分けましょう。 - パッケージの利用者が異なる型を渡す必要がない場合は、インターフェース型のパラメータを使用しないようにしましょう。
- パッケージの利用者が必要としないインターフェースはエクスポートしないようにしましょう。
Generics - Genericsについて
ジェネリクス(正式には「型パラメータ(Type Parameters)」と呼ばれる)は、ビジネス要件を満たす場合に使用が許可されます。多くのアプリケーションでは、スライス、マップ、インターフェースなどの従来のアプローチで十分に機能するため、ジェネリクスの導入には「最小メカニズムの原則(least mechanism)」、すなわち目的を達成するために最もシンプルな仕組みを選ぶべきという原則を適用して慎重に検討すべきです。
不要な抽象化を避ける
メンバー要素の型に依存しないアルゴリズムやデータ構造を実装しているからという理由だけで、ジェネリクスを使用してはいけません。実際に1つの型でしかインスタンス化されない場合は、まずその特定の型を使ったコードから始めましょう。
DSLの発明にジェネリクスを使わない
ジェネリクスの型パラメータを駆使してGo標準とは異なる独自の記法(ドメイン固有言語、DSL)を作ってはいけません。特に、読み手に大きな負担を強いるエラーハンドリングフレームワークの導入は避けましょう。テストにおいても、ジェネリクスを使ったアサーションライブラリやフレームワークを作ることは避けましょう。有用でないテスト失敗メッセージの原因になります。
一般的なガイドライン
- 複数の型が有用な統一インターフェースを共有する場合は、そのインターフェースを使った解決策を検討する。ジェネリクスは必要ないかもしれない。
-
any型と大量の型スイッチに頼るのではなく、ジェネリクスの利用を検討する。
Pass values - 値渡しについて
数バイトを節約するためだけに、関数の引数にポインタを渡してはいけません。関数が引数xを常に*xとしてしか読み取らないのであれば、その引数はポインタにすべきではありません。
よくある不要なポインタ渡しの例として、文字列やインターフェース値があります。これらは固定サイズなので、そのまま値渡しできます。
// Good:
func Process(s string) error { ... }
func Handle(r io.Reader) error { ... }
// Bad:
func Process(s *string) error { ... }
func Handle(r *io.Reader) error { ... }
大きな構造体やサイズが増大する可能性のある構造体については、ポインタ渡しが適切です。
特に、Protocol Bufferメッセージはポインタで扱うべきです。Protocol Bufferの型はproto.Messageインターフェースを満たすためにポインタ型を必要とし、メッセージのサイズが非常に大きくなったり、時間とともに成長したりする可能性があるためです。
// Good:
func Update(msg *MyProtoMessage) error { ... }
Receiver type - レシーバーに指定する型について
メソッドのレシーバーに値レシーバーとポインタレシーバーのどちらを使うかは、パフォーマンスではなく正確性に基づいて判断すべきです。以下に判断基準を示します。
値レシーバーを使う場合:
- スライスを再スライスしないメソッド
// Good:
type Buffer []byte
func (b Buffer) Len() int { return len(b) }
- マップ、関数、チャネルをレシーバーにする場合
-
intやstringのような組み込み型で、変更を必要としない場合 - 小さな配列や構造体など、自然に値として扱える型
// Good:
func (t Time) Add(d Duration) Time { ... }
ポインタレシーバーを使う場合:
- メソッドがレシーバーを変更(mutate)する場合
// Good:
type Counter int
func (c *Counter) Inc() { *c++ }
-
sync.Mutexのような安全にコピーできないフィールドを含む構造体
// Good:
type Counter struct {
mu sync.Mutex
total int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.total++
}
- 大きな構造体や配列(効率のため)
- レシーバーが構造体または配列で、その要素にミューテートされる可能性のあるポインタを含む場合
- レシーバーを変更する他の関数やメソッドと並行して実行される可能性があるメソッド
迷ったらポインタレシーバーを選択しましょう。
また、一般的なガイドラインとして、ある型のメソッドは全てポインタレシーバーか全て値レシーバーに統一することが望ましいです。
Switch and break - Switch文について
Goのswitch文では、各caseの末尾で自動的にbreakされます。C言語のような明示的なbreakは不要であり、冗長なので書かないでください。
// Good:
switch x {
case "A", "B":
buf.WriteString(x)
case "C":
// 別の箇所で処理される
default:
return fmt.Errorf("unknown value: %q", x)
}
// Bad:
switch x {
case "A", "B":
buf.WriteString(x)
break // 不要
case "C":
break // 不要
default:
return fmt.Errorf("unknown value: %q", x)
}
C言語のようにフォールスルー(次のcaseに処理を流す)動作をさせたい場合は、fallthroughキーワードを明示的に使用します。
また、switch文がforループの内部にある場合、breakはswitchのみを抜け、ループは抜けない点に注意が必要です。ループを抜けたい場合はラベル付きbreakを使用します。
loop:
for {
switch x {
case "A":
break loop // forループを抜ける
}
}
Synchronous functions - 同期関数について
同期関数は結果を直接返し、コールバックやチャネル操作をすべて完了してからreturnします。非同期関数よりも同期関数を優先すべきです。
同期関数には以下のメリットがあります。
- goroutineを呼び出し内に局所化できるため、ライフタイムの推論が容易になり、リークやデータ競合を避けやすくなる
- テストが容易になる。入力を渡して出力をチェックするだけでよく、ポーリングや同期の仕組みが不要になる
- 呼び出し側が必要に応じて別のgoroutineで関数を呼び出すことで並行性を追加できる
一方で、呼び出し側から不要な並行性を取り除くことは非常に困難(場合によっては不可能)です。
// Good: 同期関数
func Lookup(key string) (Value, error) {
// ...キーに対応する値を直接返す
}
// 呼び出し側が必要に応じてgoroutineで並行化できる
go func() {
val, err := Lookup(key)
// ...
}()
// Bad: 不要な非同期設計
func Lookup(key string) <-chan Value {
ch := make(chan Value)
go func() {
v := compute(key) // 値を取得・計算する処理
ch <- v
}()
return ch
}
Type aliases - type aliasについて
新しい型を定義するには 型定義 を使用します。
// 型定義(新しい型を作る)
type UserID int
一方、型エイリアス は主にパッケージのマイグレーション(移行)時に使用するものであり、それ以外の用途ではまれにしか必要とされません。
// 型エイリアス(マイグレーション用途のみ)
type LegacyName = NewName
型エイリアスを必要としない場面で使用しないでください。型エイリアスの主な用途は、パッケージを新しいソースコードの場所に移行する際の支援に限定されます。
Use %q - fmt.Printfにおける%qについて
Goのfmtパッケージにおける%qフォーマット動詞は、文字列をダブルクォートで囲んで出力します。人間が読む出力で、入力値が空文字やコントロール文字を含む可能性がある場合に特に便利です。
%qを使うと、空文字列が""として明確に表示されるため、何も表示されずに見逃してしまう心配がなくなります。
// Good:
fmt.Printf("value %q looks like English text", someText)
// Bad:
fmt.Printf("value \"%s\" looks like English text", someText)
fmt.Printf("value '%s' looks like English text", someText)
人間向けの出力では、手動でクォートを付けるよりも%qを使うことをお勧めします。
Use any - any型について
Go 1.18でanyがinterface{}のエイリアスとして導入されました。新しいコードではinterface{}の代わりにanyを使用することが推奨されます。anyはinterface{}と完全に等価であり、より簡潔です。
// Good:
func Process(data any) error { ... }
// Bad:(古いスタイル)
func Process(data interface{}) error { ... }
Common libraries
Flags - フラグについて
Goプログラムではflagパッケージを使ってコマンドラインフラグを定義します。フラグの命名規則として、フラグ名にはアンダースコア(snake_case)を使い、フラグの値を保持するGo変数名にはキャメルケース(MixedCaps)を使います。
// Good:
var (
pollInterval = flag.Duration("poll_interval", time.Minute,
"Interval to use for polling.")
)
// Bad:
var (
poll_interval = flag.Int("pollIntervalSeconds", 60,
"Interval to use for polling in seconds.")
)
フラグの定義に関するルールは以下の通りです。
- フラグは
package main内でのみ定義します - 汎用パッケージはコマンドラインフラグではなく、Go APIを通じて設定できるようにします
- ライブラリのインポートが副作用として新しいフラグをエクスポートしないようにします
- フラグのグローバル変数は、imports直後に独自の
varグループとして配置します
Logging - ロギングについて
Googleの内部Go開発では標準ライブラリのlogパッケージとは異なる独自のlogパッケージが使われています(オープンソース版はglog)。標準ライブラリとは異なり、log.Panic関数は存在しない点に注意してください。
プログラムの終了に関しては、以下の使い分けがあります。
| 関数 | 動作 |
|---|---|
log.Fatal |
スタックトレース付きでプログラムを終了 |
log.Exit |
スタックトレースなしでプログラムを停止 |
フォーマットが不要な場合は、非フォーマット版の関数を使うと良いでしょう。log.Info(v)はlog.Infof("%v", v)と等価です。
// Good: フォーマット不要ならシンプルな方を使う
log.Info(value)
// Bad: 不要なフォーマット指定
log.Infof("%v", value)
Contexts - コンテキストについて
context.Contextは、セキュリティ資格情報、トレース情報、デッドライン、キャンセルシグナルなどをAPI境界やプロセス間で伝搬するために使われます。関数のシグネチャでは、常に最初のパラメータに配置します。
// Good:
func F(ctx context.Context, /* other arguments */) error {
// ...
}
以下の例外があります。
- HTTPハンドラでは
req.Context()からコンテキストを取得します - gRPCストリーミングメソッドでは、ストリームの
Context()メソッドからコンテキストを取得します - エントリーポイント関数(
main、init、テスト関数)ではcontext.Background()を使用します
// Good: テストではt.Context()を使用(Go 1.24+)
func TestSomething(t *testing.T) {
ctx := t.Context()
// ...
}
コンテキストはイミュータブル(不変)であるため、同じデッドライン、キャンセルシグナル、資格情報、親トレースなどを共有する複数の呼び出しに同じコンテキストを渡すことは安全です。
構造体のフィールドにコンテキストを追加してはいけません。 代わりに、コンテキストを必要とする各メソッドにパラメータとして渡しましょう。唯一の例外は、標準ライブラリやGoogle外部のサードパーティライブラリのインターフェースにシグネチャを合わせる必要があるメソッドの場合です。
// Good:
type Server struct {
// ...
}
func (s *Server) Process(ctx context.Context, req *Request) error {
// ...
}
// Bad:
type Server struct {
ctx context.Context // NG
// ...
}
独自のコンテキスト型を作成したり、関数シグネチャでcontext.Context以外のインターフェースを使用してはいけない。このルールに例外はない。
crypto/rand - 乱数生成について
鍵やトークンの生成など、セキュリティに関わる用途ではmath/randを絶対に使用してはいけません。たとえ一時的なものであっても、crypto/randを使用してください。
math/randはシードなしでは完全に予測可能であり、time.Nanoseconds()をシードに使ったとしても、ごくわずかなエントロピーしか持ちません。
// Good:
import (
"crypto/rand"
"fmt"
)
func Key() string {
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
log.Fatalf("Out of randomness, should never happen: %v", err)
}
return fmt.Sprintf("%x", buf)
// または hex.EncodeToString(buf)
// または base64.StdEncoding.EncodeToString(buf)
}
// Bad:
import "math/rand"
func BadKey() string {
return fmt.Sprintf("%x", rand.Intn(1000000))
}
セキュリティに関わる値の生成には、必ずcrypto/rand.Readを使用してください。
Useful test failures
テストの失敗メッセージは、テストのソースコードを読まなくても原因を診断できるようにするべきです。失敗メッセージには以下の情報を含めましょう。
- 何が失敗の原因となったか
- どの入力値がエラーを引き起こしたか
- 実際に得られた結果
- 期待していた結果
Assertion libraries - アサーションライブラリについて
テスト用のヘルパーとして「アサーションライブラリ」を作成してはいけません。アサーションライブラリとは、テスト内でのバリデーションと失敗メッセージの生成を組み合わせたライブラリのことです。これらは開発者体験を分断し、テスト関数内に存在する有用なコンテキストを失わせてしまいます。
// Bad:
var obj BlogPost
assert.IsNotNil(t, "obj", obj)
assert.StringEq(t, "obj.Type", obj.Type, "blogPost")
assert.IntEq(t, "obj.Comments", obj.Comments, 2)
assert.StringNotEq(t, "obj.Body", obj.Body, "")
代わりに、cmpやfmtといった標準的なライブラリを使用しましょう。
// Good:
var got BlogPost
want := BlogPost{
Comments: 2,
Body: "Hello, world!",
}
if !cmp.Equal(got, want) {
t.Errorf("Blog post = %v, want = %v", got, want)
}
ドメイン固有の比較ヘルパーが必要な場合は、*testing.Tを受け取るのではなく、値やエラーを返す関数として実装しましょう。
// Good:
func postLength(p BlogPost) int { return len(p.Body) }
func TestBlogPost_VeritableRant(t *testing.T) {
post := BlogPost{Body: "I am Gunnery Sergeant Hartman, your senior drill instructor."}
if got, want := postLength(post), 60; got != want {
t.Errorf("Length of post = %v, want %v", got, want)
}
}
このように、ヘルパー関数が値を返すようにすることで、テスト関数内で標準的な失敗メッセージのパターンを使用でき、テストの可読性が向上します。
Identify the function - テスト対象関数の特定について
ほとんどのテストにおいて、失敗メッセージにはテスト関数名から明らかであっても、失敗した関数の名前を含めるべきです。具体的には、got %v, want %v ではなく YourFunc(%v) = %v, want %v という形式を使いましょう。
Identify the input - エラーの原因となったinputの特定について
ほとんどのテストにおいて、失敗メッセージには関数の入力値を含めるべきです。入力値が短い場合はそのまま表示しましょう。入力値が大きい場合やわかりにくい場合は、テストケースにわかりやすい名前をつけ、その名前をエラーメッセージの一部として出力しましょう。
Got before want - 実際に得られた結果は先に表示せよ
テスト出力では、関数が実際に返した値を先に表示し、その後に期待値を表示するべきです。標準的なフォーマットは YourFunc(%v) = %v, want %v です。「actual」と「expected」ではなく、「got」と「want」という用語を使いましょう。
差分を表示する場合は方向性を明示的に示しましょう。
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Foo() (-want +got):\n%s", diff)
}
Full structure comparisons - 構造体の比較について
関数がstruct(またはスライス、配列、マップなどの複数フィールドを持つデータ型)を返す場合、フィールドごとに手動で比較するテストコードは避けましょう。代わりに、期待するデータを構築し、ディープ比較を使って直接比較しましょう。
構造体の近似的な等価性の比較が必要な場合や、比較できないフィールド(例: io.Reader)が含まれる場合は、cmpopts.IgnoreInterfacesなどのオプションを使ってcmp.Diffやcmp.Equalをカスタマイズしましょう。
Tip: 関数が複数の返り値を持つ場合、わざわざstructにラップして比較する必要はありません。返り値を個別に比較しましょう。
// Good:
val, multi, tail, err := strconv.UnquoteChar(`\"Fran & Freddie's Diner\"`, '"')
if err != nil {
t.Fatalf(...)
}
if val != '"' {
t.Errorf(...)
}
if multi {
t.Errorf(...)
}
if tail != `Fran & Freddie's Diner"` {
t.Errorf(...)
}
Compare stable results - 安定した結果の比較について
自分の管理下にないパッケージの出力の安定性に依存した比較は避けましょう。フォーマットされた文字列やシリアライズされたバイト列の出力が安定していると仮定するのは一般的に安全ではありません。
例えば、json.Marshalは過去にも出力するバイト列が変更されたことがあります。JSON文字列に対して文字列の等価性テストを行うとテストが壊れる可能性があるため、JSONの内容をパースして意味的に等価かどうかを比較しましょう。
Keep going - 失敗しても突き進め
テストは、失敗した後もできるだけ長く実行を続け、1回の実行で全ての失敗箇所を出力するべきです。関数の出力の複数のプロパティを比較する場合は、t.Fatal よりも t.Error を使いましょう。
// Good:
gotMean, gotVariance, err := MyDistribution(input)
if err != nil {
t.Fatalf("MyDistribution(%v) returned unexpected error: %v", input, err)
}
if diff := cmp.Diff(wantMean, gotMean); diff != "" {
t.Errorf("MyDistribution(%v) mean (-want +got):\n%s", input, diff)
}
if diff := cmp.Diff(wantVariance, gotVariance); diff != "" {
t.Errorf("MyDistribution(%v) variance (-want +got):\n%s", input, diff)
}
t.Fatal の使用が適切なのは、予期しない状態(エラーなど)が発生し、後続の失敗が無意味またはミスリーディングになる場合です。
テーブル駆動テストでサブテスト(t.Run)を使用している場合は、t.Error と continue の代わりに t.Fatal を使いましょう。t.Fatal はサブテスト内ではそのサブテストだけを終了させ、次のテストケースの実行に進むため、テストの「Keep going」原則に反しません。
// Good:
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := Foo(tc.input)
if err != nil {
t.Fatalf("Foo(%v) returned unexpected error: %v", tc.input, err)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Foo(%v) mismatch (-want +got):\n%s", tc.input, diff)
}
})
}
Equality comparison and diffs - 等価比較と差分について
cmpパッケージを使うと、==では適切に処理できないスライスなどの複雑なデータ構造を比較できます。等価比較にはcmp.Equalを、人が読みやすい差分の取得にはcmp.Diffを使用しましょう。
// Good:
want := &Doc{
Type: "blogPost",
Comments: 2,
Body: "This is the post body.",
Authors: []string{"isaac", "albert", "emmy"},
}
if !cmp.Equal(got, want) {
t.Errorf("AddPost() = %+v, want %+v", got, want)
}
Protocol Bufferメッセージの比較にはprotocmp.Transformオプションを渡す必要があります。これは、Protocol Bufferの構造体が非公開(unexported)フィールドを含むため、通常の比較では正しく動作しないからです。
// Good:
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("Foo() returned unexpected difference (-want +got):\n%s", diff)
}
reflect.DeepEqualは非公開フィールドまで含めて比較してしまうため、内部実装の変更でテストが壊れやすく、等価性チェックに使用すべきではありません。
Level of detail - 詳細のレベルについて
ほとんどのGoテストに適した従来の失敗メッセージ形式は YourFunc(%v) = %v, want %v です。ただし、より多くの詳細が必要な場合もあります。
- 複雑なインタラクションを実行するテストでは、そのインタラクションも記述します
- データが複雑な構造体である場合、重要な部分のみを説明することは許容されますが、データを過度に隠さないようにします
テスト入出力を明確に再現するためのヒントを紹介します。
- 文字列データを表示する場合、
%qを使うと値の重要性が強調され、不正な値を見つけやすくなります - 小さなstructを表示する場合、
%vよりも%+vの方がフィールド名も表示されるため有用です
Print diffs - 比較結果のプリント方法について
関数が大きな出力を返す場合、返された値と期待値の両方を表示するのではなく、差分を表示しましょう。cmp.Diffが推奨されます。
失敗メッセージには差分の方向を説明するテキストを追加しましょう。(want, got)の順で渡す場合は (-want +got) のような記述が適切です。
// Good:
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("SomeFunction() mismatch (-want +got):\n%s", diff)
}
差分は複数行にわたるため、差分を表示する前に改行を入れましょう。
Test error semantics - 関数の返すエラーの種類チェックについて
ユニットテストで文字列比較を使って特定のエラーが返されることをチェックする場合、エラーメッセージが書き換えられるとテストが壊れやすくなります。文字列比較でエラーの種類を判定するのは避けましょう。
エラーが意味的に他のエラーと一致するかをテストしたい場合は、errors.Isやcmpopts.EquateErrorsを使ったcmpを検討しましょう。
// Good:
err := f(test.input)
if gotErr := err != nil; gotErr != test.wantErr {
t.Errorf("f(%q) = %v, want error presence = %v", test.input, err, test.wantErr)
}
Test structure
Subtests - サブテストについて
Go標準のテストライブラリにはサブテストを定義する機能があります。サブテストを使うことで、セットアップとクリーンアップの柔軟性、並列処理の制御、テストフィルタリングが可能になります。特にテーブル駆動テストでは有用です。
サブテストは、他のテストケースの実行や初期状態に依存してはいけません。サブテストはgo test -runフラグを使って個別に実行できることが期待されるためです。
サブテスト名について
サブテスト名は、テスト出力で読みやすく、テストフィルタリングでコマンドラインから使いやすいものにしましょう。テストランナーはスペースをアンダースコアに置換するため、スペースや特殊文字(特にスラッシュ)の使用は避けましょう。
// Good:
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
got := Translate(d.srcLang, d.dstLang, d.srcText)
if got != d.wantDstText {
t.Errorf("%s\nTranslate(%q, %q, %q) = %q, want %q",
d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText)
}
})
}
// Bad:
// 冗長すぎる・スラッシュはコマンドラインで問題を起こす
t.Run("check that there is no mention of scratched records or hovercrafts", ...)
t.Run("AM/PM confusion", ...)
Table-driven tests - テーブル駆動テストについて
多くの異なるテストケースを同様のテストロジックでテストできる場合は、テーブル駆動テストを使いましょう。
// Good:
func TestCompare(t *testing.T) {
compareTests := []struct {
a, b string
want int
}{
{"", "", 0},
{"a", "", 1},
{"", "a", -1},
{"abc", "abc", 0},
}
for _, test := range compareTests {
got := Compare(test.a, test.b)
if got != test.want {
t.Errorf("Compare(%q, %q) = %v, want %v", test.a, test.b, got, test.want)
}
}
}
テストテーブルのインデックスをテストの命名の代わりに使用してはいけません。どのテストケースが失敗しているか特定するために行を数えなければならない状況は望ましくありません。
// Bad:
for i, d := range tests {
if strings.ToUpper(d.input) != d.want {
t.Errorf("Failed on case #%d", i) // インデックスではなく内容を示せ
}
}
Tip: テストテーブルの構造体フィールドには、
input、want、wantErrのような説明的な名前を付けましょう。テストケースが多くの縦幅を占める場合(20〜30行以上)や、隣接するフィールドが同じ型の場合は、フィールド名を明示することで可読性が大幅に向上します。
Tip: テストケースに関係のないゼロ値フィールドは省略できます。これはLiteral formattingのルールと同様です。どのフィールドがそのテストケースに関連しているかが一目でわかるようになります。
// Good:
tests := []struct {
slice []string
separator string
skipEmpty bool
want string
}{
{
slice: []string{"a", "b", ""},
separator: ",",
want: "a,b,",
},
{
slice: []string{"a", "b", ""},
separator: ",",
skipEmpty: true, // ゼロ値でないフィールドのみ明示
want: "a,b",
},
}
Test helpers - テストヘルパー関数について
テストヘルパーは、セットアップやクリーンアップのタスクを実行する関数です。*testing.Tを受け取る場合は、t.Helperを呼び出して、テストヘルパー内の失敗をヘルパーの呼び出し元の行に帰属させましょう。
// Good:
func TestSomeFunction(t *testing.T) {
golden := readFile(t, "testdata/golden-result.txt")
// ... goldenファイルに対してテストを実行 ...
}
func readFile(t *testing.T, filename string) string {
t.Helper()
contents, err := runfiles.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
return string(contents)
}
context.Contextを第1引数に置く慣習は、テストヘルパーにも適用されます。context.Contextと*testing.Tの両方を受け取る場合は、context.Contextを先に配置しましょう。
// Good:
func readTestFile(ctx context.Context, t *testing.T, path string) string {
t.Helper()
// ...
}
MustXYZパターン
セットアップの失敗時にテストを停止させるヘルパー関数は、MustXYZ(またはexportしない場合は mustXYZ)という命名規則に従います。これらはt.Fatalを使用してエラー時にテストを即座に終了させます。前提条件のセットアップが失敗した場合、後続のテスト実行は意味をなさないためです。
// Good:
func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any {
t.Helper()
any, err := anypb.New(m)
if err != nil {
t.Fatalf("mustMarshalAny(t, m) = %v; want nil error", err)
}
return any
}
Test package - テストパッケージについて
テストは、テスト対象のコードと同じパッケージ内または異なるパッケージに定義できます。
同じパッケージ内のテスト
- テストファイルを
foo_test.goに配置し、package fooを使用します - unexportedな識別子にアクセスできるため、より良いテストカバレッジが可能になります
異なるパッケージでのテスト
テスト対象のコードと同じパッケージでテストを定義することが適切でない場合は、_testサフィックスを持つパッケージ名を使用します。これはパッケージ名の「アンダースコア禁止」ルールの例外です。
// Good:
// 循環依存が発生する場合や統合テストの場合
package fireworks_test
import (
"fireworks"
"fireworkstestutil"
)
Use package testing - testingライブラリを使いましょう
Go標準ライブラリのtestingパッケージは、Googleのコードベースで許可されている唯一のテストフレームワークです。特に、アサーションライブラリやサードパーティのテストフレームワークは使用できません。
testingパッケージは、優れたテストを書くために必要な最小限かつ完全な機能セットを提供しています。
- トップレベルテスト
- ベンチマーク
- 実行可能な例(Example)
- サブテスト
- ログ出力
- 失敗とフェイタル失敗
Non-decisions
スタイルガイドはすべての事柄に対して肯定的な規則を列挙することも、意見を持たない事柄を全て列挙することもできません。とはいえ、このスタイルガイドのコミュニティで以前に議論されたものの、コンセンサスが得られなかったいくつかの事柄を以下に挙げます。
-
ゼロ値によるローカル変数の初期化:
var i intとi := 0は等価です。詳細は初期化のベストプラクティスを参照してください。 -
空の複合リテラル vs.
new/make:&File{}とnew(File)は等価です。同様にmap[string]bool{}とmake(map[string]bool)も等価です。詳細は複合宣言のベストプラクティスを参照してください。 -
cmp.Diff呼び出しにおける got・want の引数順序: ローカルで一貫性を保ち、失敗メッセージに凡例を含めるようにしてください。 -
フォーマットなし文字列における
errors.Newvs.fmt.Errorf:errors.New("foo")とfmt.Errorf("foo")は互換的に使用できます。
これらが再び問題になるような特殊な状況では、レビュワーが状況に応じてコメントすることもありますが、一般的にはその状況で好みのスタイルを自由に選択できます。