Go のコードを「読みやすく・保守しやすく・安全に」書くための実践的な指針をまとめたものです。 公式の Effective Go、Go Code Review Comments、Google Go Style Guide の考え方をベースにしています。


目次

  1. 基本方針
  2. フォーマットとツール
  3. 命名規則
  4. パッケージ設計
  5. プロジェクト構成
  6. エラーハンドリング
  7. インターフェース
  8. 構造体と初期化
  9. 並行処理
  10. context の扱い
  11. ジェネリクス
  12. テスト
  13. パフォーマンス
  14. ロギング
  15. 依存関係管理
  16. よくあるアンチパターン
  17. チェックリスト

1. 基本方針

  • シンプルさを優先する:賢いコードより、誰が読んでもすぐ分かるコードを書く。
  • 明示的であること:暗黙の挙動(魔法)より、多少冗長でも明示的なコードを選ぶ。
  • ゼロ値を有用にする:型のゼロ値がそのまま使えるように設計する(例: sync.Mutex、bytes.Buffer)。
  • “A little copying is better than a little dependency”:小さな共通化のために依存を増やさない。
  • 早期リターン:ネストを深くせず、異常系を先に処理して正常系を左端に揃える。
// Good: 早期リターンで正常系が左端に揃う
func process(u *User) error {
    if u == nil {
        return errors.New("user is nil")
    }
    if !u.Active {
        return ErrInactive
    }
    return save(u)
}
 
// Bad: ネストが深い
func process(u *User) error {
    if u != nil {
        if u.Active {
            return save(u)
        } else {
            return ErrInactive
        }
    }
    return errors.New("user is nil")
}

2. フォーマットとツール

ツール用途
gofmt / goimportsコード整形・import 整理(必須。議論の余地なし)
go vet典型的なバグの静的検出
staticcheckより強力な静的解析
golangci-lint複数リンターの統合実行(CI 向け)
govulncheck依存パッケージの既知脆弱性チェック
go test -raceデータ競合の検出
  • エディタ保存時に goimports(または gopls)で自動整形する設定にする。
  • CI では go vet、golangci-lint、go test -race ./...、govulncheck ./... を回す。

.golangci.yml の最小例:

linters:
  enable:
    - errcheck
    - govet
    - staticcheck
    - unused
    - ineffassign
    - gosec
    - revive
    - errorlint
    - bodyclose

3. 命名規則

基本

  • MixedCaps を使う(snake_case は使わない)。公開は先頭大文字、非公開は先頭小文字。
  • 頭字語は大文字・小文字を揃える:URL, ID, HTTP → userID, ServeHTTP, parseURL(UserId, ParseUrl は NG)。
  • スコープが狭いほど短く:ループ変数は i、レシーバは s など。スコープが広いほど説明的に。

パッケージ名

  • 小文字・単数形・短く:user, http, strconv
  • util, common, helpers, misc のような意味の薄い名前は避ける。
  • パッケージ名と重複する名前を付けない(stutter を避ける)。
// Bad
package user
type UserService struct{}   // user.UserService
 
// Good
package user
type Service struct{}       // user.Service

ゲッター / インターフェース

  • ゲッターに Get を付けない:Owner()(GetOwner() ではない)。セッターは SetOwner()。
  • 1メソッドのインターフェースは -er で命名:Reader, Writer, Stringer。

レシーバ名

  • 型名の1〜2文字の略語を使い、型内で統一する。this や self は使わない。
func (c *Client) Do(req *Request) (*Response, error)

エラー

  • エラー変数は Err プレフィックス:ErrNotFound
  • エラー型は Error サフィックス:ValidationError

4. パッケージ設計

  • 機能(ドメイン)単位で分割する。models/, controllers/ のようなレイヤー単位の分割は循環依存を招きやすい。
  • 循環 import は許されないので、依存の方向を意識して設計する。
  • internal/ を活用する:外部に公開したくないコードは internal/ 配下に置くとコンパイラが外部からの import を禁止してくれる。
  • パッケージの公開 API は最小限に。公開すると後から変更しづらい。
  • init() の多用は避ける。暗黙の副作用はテストやデバッグを難しくする。
  • グローバル変数(特に可変なもの)は避け、依存は引数や構造体フィールドで渡す。

