syncパッケージ完全レビューガイド:mutex設計と可読性の評価軸
syncパッケージ完全レビューガイド:mutex設計と可読性の評価軸
Go言語における並行プログラミングでは sync.Mutex
や sync.RWMutex
を使った排他制御が中核を担う。
コードが正しく動くかではなく、設計として安全で理解しやすいか がレビューでは重要になる。
mutexレビューは「排他さえできればよい」という動作確認では不十分。
責務の分離・ロック粒度・可読性・保守性が最重要の評価軸となる。
良い設計例:mutexを「何を守るのか」で構造化する
type Counter struct {
mu sync.Mutex
val int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.val++
c.mu.Unlock()
}
func (c *Counter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.val
}
- どの変数がmutexに守られているのかが明示されている
- mutexと守る変数が構造体内で物理的に隣接している
- ロック粒度が適切に制御されている
このように構造的に「mutex責務」を明示することが、レビューでまず評価すべき良い設計の典型。
問題のある実装例とレビュー指摘
mutexが何を守っているのか不明瞭な設計
var mu sync.Mutex
var data int
@Reviewermuが何を守っているのか明確でありません。保護対象の変数とmutexを構造体にまとめ、責務を明示化してください。
mutex定義がグローバルに浮いていると、保守時に「このmutexがどの変数を守っているのか?」が常に読み手の想像に依存してしまう。
ロック粒度が広すぎる設計
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
time.Sleep(100 * time.Millisecond)
c.val++
}
@Reviewer副作用のない処理までロック内に含まれています。ロック内は共有資源に直接アクセスする最小範囲に限定してください。
ロック粒度は「狭くしすぎると安全性を失い、広すぎると性能を失う」。
ロックすべき処理だけを対象に限定する意識が基本。
defer使用の適用判断が曖昧な設計
func (c *Counter) Complex() {
c.mu.Lock()
defer c.mu.Unlock()
a := compute()
b := doAnotherThing(a)
c.val += b
}
@Reviewerpanicの発生可能性が低い処理であれば、Unlockを明示的に記述しロック範囲を明示化してください。panic保護と可読性を両立できる設計に整理してください。
- panicの可能性が高い処理ならdeferが安全
- 副作用のない純粋計算処理なら明示的Unlockが読みやすい
スコープ可視性もレビュー評価の重要ポイントになる。
RWMutexの不適切な適用
type Data struct {
mu sync.RWMutex
val int
}
func (d *Data) Get() int {
d.mu.RLock()
defer d.mu.RUnlock()
return d.val
}
func (d *Data) Set(v int) {
d.mu.Lock()
defer d.mu.Unlock()
d.val = v
}
@Reviewer読み取り頻度と書き込み頻度のバランスが考慮されていません。RWMutex使用はGet/Set頻度の計測に基づき適用可否を判断してください。
RWMutexは「読みが多い場合にのみ有効」であり、書き込み競合が多い場合は逆に性能悪化を招く。
安易な最適化目的でのRWMutex採用は危険。
多重ロックによるデッドロックリスク
func A() {
x.mu.Lock()
y.mu.Lock()
defer y.mu.Unlock()
defer x.mu.Unlock()
}
func B() {
y.mu.Lock()
x.mu.Lock()
defer x.mu.Unlock()
defer y.mu.Unlock()
}
@ReviewerAとBでロック取得順序が逆転しています。このままではデッドロックが発生します。ロック取得順序を全体で統一してください。
ロック順序の一貫性は多重ロック設計の絶対条件。
複数mutexを組み合わせる際は必ず順序ルールをレビューで明文化させること。
デッドロックパターンのUML可視化
ロック設計におけるレビュー観点チェックリスト
チェック項目 | 説明 |
---|---|
ロック対象明示性 | mutexと守る変数が物理的に隣接しているか |
ロック粒度適正性 | 余計な処理がロック範囲に含まれていないか |
defer適用妥当性 | panic保護の要否に応じた適切適用か |
RWMutex選択根拠 | 読み書き頻度計測に基づくか |
ロック順序一貫性 | 多重ロック時に取得順序が統一されているか |
構造的責務分離 | 排他対象が構造単位で分離できているか |
さらにレビュー対象にすべき実務での注意点
- 「mutexはいつ初期化されるか」も実務バグ源になりやすい
- ゼロ値有効とはいえ、初期化時の責務明示も可読性を高める
- グローバルmutex設計は極力避け、構造体内部に閉じ込める
実運用で発生しやすいmutex設計事故事例
① グローバルmutex肥大化による競合悪化
var globalMu sync.Mutex
var dbMap = map[string]string{}
%% @Reviewer グローバルスコープのmutexは責務が肥大化しやすく、他の処理と競合が頻発します。構造体化して局所mutexに分離してください。
② 複数goroutineからのmapアクセスでのmutex不足
var dbMap = map[string]string{}
func write(key, val string) {
dbMap[key] = val
}
@Reviewermapはスレッドセーフではありません。複数goroutineからの同時書き込みにはmutex保護を追加してください。
mapはGo 1.19以前は競合アクセスでpanic、現在も実務ではrace発生源になりやすい。
sync.Map適用も安易に選ばず必ず責務を評価する。
まとめ:mutexレビューは構造読み解き訓練
mutexレビューは「動けばよい」ではなく「設計意図を明文化できるか」を読む。
責務の物理整理、スコープ設計、順序一貫性──全てが構造レビュー対象になる。
レビューアーは必ず以下を確認する:
- mutexと守る資源は近接構造化されているか
- 排他必要性は妥当か(実はmutex不要ケースも多い)
- panic保護・粒度調整・順序統一が設計内に組み込まれているか