TL;DR: Can we check if a mutex is locked in Go? Yes, but not with a mutex API. Here’s a solution for use in debug builds.

Although you can Lock() or Unlock() a mutex, you can’t check whether it’s locked. While it is a reasonable omission (e.g., due to possible race conditions; see also Why can’t I check whether a mutex is locked?), having such functionality can still be useful for testing whether the software does what it is supposed to do.

In other words, it would be nice to have an AssertMutexLocked function solely for debug builds, which could be used like this:

// this method should always be called with o.lock locked
func (Object* o) someMethodImpl() {
    AssertMutexLocked(&o.lock)
    // (...)
}

Having such a function would allow us to confirm the assumption that a given mutex is locked and find potential bugs when it’s added into an existing codebase. In fact, there was a GitHub issue about adding this exact functionality in the official Go repository (golang/go#1366), but it was closed with a WontFix status.

I also learned via the great grep.app project that many projects have similar preconditions about mutexes, such as google/gvisor, ghettovoice/gossip, vitessio/vitess, and others.

Now let’s implement the MutexLocked (and other) functions.

Checking if a mutex is locked

To check whether a mutex is locked, we have to read its state. The sync.Mutex structure contains two fields:

type Mutex struct {
	state int32
	sema  uint32
}

The state field’s bits correspond to the following flags (source):

const (
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexStarving
	mutexWaiterShift = iota
	// (...)

So if a mutex is locked, its state field has the mutexLocked (1) bit set. However, we can’t just access the state field directly from a Go program, because this field is not exported (its name does not start with a capital letter). Luckily, the field can still be accessed with Go reflection, which I used in the code below when I implemented the functions that allow us to check if a given sync.Mutex or sync.RWMutex is locked.

package main

import (
	"fmt"
	"reflect"
	"sync"
)

const mutexLocked = 1

func MutexLocked(m *sync.Mutex) bool {
	state := reflect.ValueOf(m).Elem().FieldByName("state")
	return state.Int()&amp;mutexLocked == mutexLocked
}

func RWMutexWriteLocked(rw *sync.RWMutex) bool {
	// RWMutex has a "w" sync.Mutex field for write lock
	state := reflect.ValueOf(rw).Elem().FieldByName("w").FieldByName("state")
	return state.Int()&amp;mutexLocked == mutexLocked
}

func RWMutexReadLocked(rw *sync.RWMutex) bool {
	return reflect.ValueOf(rw).Elem().FieldByName("readerCount").Int() &gt; 0
}

func main() {
	m := sync.Mutex{}
	fmt.Println("m locked =", MutexLocked(&amp;m))
	m.Lock()
	fmt.Println("m locked =", MutexLocked(&amp;m))
	m.Unlock()
	fmt.Println("m locked =", MutexLocked(&amp;m))

	rw := sync.RWMutex{}
	fmt.Println("rw write locked =", RWMutexWriteLocked(&amp;rw), " read locked =", RWMutexReadLocked(&amp;rw))
	rw.Lock()
	fmt.Println("rw write locked =", RWMutexWriteLocked(&amp;rw), " read locked =", RWMutexReadLocked(&amp;rw))
	rw.Unlock()
	fmt.Println("rw write locked =", RWMutexWriteLocked(&amp;rw), " read locked =", RWMutexReadLocked(&amp;rw))
	rw.RLock()
	fmt.Println("rw write locked =", RWMutexWriteLocked(&amp;rw), " read locked =", RWMutexReadLocked(&amp;rw))
	rw.RLock()
	fmt.Println("rw write locked =", RWMutexWriteLocked(&amp;rw), " read locked =", RWMutexReadLocked(&amp;rw))
	rw.RUnlock()
	fmt.Println("rw write locked =", RWMutexWriteLocked(&amp;rw), " read locked =", RWMutexReadLocked(&amp;rw))
	rw.RUnlock()
	fmt.Println("rw write locked =", RWMutexWriteLocked(&amp;rw), " read locked =", RWMutexReadLocked(&amp;rw))
}

We can see this program’s output below:

m locked = false
m locked = true
m locked = false
rw write locked = false  read locked = false
rw write locked = true  read locked = false
rw write locked = false  read locked = false
rw write locked = false  read locked = true
rw write locked = false  read locked = true
rw write locked = false  read locked = true
rw write locked = false  read locked = false

And this can later be used to create AssertMutexLocked and other functions. To that end, I’ve created a small library with these functions at trailofbits/go-mutexasserts—which enables the assertion checks only in builds with a debug tag.

Note: Although there are other tools for detecting race conditions in Go, such as Go’s race detector or OnEdge from Trail of Bits, these tools will detect problematic situations only once they occur, and won’t allow you to assert whether the mutex precondition holds.

We’re always developing tools to help you work faster and smarter. Need help with your next project? Contact us!