5. プロジェクト構成

公式の推奨は Organizing a Go module を参照。小さく始めて必要に応じて分割するのが基本です。

myapp/
├── go.mod
├── go.sum
├── main.go              # 小規模なら直下でOK
├── cmd/                 # 複数バイナリがある場合
│   ├── server/
│   │   └── main.go
│   └── cli/
│       └── main.go
├── internal/            # 外部非公開のアプリケーションコード
│   ├── user/
│   │   ├── service.go
│   │   ├── repository.go
│   │   └── service_test.go
│   └── order/
└── pkg/                 # 外部公開ライブラリ(必要な場合のみ)
  • main パッケージは薄く保ち、設定読み込み・依存の組み立て・起動だけを行う。
  • pkg/ は必須ではない。公開する意図がなければ internal/ で十分。
  • 最初から過剰なディレクトリ構成(“standard layout” の丸写し)にしない。

6. エラーハンドリング

基本原則

  • エラーは必ず処理する。_ で捨てるのは意図が明確な場合のみ(コメントを添える)。
  • エラーは戻り値の最後に置く。
  • エラーメッセージは小文字始まり・句読点なし(連結されるため)。
// Good
return fmt.Errorf("open config: %w", err)
 
// Bad
return fmt.Errorf("Failed to open config file.: %w", err)

ラップとコンテキスト付与

  • %w でラップして原因を保持し、呼び出し側が errors.Is / errors.As で判定できるようにする。
  • 「何をしようとして失敗したか」を付け加える。“failed to” は冗長なので省略してよい。
var ErrNotFound = errors.New("not found")
 
func (r *Repo) Find(id string) (*User, error) {
    u, err := r.db.Get(id)
    if err != nil {
        return nil, fmt.Errorf("find user %s: %w", id, err)
    }
    return u, nil
}
 
// 呼び出し側
if errors.Is(err, ErrNotFound) {
    // 404 を返す
}
 
var verr *ValidationError
if errors.As(err, &verr) {
    // フィールド情報を使う
}
  • err == ErrX での比較や err.Error() の文字列比較は避け、errors.Is / errors.As を使う。
  • 複数エラーをまとめる場合は errors.Join を使う。

一度だけ処理する

  • ログに出して、さらに return するのは NG(同じエラーが何度もログに出る)。ログに出すか返すか、どちらか一方。
// Bad
if err != nil {
    log.Printf("query failed: %v", err)
    return err
}
 
// Good
if err != nil {
    return fmt.Errorf("query: %w", err)
}

panic

  • panic は「プログラマのバグ」や「起動時の回復不能な設定ミス」に限定する。通常のエラーには使わない。
  • ライブラリは panic を外部に漏らさない。
  • Must 系関数(regexp.MustCompile など)はパッケージ変数の初期化など、失敗が即バグを意味する場面でのみ使う。

7. インターフェース

  • インターフェースは使う側(consumer)で定義する。実装側で先回りして定義しない。
  • 小さく保つ。1〜3メソッド程度が理想。大きなインターフェースは小さなものの合成で表現する。
  • “Accept interfaces, return structs”:引数はインターフェース、戻り値は具体型。
  • 実装が1つしかないのに「将来のため」にインターフェースを作らない(テストで差し替えが必要になった時点で作る)。
// 使う側のパッケージで、必要なメソッドだけを定義
package notifier
 
type userGetter interface {
    GetUser(ctx context.Context, id string) (*user.User, error)
}
 
type Notifier struct {
    users userGetter
}
 
func New(users userGetter) *Notifier {
    return &Notifier{users: users}
}
  • コンパイル時に実装を保証したい場合:
var _ io.Writer = (*MyWriter)(nil)
  • interface{} より any を使う(同義だが読みやすい)。ただし any の多用自体は型安全性を損なうので避ける。

8. 構造体と初期化

