Go: 慣用的で効率的なリファレンス
目次
1. エラーハンドリング {#errors}
// ❌ エラーを無視する
result, _ := os.Open(path)
// ✅ — 常にハンドリングする。エラーが明らかに無関係な場合のみ _ を使用
result, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
// ❌ 冗長なエラー変数
var err エラー
err := doA()
if err != nil { return err }
err = doB()
if err != nil { return err }
// ✅ — それぞれの :=/: は問題ない。これが慣用的なGo。"修正"しようとしないこと。
// 簡略化できること: if の本体が単一のreturn文である場合、1行にまとめる
if err := doA(); err != nil { return err }
if err := doB(); err != nil { return err }
// ❌ 付加価値のないカスタムエラー型
type MyError struct{ msg string }
func (e MyError) Error() string { return e.msg }
// ✅ — 呼び出し側が型を検査する必要がない限り、errors.New または fmt.Errorf を使用
var ErrNotFound = errors.New("not found")
return fmt.Errorf("lookup %q: %w", key, ErrNotFound)
エラーを %w(%v ではない)でラップし、呼び出し側が errors.Is / errors.As を使用できるようにする。
2. スライスとマップ {#slices}
// ❌ サイズが既知の場合でも、事前割り当てなしでスライスを成長させる
var result []string
for _, item := range items {
result = append(result, item.Name)
}
// ✅
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.Name)
}
// ❌ マップへの書き込み前に手動で存在確認を行う
if _, ok := m[key]; !ok {
m[key] = []string{}
}
m[key] = append(m[key], value)
// ✅ — nilスライスへのappendは有効なGoのコード
m[key] = append(m[key], value)
// ❌ 代入によるマップのコピー(参照をコピーするだけ)
copy := original
// ✅
copy := make(map[K]V, len(original))
for k, v := range original { copy[k] = v }
3. ゴルーチンとチャネル {#concurrency}
// ❌ ライフサイクル管理なしのファイアアンドフォーゲットゴルーチン
go doWork()
// ✅ — errgroup または WaitGroup を使用して完了を追跡
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
// ❌ バッファなしチャネルが不必要なゴルーチンブロックを引き起こす
ch := make(chan Result)
go func() { ch <- compute() }()
result := <-ch
// ✅ — 単一の結果の場合、バッファ付きチャネルを使用して、受信側が早期に終了した場合のゴルーチンリークを回避
ch := make(chan Result, 1)
go func() { ch <- compute() }()
result := <-ch
// ❌ ビジーウェイトのデフォルトケースを持つselect
for {
select {
case v := <-ch:
process(v)
default:
// スピン
}
}
// ✅ — 本当に非ブロッキングが必要な場合を除き、ブロッキングselect
for v := range ch {
process(v)
}
エラー収集を伴うファンアウトには golang.org/x/sync/errgroup を使用する。
4. 構造体とインターフェース {#structs}
// ❌ 巨大なインターフェース
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
// ... さらに10個のメソッド
}
// ✅ — 小さく、合成可能なインターフェース
type Getter interface { Get(key string) ([]byte, error) }
type Setter interface { Set(key string, val []byte) error }
type Storage interface { Getter; Setter }
// ❌ コンストラクタから具象構造体を返す(呼び出し側を実装に縛る)
func NewStore() *RedisStore { ... }
// ✅ — 複数の実装がある、または想定される場合はインターフェースを返す
func NewStore() Storage { return &RedisStore{...} }
// ❌ 小さな値型に対するポインタレシーバ
func (p *Point) X() float64 { return p.x }
// ✅ — 小さな不変型には値レシーバ
func (p Point) X() float64 { return p.x }
ルール: メソッドが状態を変更する場合、または構造体が大きい場合(3フィールド以上で、かつ重要なサイズ)はポインタレシーバ。それ以外は値レシーバ。
5. 関数とクロージャ {#functions}
// ❌ 単なる変数宣言の回避のために名前付き戻り値を使用
func divide(a, b float64) (result float64, err error) {
result = a / b
return
}
// ✅ — 名前付き戻り値は、遅延ミューテーションやドキュメンテーションの場合にのみ価値がある
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
// ❌ ループ変数をキャプチャするクロージャ(古典的なGoのバグ。Go 1.22+ で修正)
// 1.22未満: 各ゴルーチンが同じ i をキャプチャする
for i := 0; i < n; i++ {
go func() { use(i) }()
}
// ✅ (Go <1.22 — パラメータとして渡す)
for i := 0; i < n; i++ {
go func(i int) { use(i) }(i)
}
// Go 1.22+: ループ変数は各イテレーションでスコープされるため、元のコードは安全
6. Go特有のアンチパターン {#antipatterns}
| アンチパターン | 推奨パターン |
|---|---|
if err != nil { return err } を5回以上繰り返す | 許容範囲 — 慣用的なGoである |
予期されるエラーに対する panic | return err |
副作用のある init() | main またはコンストラクタでの明示的な初期化 |
ジェネリクスなしの interface{} / any | ジェネリクス(Go 1.18+)または型付きインターフェースを使用 |
| 保護するデータの隣にないMutexフィールド | mu を保護するフィールドの真上に配置 |
| チャネルのチャネル | 通常は設計過剰の兆候。再設計する |
テスト内の time.Sleep | 同期のために testing フックまたはチャネルを使用 |
| エクスポートされていないフィールドを持つエクスポートされた型(フィールドが全てを決める場合) | 全フィールドがエクスポートされた record スタイルの構造体 |
main 以外での log.Fatal | スタックの上位にエラーを返す |
制限事項
- これらは言語固有のガイドラインであり、全体的なアーキテクチャ上の決定をカバーするものではありません。
- 過度な圧縮は可読性を低下させる可能性があるため、判断を適用してください。