lock
One lock interface with five backends (filelock, flock, Redis, Postgres, etcd) and fencing tokens.
go get github.com/ubgo/lock
- Language
- Go
- Go version
- 1.24
- Core deps
- 0 direct
- Latest tag
- v0.1.0
- License
- Apache-2.0
- Go modules
- 8
How it fits together
Highlights from the README
- One contract. All five backends satisfy
lock.Locker. Your service code acceptslock.Lockerand you swap concrete factories at startup.flockfor local dev →pglockin staging →etcdlockin prod, with zero application-code changes. - Production-grade defaults. Crash recovery, fencing tokens, observability hooks, structured logging, periodic stale cleanup. Things every “tutorial” lock library forgets.
- No-surprise semantics. Every backend’s
Acquireis non-blocking and returnslock.ErrLockedon contention — same sentinel everywhere. No backend silently waits, no backend uses a custom error type. Predictable. - Tested across platforms. Linux, macOS, Windows in CI. Real Postgres + real etcd in CI service containers. Unit tests run in milliseconds via
memlock. - Minimal cores. The interface module has zero deps. Each backend has only the deps it needs (e.g.
pglockhas pgx,redislockhas go-redis — never both).
Code from the README
package payments
import (
"context"
"errors"
"github.com/ubgo/lock"
)
type Service struct {
locks lock.Locker
}
func (s *Service) DailyExport(ctx context.Context) error {
h, err := s.locks.Acquire(ctx, "daily-export")
if errors.Is(err, lock.ErrLocked) {
return nil // another worker is on it; skip
}
if err != nil {
return err
}
defer h.Release()
return s.runExport(ctx)
}locks := redislock.NewFactory(rdb, redislock.WithTTL(10*time.Minute))
err := locks.WithLock(ctx, "midnight-billing", processBilling)
if errors.Is(err, redislock.ErrLocked) {
log.Info("billing run already in progress on another replica")
return nil
}
return errfl := flock.New("nightly-import", flock.WithDir("/var/run"))
holder, err := fl.Acquire(ctx)
if errors.Is(err, flock.ErrLocked) {
log.Println("previous run still active; skipping")
return
}
defer holder.Release()
runImport(ctx)holder, _ := locks.Acquire(ctx, "payment-export")
defer holder.Release()
fence := holder.Token() // monotonic uint64
if err := s3.PutWithFence(ctx, "payments/today.csv", data, fence); err != nil {
return err
}From the lock README.
At a glance (from the README)
| Concern | filelock | flock | redislock | pglock | etcdlock |
|---|---|---|---|---|---|
| Scope | single-host | single-host | multi-host | multi-host | multi-host |
| Crash recovery | PID probe + stale window + Sweep | Kernel — instant on fd close | TTL expiry | Session close | Lease expiry |
| Need extra infra | none | none | Redis | Postgres | etcd cluster |
| TTL to tune | optional | none | yes | none | yes |
| Reentrant | no | no | no | yes (PG native) | no |
| Fencing tokens | per-name sidecar | per-name sidecar | per-name INCR | per-session txid | mod_revision (global) |
Semaphore (WithMaxConcurrent) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Sweep | ✅ | n/a (kernel cleans) | n/a (TTL cleans) | n/a (session cleans) | n/a (lease cleans) |
| Operator visibility | rich marker fields | none | redis-cli GET <key> | pg_locks view | etcdctl get <key> |
| Observability hooks | ✅ slog/metrics/spans | ✅ same | ✅ same | ✅ same | ✅ same |
| TraceID propagation | marker debug field | slog field | embedded in SET value | via application_name | in lock key value |
| Strong consistency | local-fs only | local-fs only | weakly (AP) | ACID single primary | ✅ Raft |