コンストラクタ

  • 初期化が必要なら NewXxx 関数を用意する。ゼロ値で使えるならコンストラクタは不要。

Functional Options パターン

オプションが多い場合の定番パターン:

type Server struct {
    addr    string
    timeout time.Duration
}
 
type Option func(*Server)
 
func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}
 
func NewServer(addr string, opts ...Option) *Server {
    s := &Server{addr: addr, timeout: 30 * time.Second}
    for _, opt := range opts {
        opt(s)
    }
    return s
}
 
srv := NewServer(":8080", WithTimeout(10*time.Second))

※ オプションが少なければ設定用構造体(Config)を渡す方がシンプルな場合も多い。

構造体リテラル

  • フィールド名を明示する(位置指定は型変更に弱い)。
// Good
u := User{Name: "Alice", Age: 30}
 
// Bad
u := User{"Alice", 30}

レシーバの選択(値 vs ポインタ)

  • 状態を変更する、構造体が大きい、sync.Mutex などコピー不可なフィールドを含む → ポインタレシーバ
  • 小さく不変な値型(time.Time のような) → 値レシーバ
  • 1つの型の中でレシーバの種類を混在させない(迷ったらポインタ)。

スライスとマップ

  • 要素数が分かっている場合は容量を事前確保する。
  • nil スライスと空スライスはほぼ同様に扱えるが、JSON では null と [] で違いが出る点に注意。
  • 外部から受け取ったスライス・マップを内部に保持する場合や返す場合は、必要に応じてコピーする(意図しない共有を防ぐ)。
users := make([]User, 0, len(ids))
m := make(map[string]int, len(keys))
 
// 防御的コピー
func (s *Store) SetItems(items []Item) {
    s.items = slices.Clone(items)
}

9. 並行処理

原則

  • “Don’t communicate by sharing memory; share memory by communicating.” ただし単純な共有状態の保護には sync.Mutex の方が素直なことも多い。適材適所で選ぶ。
  • goroutine を起動したら、終了方法を必ず決めておく。止められない goroutine はリークする。
  • 並行処理は必要になってから導入する。同期 API を提供し、並行化は呼び出し側に任せるのが基本。

goroutine のライフサイクル管理

sync.WaitGroup または golang.org/x/sync/errgroup で終了を待つ。

import "golang.org/x/sync/errgroup"
 
func fetchAll(ctx context.Context, urls []string) ([]Result, error) {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // 同時実行数の制限
 
    results := make([]Result, len(urls))
    for i, url := range urls {
        g.Go(func() error {
            r, err := fetch(ctx, url)
            if err != nil {
                return err
            }
            results[i] = r // インデックスごとに書き込むのでロック不要
            return nil
        })
    }
    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}

※ Go 1.22 以降、for ループ変数はイテレーションごとに新しく作られるため、i := i のようなコピーは不要。

Mutex

type Counter struct {
    mu sync.Mutex
    n  map[string]int
}
 
func (c *Counter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n[key]++
}
  • Mutex は保護対象のフィールドの直上に置く。
  • Mutex を含む構造体はコピーしない(go vet が検出してくれる)。
  • 読み取りが圧倒的に多い場合のみ sync.RWMutex を検討する。
  • 単純なカウンタやフラグは sync/atomic の型(atomic.Int64 など)も有効。

チャネル

  • チャネルを close するのは送信側。受信側が close してはいけない。
  • バッファサイズは原則 0 か 1。それ以上にする場合は根拠を持つ。
  • 関数シグネチャでは方向付きチャネル(<-chan T, chan<- T)を使い、意図を明確にする。
  • select では ctx.Done() を必ず監視し、キャンセル可能にする。
func worker(ctx context.Context, jobs <-chan Job, results chan<- Result) {
    for {
        select {
        case <-ctx.Done():
            return
        case j, ok := <-jobs:
            if !ok {
                return
            }
            results <- process(j)
        }
    }
}

その他

  • テストでは常に -race を付けて実行する。
  • 一度だけ実行したい初期化には sync.Once(または sync.OnceValue / sync.OnceFunc)を使う。

