Migration guide
May 4, 2026 · View on GitHub
Moving from another Go locking library to ubgo/lock. The shape
is small enough that migrations are typically 1–2 lines per call
site.
From gofrs/flock
- import "github.com/gofrs/flock"
+ import "github.com/ubgo/lock/flock"
- fl := flock.New("/var/run/myservice/job.lock")
- if locked, err := fl.TryLock(); err != nil || !locked {
- return // already running
- }
- defer fl.Unlock()
+ fl := flock.New("job", flock.WithDir("/var/run/myservice"))
+ holder, err := fl.Acquire(ctx)
+ if errors.Is(err, flock.ErrLocked) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ defer holder.Release()
Notes:
gofrs/flocktakes a full file path;ubgo/lock/flocktakes a name + dir option. The marker file ends up at<dir>/<name>.lock.gofrs/flockreturns(locked, err)fromTryLock;ubgo/lock/flockreturns(*Holder, err)and usesErrLockedas a sentinel. Useerrors.Isto check.- Crash safety is the same — kernel-fenced via
flock(2)/LockFileEx.
From bsm/redislock
- import "github.com/bsm/redislock"
+ import "github.com/ubgo/lock/redislock"
- locker := redislock.New(rdb)
- lock, err := locker.Obtain(ctx, "job", time.Minute, nil)
- if err == redislock.ErrNotObtained {
- return // already held
- }
- defer lock.Release(ctx)
+ locks := redislock.NewFactory(rdb, redislock.WithTTL(time.Minute))
+ holder, err := locks.Acquire(ctx, "job")
+ if errors.Is(err, redislock.ErrLocked) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ defer holder.Release()
Notes:
bsm/redislock'sredislock.New(rdb)becomesredislock.NewFactory(rdb)— the rest of the API mirrors.ErrNotObtainedbecomesErrLocked(consistent across the family).- Lua-guarded release works the same way under the hood (we always check the holder value before deleting).
- Add
WithTTL(...)to the factory or per-call options.
From go-redsync/redsync
redsync implements Redlock (multi-master quorum). ubgo/lock
deliberately does NOT implement Redlock — see
non-goals.md.
If your existing redsync usage is single-master (one Redis pool), the migration is straightforward:
- import (
- "github.com/go-redsync/redsync/v4"
- "github.com/go-redsync/redsync/v4/redis/goredis/v9"
- )
+ import "github.com/ubgo/lock/redislock"
- pool := goredis.NewPool(rdb)
- rs := redsync.New(pool)
- mutex := rs.NewMutex("job", redsync.WithExpiry(time.Minute))
- if err := mutex.LockContext(ctx); err != nil { /* ... */ }
- defer mutex.UnlockContext(ctx)
+ locks := redislock.NewFactory(rdb, redislock.WithTTL(time.Minute))
+ holder, err := locks.Acquire(ctx, "job")
+ if err != nil { /* ... */ }
+ defer holder.Release()
If you're using redsync with multi-master quorum (multiple
pool.NewPool instances), don't migrate — use ubgo/lock/etcdlock
instead. etcd's Raft gives you the safety guarantees Redlock
approximates.
From lace/filelock (or any os.Create-based marker file)
This is the migration the family was originally designed for.
- import "github.com/<org>/lace/filelock"
+ import "github.com/ubgo/lock/filelock"
- flock := filelock.New("syncjob").SetDir(cfg.TempDir)
- if flock.IsActive() {
- fmt.Println("cron already running")
- return
- }
- flock.Lock()
- defer flock.Release()
+ locks := filelock.NewFactory(filelock.WithDir(cfg.TempDir))
+ err := locks.WithLock(ctx, "syncjob", syncJobFn,
+ filelock.WithStaleAfter(2*time.Hour),
+ )
+ if errors.Is(err, filelock.ErrLocked) {
+ return // another run active; skip
+ }
+ if err != nil { return err }
Notes:
- The old
IsActive() → Lock()pattern had a TOCTOU race that let two processes both think they were the singleton.WithLockcollapses to a single atomic call. lace/filelockleft crashed-process markers forever.filelockhere probes the PID + falls back toWithStaleAfterfor cross-host or inconclusive cases.- For services that lock many names,
Factory(WithDir(...))lifts the shared dir out of every call site.
From Postgres advisory locks (raw SQL)
- _, err := db.ExecContext(ctx, "SELECT pg_try_advisory_lock(\$1)", lockKey)
- // ... business logic ...
- _, err = db.ExecContext(ctx, "SELECT pg_advisory_unlock(\$1)", lockKey)
+ import "github.com/ubgo/lock/pglock"
+
+ locks := pglock.NewFactory(pool)
+ err := locks.WithLock(ctx, "lock-name", businessLogic)
+ if errors.Is(err, pglock.ErrLocked) {
+ return nil
+ }
Notes:
- The raw SQL approach has a subtle bug: if you don't pull a
dedicated connection from the pool, the lock and unlock can
end up on different connections (advisory locks are
session-tied).
pglockhandles connection pinning for you. pglockhashes string names into the int64 key for you (FNV-1a), so you can name locks by job name instead of allocating an integer keyspace manually.- Use
WithKeyOffsetto namespace your keyspace if other code in the same database usespg_advisory_lock.
From etcd/clientv3/concurrency.Mutex directly
- session, _ := concurrency.NewSession(cli, concurrency.WithTTL(30))
- defer session.Close()
- mutex := concurrency.NewMutex(session, "/locks/job")
- if err := mutex.TryLock(ctx); err == concurrency.ErrLocked {
- return // skip
- }
- defer mutex.Unlock(ctx)
+ import "github.com/ubgo/lock/etcdlock"
+
+ locks := etcdlock.NewFactory(cli, etcdlock.WithTTL(30*time.Second))
+ err := locks.WithLock(ctx, "job", businessLogic)
+ if errors.Is(err, etcdlock.ErrLocked) { return nil }
The wrapper is mostly ergonomics: a Factory pattern, a
WithLock(ctx, name, fn) helper, and translation of
concurrency.ErrLocked into the family's etcdlock.ErrLocked
sentinel. The fencing token (Holder.Token() returning
mod_revision) is exposed cleanly without you managing the
ResponseHeader yourself.
Decision: should I migrate?
You should migrate when you get one of these:
-
You need to swap backends. Your service started single-host with
gofrs/flock; now you're going multi-host and need Redis. Other libraries are point solutions;ubgo/locklets the same code path use either backend. -
You want fencing tokens but your library doesn't expose them. Especially relevant for
bsm/redislock(no fencing) →ubgo/lock/redislock. -
You want operator-readable markers.
lace/filelockand bareos.Createmarkers don't tell you who held the lock when ops finds a stale.lockfile at 3am. -
You want consistent test ergonomics.
memlockis a drop-in that works with any production backend in the family.
You should NOT migrate if:
- Your existing usage is correct, mature, and unlikely to change scope. Migration cost > benefit.
- You depend on a feature ubgo/lock deliberately doesn't ship
(Redlock, reentrancy outside pglock, blocking acquire). See
non-goals.md.