Goの「panic: sync: unlock of unlocked mutex」を修正する

intermediate🔷 Go2026-05-14| Go 1.13以降、Linux / macOS / Windows、sync.MutexまたはSync.RWMutexを使用するプログラム全般

Error Message

panic: sync: unlock of unlocked mutex
#go#mutex#並行処理#sync#panic

何が起きたのか?

プログラムが以下のエラーでクラッシュしました:

panic: sync: unlock of unlocked mutex

goroutine 1 [running]:
sync.throw2({0x5a3b2c?, 0x0?})
        /usr/local/go/src/sync/mutex.go:35 +0x5c
sync.(*Mutex).unlockSlow(0xc000014090, 0xffffffff)
        /usr/local/go/src/sync/mutex.go:220 +0x38
sync.(*Mutex).Unlock(...)
        /usr/local/go/src/sync/mutex.go:193
main.main()
        /home/user/myapp/main.go:18 +0x5c

このクラッシュは回復不可能です。すでにアンロック済みの sync.Mutex に対して Unlock() を呼び出した瞬間、ミューテックスの内部状態が負の値に反転し、ランタイムがそれを検出してゴルーチンが終了します。やり直しは一切できません。recover() でラップして処理を続けることもできません。

クラッシュを引き起こす3つのパターン

ほとんどのケースは以下のいずれかのパターンに当てはまります:

1. 二重 Unlock

var mu sync.Mutex
mu.Lock()
mu.Unlock()
mu.Unlock() // ここでパニック — すでにアンロック済み

2. Lock がループ外、Unlock がループ内

var mu sync.Mutex
mu.Lock()
for i := 0; i < 3; i++ {
    doWork()
    mu.Unlock() // 1回目のイテレーション:正常。2回目:パニック。
}

3. defer と手動 Unlock の二重発火

func process(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock() // defer スタックにキュー登録

    // ... 何らかの処理 ...
    mu.Unlock() // 明示的な早期アンロック — この時点ではまだ問題ない
    // 関数が返る → defer が再度発火 → パニック
}

Go がエラーを無視しない理由

sync.Mutex はオーナーという概念を持たず、再入可能でもありません。追跡するのはロック済みかアンロック済みかという1ビットだけです。すでにアンロック済みのミューテックスに対して Unlock() を呼び出すのは、常にプログラマーのミスです。Go が安全に処理を続けられる曖昧なケースは存在しないため、続行しようとしません。

同じことが sync.RWMutex にも当てはまります。こちらには同じ問題の2つのバリエーションがあります:

panic: sync: unlock of unlocked mutex       // RLock/RUnlock の不一致
panic: sync: RUnlock of unlocked RWMutex    // RWMutex 固有のバリアント

まず不一致を見つける

レースディテクターから始めましょう。このパニック自体はデータ競合ではないため直接は検出できませんが、二重アンロックのバグはよく隣接する問題を伴います:

go run -race main.go
# または
go test -race ./...

次に、問題のミューテックスに対するすべての Lock/Unlock 呼び出しを追跡します:

grep -n 'mu\.Unlock\|mu\.Lock' yourfile.go

数を数えてください。すべての Lock に対して、早期リターンやエラー分岐を含むあらゆるコードパスで、正確に1つの Unlock が対応していなければなりません。カウントがずれているパスが1つでもあれば、プログラム全体がクラッシュするのに十分です。

確実に効く修正方法

ルール1 — Lock の直後に defer、それ以外の場所では使わない

関数ごとにスタイルを1つに統一してください。迷わないことが重要です。最もクリーンなパターン:

func safeProcess(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock() // 関数が終了したときに一度だけ発火

    doWork()
    // この関数内に他の mu.Unlock() 呼び出しは一切書かない
}

ルール2 — 早期アンロックが必要なら defer を完全に使わない

func processWithEarlyUnlock(mu *sync.Mutex, data []byte) error {
    mu.Lock()
    snapshot := copyData(data)
    mu.Unlock() // 明示的 — defer は一切なし

    // ロックを保持せずにスナップショットを処理
    return process(snapshot)
}

ルール3 — Lock と Unlock は同じループレベルに置く

// 誤り — Lock は1回、Unlock は複数回
mu.Lock()
for _, item := range items {
    process(item)
    mu.Unlock() // 最初のイテレーション後にクラッシュ
}

// 正しい — イテレーションごとにペアにする
for _, item := range items {
    mu.Lock()
    process(item)
    mu.Unlock()
}

ルール4 — 複雑な状態は構造体にラップする

ミューテックスが複数の操作を保護する場合、型の内部に隠蔽してください。各メソッドが小さく監査可能な Lock/Unlock ペアになります:

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

メソッドが小さければ、影響範囲も小さくなります。Lock/Unlock ペアが1つある5行のメソッドは、ほぼ間違いを犯しようがありません。

defer と手動 Unlock の二重発火を修正する

// 壊れているコード
func broken(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock() // return 時にも実行される
    // ...
    mu.Unlock() // 最初のアンロック — 無害に見える
    // 関数が返る → defer が発火 → パニック
}

// 修正版A — defer にすべてを任せる
func fixedA(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock()
    // ...
    // この行以降に明示的な Unlock は一切書かない
}

// 修正版B — defer を使わず、完全に明示的に書く
func fixedB(mu *sync.Mutex) error {
    mu.Lock()
    // ...
    if err := validate(); err != nil {
        mu.Unlock()
        return err
    }
    mu.Unlock()
    return nil
}

修正を検証する

以下の3つのコマンドを順番に実行してください:

# 基本的な動作確認
go run main.go

# レースディテクターによる確認
go run -race main.go

# レース検出付きでフルテストスイートを実行
go test -race -count=1 ./...

パニックが発生していた正確なコードパスを集中的にテストするテストを書いてください。例えば、SafeCounter が原因だった場合:

func TestNoDoubleLock(t *testing.T) {
    var wg sync.WaitGroup
    sc := &SafeCounter{}

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            sc.Increment()
        }()
    }
    wg.Wait()

    if sc.Value() != 1000 {
        t.Fatalf("expected 1000, got %d", sc.Value())
    }
}

1000個のゴルーチン、スリープなし、レースディテクター有効。これがクリーンに通れば、修正は有効です。

クイックリファレンス

  • 二重 Unlock → 余分な呼び出しを削除する。Lock 1回、Unlock 1回、以上。
  • defer + 手動 Unlock → 関数ごとに1つのスタイルに統一する。両方を混在させない。
  • Lock がループ外、Unlock がループ内 → Lock をループ本体の内側に移動する。
  • RWMutex バリアントRLock/RUnlockLock/Unlock は別々のペアです。混在させないこと。

Related Error Notes