We often use Mutex or RWMutex as locks in Go, but sometimes we need a lock that can be cancelled by a context during the lock attempt.

The pattern is simple - we use a channel with length 1:

lockChan := make(chan struct{}, 1)
lockChan <- struct{}{} // lock
<- lockChan            // unlock

When multiple goroutines try to obtain the lock, only one of them is able to fill into the only slot, and the rest are blocked until the slot is empty again after a readout.

Unlike mutexes, we could easily cancel the lock attempt with a context:

select {
    case <-ctx.Done():
        // cancelled
    case lockChan <- struct{}{}:
        // locked
}

Let’s wrap it up:

type CtxMutex struct {
    ch chan struct{}
}

func (mu *CtxMutex) Lock(ctx context.Context) bool {
    select {
        case <-ctx.Done():
            return false
        case mu.ch <- struct{}{}:
            return true
    }
}

func (mu *CtxMutex) Unlock() {
    <- mu.ch
}


func (mu *CtxMutex) Locked() bool {
    return len(mu.ch) > 0 // locked or not
}

Further, context.WithTimeout could be used to apply a timeout to the lock.