10. context の扱い

  • context.Context は関数の第1引数に ctx という名前で渡す。
  • 構造体のフィールドに保持しない(リクエストスコープを越えてしまう)。
  • nil の context を渡さない。不明な場合は context.TODO()、トップレベルでは context.Background()。
  • WithTimeout / WithCancel で得た cancel は必ず defer cancel() する。
  • context.WithValue はリクエストスコープのメタデータ(トレースID、認証情報など)に限定する。関数の必須パラメータを context で渡さない。キーには非公開の独自型を使う。
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    return s.repo.Find(ctx, id)
}
 
type ctxKey struct{}
 
func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, ctxKey{}, id)
}

11. ジェネリクス

  • まずは具体型で書き、同じロジックが複数の型で重複したらジェネリクスを検討する。
  • コレクション操作、データ構造(スタック・キューなど)、型安全なユーティリティに向いている。
  • メソッドの振る舞いだけを抽象化したいならインターフェースで十分なことが多い。
  • 標準ライブラリの slices, maps, cmp パッケージをまず活用する。
// 標準ライブラリで足りるケース
slices.Sort(xs)
idx := slices.Index(xs, target)
keys := slices.Sorted(maps.Keys(m))
m := max(a, b) // 組み込み関数(Go 1.21+)
 
// 自前で書く場合
func Map[T, U any](s []T, f func(T) U) []U {
    out := make([]U, 0, len(s))
    for _, v := range s {
        out = append(out, f(v))
    }
    return out
}
  • 型制約が複雑になりすぎたら、設計を見直すサイン。

12. テスト

テーブル駆動テスト

func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {name: "positive", a: 1, b: 2, want: 3},
        {name: "negative", a: -1, b: -2, want: -3},
        {name: "zero", a: 0, b: 0, want: 0},
    }
 
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            if got := Add(tt.a, tt.b); got != tt.want {
                t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

ポイント

  • 失敗メッセージは “got X, want Y” 形式で、入力値も含めると原因が分かりやすい。
  • 以降の検証が無意味になる場合のみ t.Fatal、それ以外は t.Error で継続する。
  • ヘルパー関数には t.Helper() を付けて、失敗行を呼び出し元で表示させる。
  • 後片付けは t.Cleanup()、一時ディレクトリは t.TempDir()、環境変数は t.Setenv()。
  • 構造体の比較には github.com/google/go-cmp/cmp の cmp.Diff が便利。
  • 外部依存はインターフェースで差し替える。モックライブラリより手書きのフェイクの方がシンプルなことが多い。
  • HTTP のテストには net/http/httptest を使う。
  • ブラックボックステストには package foo_test を使い、公開 API のみを検証する。
  • テストデータは testdata/ ディレクトリに置く(ビルド対象外になる)。
  • 入力の網羅性が重要な関数にはファジング(func FuzzXxx(f *testing.F))を検討する。
func newTestServer(t *testing.T) *Server {
    t.Helper()
    s := NewServer(":0")
    t.Cleanup(func() { s.Close() })
    return s
}

よく使うコマンド

go test ./...                       # 全テスト
go test -race ./...                 # 競合検出付き
go test -run TestAdd/positive ./... # 特定テストのみ
go test -cover ./...                # カバレッジ
go test -bench=. -benchmem ./...    # ベンチマーク

13. パフォーマンス

  • 推測するな、計測せよ。最適化の前に pprof とベンチマークでボトルネックを特定する。
  • まず読みやすいコードを書き、必要な箇所だけ最適化する。

主なテクニック

  • スライス・マップの容量を事前確保する(make([]T, 0, n))。
  • 文字列の連結には strings.Builder を使う。
  • ホットパスでは fmt.Sprintf より strconv の方が速い。
  • 頻繁に生成・破棄される一時オブジェクトには sync.Pool を検討する(計測してから)。
  • 大きな構造体を値渡しで大量にコピーしない。
  • defer のオーバーヘッドは現在ほぼ無視できるので、可読性を優先して使ってよい。

ベンチマーク

func BenchmarkConcat(b *testing.B) {
    for b.Loop() { // Go 1.24+。古いバージョンでは for i := 0; i < b.N; i++
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("x")
        }
        _ = sb.String()
    }
}

