Sửa lỗi "panic: sync: unlock of unlocked mutex" trong Go

intermediate🔷 Go2026-05-14| Go 1.13+, Linux / macOS / Windows, mọi chương trình sử dụng sync.Mutex hoặc sync.RWMutex

Error Message

panic: sync: unlock of unlocked mutex
#go#mutex#concurrency#sync#panic

Chuyện gì vừa xảy ra?

Chương trình của bạn bị crash với lỗi:

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

Lỗi crash này không thể phục hồi. Ngay khi bạn gọi Unlock() trên một sync.Mutex đã được unlock trước đó, trạng thái nội bộ của mutex sẽ bị đảo thành âm, runtime phát hiện ra điều này và goroutine chết ngay — không có cơ hội thứ hai. Bạn không thể bọc nó trong recover() rồi tiếp tục được.

Ba cách phổ biến gây ra lỗi này

Hầu hết các trường hợp đều rơi vào một trong các mẫu sau:

1. Unlock hai lần (Double Unlock)

var mu sync.Mutex
mu.Lock()
mu.Unlock()
mu.Unlock() // panic ở đây — đã unlock rồi

2. Unlock bên trong vòng lặp trong khi Lock ở bên ngoài

var mu sync.Mutex
mu.Lock()
for i := 0; i < 3; i++ {
    doWork()
    mu.Unlock() // lần lặp đầu: ổn. lần thứ hai: panic.
}

3. defer kết hợp với Unlock thủ công bị gọi hai lần

func process(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock() // đã được đưa vào ngăn xếp defer

    // ... một số code ...
    mu.Unlock() // unlock thủ công sớm — đến đây vẫn ổn
    // hàm return → defer được gọi lần nữa → panic
}

Tại sao Go không bỏ qua lỗi này

sync.Mutex không có khái niệm owner và không hỗ trợ re-entrant. Nó chỉ theo dõi một bit duy nhất: đang khóa hay đã mở khóa. Gọi Unlock() trên một mutex đã được unlock luôn là lỗi lập trình — không có trường hợp mơ hồ nào mà Go có thể tiếp tục an toàn. Vì vậy Go không cố gắng làm vậy.

Điều tương tự áp dụng cho sync.RWMutex, với hai biến thể của cùng một vấn đề:

panic: sync: unlock of unlocked mutex       // mất cân bằng RLock/RUnlock
panic: sync: RUnlock of unlocked RWMutex    // biến thể đặc thù của RWMutex

Trước tiên hãy tìm chỗ mất cân bằng

Bắt đầu với race detector. Nó sẽ không bắt được panic chính xác này — đây không phải data race — nhưng các lỗi double-unlock thường đi kèm với các vấn đề lân cận:

go run -race main.go
# hoặc
go test -race ./...

Sau đó theo dõi mọi lệnh gọi Lock/Unlock trên mutex đang nghi ngờ:

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

Đếm chúng lại. Mỗi Lock có đúng một Unlock tương ứng — trên mọi nhánh code, kể cả các điểm return sớm và nhánh xử lý lỗi. Chỉ cần một đường dẫn có số lượng lệch là đủ để crash toàn bộ chương trình.

Các cách sửa thực sự hiệu quả

Quy tắc 1 — defer ngay sau Lock, và không ở đâu khác

Chọn một phong cách cho mỗi hàm. Đừng dùng lẫn lộn. Mẫu code sạch nhất:

func safeProcess(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock() // chỉ gọi một lần, khi hàm kết thúc

    doWork()
    // không có lệnh gọi mu.Unlock() nào khác trong hàm này — không bao giờ
}

Quy tắc 2 — cần unlock sớm? Bỏ defer đi hoàn toàn

func processWithEarlyUnlock(mu *sync.Mutex, data []byte) error {
    mu.Lock()
    snapshot := copyData(data)
    mu.Unlock() // tường minh — không có defer nào ở đây

    // xử lý snapshot mà không giữ khóa
    return process(snapshot)
}

Quy tắc 3 — Lock và Unlock phải nằm cùng cấp trong vòng lặp

// SAI — Lock một lần, Unlock nhiều lần
mu.Lock()
for _, item := range items {
    process(item)
    mu.Unlock() // crash sau lần lặp đầu tiên
}

// ĐÚNG — ghép cặp theo từng lần lặp
for _, item := range items {
    mu.Lock()
    process(item)
    mu.Unlock()
}

Quy tắc 4 — bọc trạng thái phức tạp vào một struct

Khi một mutex bảo vệ nhiều thao tác, hãy ẩn nó bên trong một kiểu dữ liệu. Mỗi method trở thành một cặp Lock/Unlock nhỏ, dễ kiểm tra:

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
}

Method nhỏ đồng nghĩa với phạm vi ảnh hưởng nhỏ. Một method 5 dòng với một cặp Lock/Unlock gần như không thể viết sai.

Sửa lỗi defer kết hợp Unlock thủ công bị gọi hai lần

// BỊ LỖI
func broken(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock() // cũng sẽ chạy khi return
    // ...
    mu.Unlock() // unlock lần đầu — trông có vẻ vô hại
    // hàm return → defer được gọi → panic
}

// ĐÃ SỬA phương án A — để defer làm tất cả
func fixedA(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock()
    // ...
    // không có Unlock tường minh nào bên dưới dòng này
}

// ĐÃ SỬA phương án B — bỏ defer, dùng hoàn toàn tường minh
func fixedB(mu *sync.Mutex) error {
    mu.Lock()
    // ...
    if err := validate(); err != nil {
        mu.Unlock()
        return err
    }
    mu.Unlock()
    return nil
}

Xác nhận bản sửa lỗi

Ba lệnh, chạy theo thứ tự:

# Kiểm tra cơ bản
go run main.go

# Chạy với race detector
go run -race main.go

# Chạy toàn bộ test suite với race detection
go test -race -count=1 ./...

Viết một test có mục tiêu cụ thể để tấn công vào đúng đoạn code đang gây panic. Ví dụ, nếu SafeCounter là thủ phạm:

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 goroutine, không có sleep, race detector bật. Nếu pass sạch, bản sửa lỗi đã hoạt động đúng.

Tham khảo nhanh

  • Double Unlock → xóa lệnh gọi thừa. Một Lock, một Unlock, chấm hết.
  • defer + Unlock thủ công → chọn một phong cách cho mỗi hàm. Không bao giờ dùng cả hai.
  • Unlock trong vòng lặp, Lock ở ngoài → chuyển Lock vào bên trong thân vòng lặp.
  • Biến thể RWMutexRLock/RUnlockLock/Unlock là hai cặp riêng biệt. Đừng dùng lẫn lộn chúng.

Related Error Notes