Golangコーディング標準基本ガイドライン
リソースの解放にはdeferを使用する
ファイルやロックなどのリソースを解放する際には、deferを使用してください。
推奨されない方法:
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// 複数のリターンブランチがある場合、Unlockを忘れやすい
推奨される方法:
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// より読みやすい
deferのオーバーヘッドは非常に低いため、関数の実行時間がナノ秒レベルであることが証明できる場合を除いて避けるべきです。deferを使用して可読性を向上させることはコストが無視できるほど少ないため、特にdefer
のリソース消費が他の計算のリソース消費よりもずっと少ない単純なメモリアクセスを超える複雑なメソッドに適用されます。
チャネルのサイズは1または非バッファリングであるべき
チャネルのサイズは通常、1または非バッファリングであるべきです。デフォルトでは、チャネルのサイズは0の非バッファリングです。他のサイズを使用する場合は厳格にレビューする必要があります。サイズの決定方法、高負荷時の書き込み防止とブロック時の変更、システムロジックにどのような変更が起こるかを考慮する必要があります。
推奨されない方法:
// どんな状況にも対処できるはず!
c := make(chan int, 64)
推奨される方法:
// サイズ: 1
c := make(chan int, 1) // または
// 非バッファリングチャネル、サイズは0
c := make(chan int)
列挙型の開始値は1から始める
Goでの列挙型の標準的な導入方法は、カスタムタイプとiotaを使用するconstグループを宣言することです。変数のデフォルト値が0であるため、列挙型は通常、0以外の値で始めるべきです。
推奨されない方法:
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
推奨される方法:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
0から始まる場合(列挙型が0から始まる場合)、ゼロ値を使用することが適切な場合もあります。たとえば、ゼロ値が理想的なデフォルト動作である場合です。
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
atomicの使用
プリミティブ型(int32
、int64
など)の操作には、sync/atomicパッケージのatomic操作を使用してください。変数の読み取りまたは変更にatomic操作を使用を忘れることが容易です。
go.uber.org/atomicは、基礎となる型を隠してこれらの操作に型安全性を追加します。また、便利なatomic.Bool
型を含んでいます。
推奨されないアプローチ:
type foo struct {
running int32 // atomic
}
func (f *foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// 既に実行中…
return
}
// fooを開始
}
func (f *foo) isRunning() bool {
return f.running == 1 // 競合条件!
}
推奨されるアプローチ:
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// 既に実行中…
return
}
// fooを開始
}
func (f *foo) isRunning() bool {
return f.running.Load()
}
可変なグローバル変数を避ける
グローバル変数を変更しないように、依存性注入アプローチを使用してください。これは、関数ポインタや他の値の種類にも適用されます。
推奨されないアプローチ 1:
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
推奨されるアプローチ 1:
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
推奨されないアプローチ 2:
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
推奨されるアプローチ 2:
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
事前宣言された識別子の使用を避ける
Go言語仕様には、Goプロジェクトで使用しないであろう複数の事前宣言された識別子が記述されています。これらの事前宣言された識別子は、異なるコンテキストで名前を再利用すべきではなく、現在のスコープ(または任意のネストされたスコープ)で元の識別子を隠す可能性があり、コードの混乱を招く可能性があります。最善の場合は、コンパイラがエラーを発生させます。最悪の場合は、そのようなコードによって復旧が難しい潜在的なエラーが導入される可能性があります。
非推奨の慣習 1:
var error string
// `error`は暗黙的に組み込みの識別子を隠します
// または
func handleErrorMessage(error string) {
// `error`は暗黙的に組み込みの識別子を隠します
}
推奨される慣習 1:
var errorMessage string
// `error`は非影響の組み込み識別子を指します
// または
func handleErrorMessage(msg string) {
// `error`は非影響の組み込み識別子を指します
}
非推奨の慣習 2:
type Foo struct {
// これらのフィールドは厳密には隠さないが、`error`や`string`の再定義は曖昧になります。
error error
string string
}
func (f Foo) Error() error {
// `error`と`f.error`は視覚的に似ています
return f.error
}
func (f Foo) String() string {
// `string`と`f.string`は視覚的に似ています
return f.string
}
推奨される慣習 2:
type Foo struct {
// `error`と`string`は明示的になりました。
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
コンパイラは事前宣言された識別子を使用してもエラーを生成しませんが、go vet
のようなツールはこれらの問題やその他の暗黙的に関連する問題を正しく指摘します。
init()
の使用を避ける
できる限りinit()
の使用を避けるようにしてください。init()
が避けられないか、あるいは望ましい場合は、コードは次のようにする必要があります:
- プログラムの環境や呼び出しに関係なく完全性を確保する。
- 他の
init()
関数の順序や副作用に依存しないようにする。init()
の順序は明示的であるが、コードは変更する可能性があるため、init()
関数の関係はコードを壊れやすくし、エラーを生じさせる可能性がある。 - マシン情報、環境変数、作業ディレクトリ、プログラムのパラメータ/入力などのグローバルまたは環境の状態にアクセスしたり操作しない。
- ファイルシステム、ネットワーキング、システムコールなどのI/Oを行わない。
これらの要件を満たさないコードは、main()
呼び出し(またはプログラムのライフサイクルの他の場所)の一部として属するか、main()
自体の一部として書かれるべきです。特に、他のプログラムによって使用されることを意図しているライブラリは、「initマジック」を実行するのではなく、完全性に特に注意を払うべきです。
非推奨のアプローチ1:
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
推奨されるアプローチ1:
var _defaultFoo = Foo{
// ...
}
// または、よりテストしやすいように:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
非推奨のアプローチ2:
type Config struct {
// ...
}
var _config Config
func init() {
// 悪い: 現在のディレクトリに基づく
cwd, _ := os.Getwd()
// 悪い: I/O
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
推奨されるアプローチ2:
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// エラーを処理する
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// エラーを処理する
var config Config
yaml.Unmarshal(raw, &config)
return config
}
上記の考慮事項を踏まえると、いくつかの場合には init()
がより望ましいか必要となることがあります。例えば:
- 複雑な式の単一の代入として表現できない場合。
-
database/sql
、型レジストリなどの挿入可能なフック。
append
時にスライスの容量を指定することを優先する
スライスを初期化する際にmake()
に容量値を指定することを常に優先してください。
非推奨のアプローチ:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
推奨されるアプローチ:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
構造体のシリアライズでのフィールドタグの使用
JSON、YAML、または他のタグに基づくフィールド名をサポートするフォーマットにシリアライズする場合、関連するタグを注釈に使用するべきです。
非推奨の方法:
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
推奨される方法:
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Name を Symbol に安全に変更することができます。
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
理論的に構造体のシリアライズ形式は、異なるシステム間の契約です。構造体のシリアライズ形式(フィールド名を含む)を変更することはこの契約を破ります。タグでフィールド名を指定することで契約が明示的になり、また、フィールドのリファクタリングや名前変更による契約の意図せぬ違反を防ぐのに役立ちます。