プロファイリング

go test -bench=. -cpuprofile=cpu.out -memprofile=mem.out
go tool pprof -http=:8080 cpu.out

サーバーアプリなら net/http/pprof を import して実行中にプロファイルを取得できる(本番では公開範囲に注意)。


14. ロギング

  • 標準ライブラリの log/slog(構造化ロギング) を使う。
  • メッセージ文字列にデータを埋め込まず、キー・バリューで属性として渡す。
  • ライブラリ側ではロガーを直接生成せず、注入できるようにする(またはログを出さずエラーを返す)。
  • パスワード・トークンなどの機密情報をログに出さない。
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))
 
logger.Info("user created",
    slog.String("user_id", u.ID),
    slog.Int("age", u.Age),
)
 
// リクエスト単位の共通属性
reqLogger := logger.With(slog.String("request_id", reqID))

15. 依存関係管理

  • Go Modules(go.mod / go.sum)を使い、両方をバージョン管理にコミットする。
  • go mod tidy で不要な依存を削除し、CI で差分がないことを確認する。
  • 依存は慎重に追加する。標準ライブラリで済むなら標準ライブラリを使う。
  • 依存の更新は定期的に行い、govulncheck で脆弱性をチェックする。
  • 開発用ツールのバージョン固定には go.mod の tool ディレクティブ(Go 1.24+)が使える。
go get example.com/pkg@latest   # 追加・更新
go mod tidy                     # 整理
go get -tool golang.org/x/tools/cmd/stringer  # ツールとして追加(Go 1.24+)

16. よくあるアンチパターン

アンチパターン問題点対策
エラーを _ で握りつぶす障害の原因が追えない必ず処理するか、理由をコメントで残す
ログ出力してからエラーを return同じエラーが重複してログに出るどちらか一方にする
止める手段のない goroutinegoroutine リークcontext や done チャネルで終了させる
context を構造体に保持スコープが曖昧になる引数で渡す
実装側で巨大なインターフェースを定義不要な結合、テストしづらい使う側で小さく定義する
util / common パッケージ何でも入る「ゴミ箱」化機能に応じた名前のパッケージに分ける
可変なグローバル変数テスト困難、競合の温床依存性注入
panic を通常のエラー処理に使う呼び出し側が回復できないerror を返す
http.Response.Body を閉じないコネクション・リソースリークdefer resp.Body.Close()
タイムアウトなしの http.Client無限に待ち続ける可能性Timeout を設定する(http.DefaultClient は無制限)
time.After を長寿命ループの select で使う古い Go ではタイマーが溜まりやすいtime.NewTimer / time.Ticker を再利用
err.Error() の文字列比較メッセージ変更で壊れるerrors.Is / errors.As
// HTTP クライアントの例
client := &http.Client{Timeout: 10 * time.Second}
 
resp, err := client.Do(req)
if err != nil {
    return fmt.Errorf("request: %w", err)
}
defer resp.Body.Close()

17. チェックリスト

コードレビュー時に確認する項目:

  • gofmt / goimports で整形されている
  • go vet と golangci-lint が通る
  • go test -race ./... が通る
  • すべてのエラーが処理されている(握りつぶしがない)
  • エラーに十分なコンテキストが付与され、%w でラップされている
  • 命名が Go の慣習に沿っている(頭字語、stutter なし、Get なし)
  • 起動した goroutine に終了手段がある
  • context.Context が第1引数で渡され、構造体に保持されていない
  • インターフェースは使う側で小さく定義されている
  • 公開 API は必要最小限で、ドキュメントコメントがある
  • リソース(ファイル、レスポンスボディ、ロックなど)が確実に解放されている
  • 外部通信にタイムアウトが設定されている
  • 機密情報がログやエラーメッセージに含まれていない
  • テストがテーブル駆動などで読みやすく、失敗時に原因が分かる

参考資料

0件のページ