Help us shape the future of Go! Take the 2026 Go Developer Survey

keylock

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

README

keylock

Go Reference Go version

keylock gives your Go code one lock per key, so work on one file, user or record waits for itself while work on other keys runs.

It replaces the hand-written map of mutexes behind a mutex. It uses only the standard library, needs Go 1.27 or later and is licensed under Apache-2.0.

Why use it

keylock is built for services that serialize work per item, where the set of items is large or unbounded.

  • A wait ends when its context does. The caller then gets the context's error and holds nothing, so a cancelled request never takes a lock it gave up on.
  • A key's entry exists only while a caller holds or waits on it, so memory follows the work in flight, not the number of keys ever seen.
  • LockAll takes several keys in one fixed order, so two callers naming the same keys in a different order never deadlock.
  • Each unlock function is safe to call twice, so a defer unlock() beside an early unlock does not release someone else's lock.

Consider sync.Mutex if you need one lock for the whole process, because it adds no dependency at all.

Install

go get github.com/cplieger/keylock@latest

Usage

files := keylock.New(strings.Compare)

unlock, err := files.Lock(ctx, "movie.en.srt")
if err != nil {
    return err // ctx ended while waiting
}
defer unlock()

To hold several keys at once, pass them all to one LockAll call:

unlock, err := files.LockAll(ctx, []string{"movie.fr.srt", "movie.en.srt"})

LockAll removes duplicate keys, sorts the rest with the function you gave New and takes them one by one. When the context ends partway, it releases every key it took before returning the error. The package examples on pkg.go.dev show the common cases, and go test keeps them true.

The ordering rule

Every caller that takes more than one key must do it through one LockAll call. Two nested Lock calls can deadlock against another caller taking the same keys the other way round, and keylock cannot see that.

The function given to New must be a strict total order that agrees with ==. It returns 0 for two keys only when they are the same key. LockAll panics when it calls two different keys equal, because it could not order them the same way for every caller.

Lock and LockAll also panic on a key that cannot be found again or cannot be compared: a floating-point NaN, or an any key holding a slice. They check every key before taking any, so the panic leaves nothing held.

API

  • New builds a Set for one key type and its order.
  • Set.Lock takes one key and Set.LockAll takes several. Both return an unlock function, or the context's error.

The full reference is on pkg.go.dev.

Contributing

Issues and pull requests are welcome. The shared contributing rules apply.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

Apache-2.0. See LICENSE.

Documentation

Overview

Package keylock is a set of mutexes, one per key, for work that must not interleave with itself on one key while other keys proceed. A wait ends when its context does, and the caller then holds nothing. A key's entry is reference-counted and lives only while a caller holds or waits on it, so memory follows in-flight work, not the key space. Set.LockAll takes several keys in the one order the Set was built with, so callers naming the same keys in any order never deadlock.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Set

type Set[K comparable] struct {
	// contains filtered or unexported fields
}

Set holds one lock per key and is safe for concurrent use. Construct it with New; the zero value is not usable, and a Set must not be copied after first use. Lock and LockAll panic, holding nothing, on a key that is not equal to itself: a floating-point NaN, or an interface value whose dynamic type is not comparable.

func New

func New[K comparable](cmp func(a, b K) int) *Set[K]

New returns an empty Set whose Set.LockAll orders keys by cmp. cmp must be a strict total order consistent with ==: cmp(a, b) == 0 exactly when a == b. New panics on a nil cmp.

func (*Set[K]) Lock

func (s *Set[K]) Lock(ctx context.Context, k K) (unlock func(), err error)

Lock blocks until k is free or ctx is done. On success the caller holds k until it calls unlock, which is safe to call more than once. On failure it returns ctx.Err() and holds nothing; a ctx already done never takes k, even a free one. It panics, holding nothing, on a k not equal to itself (see Set).

Example
package main

import (
	"cmp"
	"context"
	"fmt"
	"time"

	"github.com/cplieger/keylock"
)

func main() {
	users := keylock.New(cmp.Compare[int])

	unlock, err := users.Lock(context.Background(), 42)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer unlock()

	// A second caller on the same user waits until its context ends, then
	// holds nothing.
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
	defer cancel()
	_, err = users.Lock(ctx, 42)
	fmt.Println(err)
}
Output:
context deadline exceeded

func (*Set[K]) LockAll

func (s *Set[K]) LockAll(ctx context.Context, keys []K) (unlock func(), err error)

LockAll takes every key in keys or none of them. It takes each distinct key once, in the Set's order, so callers naming the same keys in any order never deadlock, and it leaves keys unmodified. On failure it returns ctx.Err() with every key it took released. unlock releases them all and is safe to call more than once; with no keys there is nothing to take and LockAll succeeds. LockAll panics when the order calls two distinct keys equal, because it could not order them the same way for every caller, and on any key not equal to itself (see Set); it checks every key before it takes one, so either panic leaves nothing held.

Example
package main

import (
	"context"
	"fmt"
	"strings"
	"sync"

	"github.com/cplieger/keylock"
)

func main() {
	files := keylock.New(strings.Compare)
	ctx := context.Background()

	// Each writer names the same two files in its own order. LockAll takes
	// them in the Set's order, so neither writer can hold one file while
	// waiting on the other's.
	var wg sync.WaitGroup
	for _, names := range [][]string{{"movie.en.srt", "movie.fr.srt"}, {"movie.fr.srt", "movie.en.srt"}} {
		wg.Go(func() {
			unlock, err := files.LockAll(ctx, names)
			if err != nil {
				fmt.Println(err)
				return
			}
			defer unlock()
		})
	}
	wg.Wait()
	fmt.Println("both writers finished")
}
Output:
both writers finished

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL