cache
The core cache interface with generics, single-flight Remember, a distributed locker and a shared conformance suite.
go get github.com/ubgo/cache
- Language
- Go
- Go version
- 1.24
- Core deps
- 0 direct
- Latest tag
- v0.1.0
- License
- Apache-2.0
- Go modules
- 6
How it fits together
passescachetestdoes not passcachetest is the shared conformance suite: each one marked green has a test that runs it. cache-memcached does not pass by design, because Memcached cannot read a key's TTL, scan by prefix or list keys.
Highlights from the README
- One interface, every backend —
Cacheis bytes-in/bytes-out; adapters for in-memory, Redis, Postgres/SQLite, tiered, and Memcached. - Typed generics —
Remember[T],GetT[T],SetT[T],Typed[T],RememberMulti[T],Warm[T],Memoize— no manual (de)serialization at call sites. - Single-flight
Remember— concurrent misses for the same key collapse to one loader call. - Cache patterns built in — refresh-ahead, stale-while-revalidate (SWR), stale-if-error, negative caching, TTL jitter.
- Pluggable codecs — JSON, gob, raw;
EncryptedCodec(AES-GCM) for caching PII/secrets in shared stores; msgpack/zstd/protobuf incontrib/. - Namespace isolation —
Namespacedprefixes keys so many services share one backend safely. - Portable distributed lock —
Lockerbuilt onSetNX, works on any adapter with no backend-specific code. - Cross-process invalidation — best-effort fan-out bus so L1 caches drop stale copies.
Code from the README
c := memcache.New(memcache.WithMaxEntries(100_000))
defer c.Close()
// Bytes in, bytes out. Get returns (nil, cache.ErrNotFound) on miss — never (nil, nil).
_ = c.Set(ctx, "k", []byte("v"), time.Minute)
b, err := c.Get(ctx, "k")
// Typed load-through with stampede protection and the differentiator options:
u, err := cache.Remember(ctx, c, "user:42", 5*time.Minute,
func(ctx context.Context) (User, error) { return db.LoadUser(ctx, 42) },
cache.WithJitter(0.1), // spread expiry across a batch of keys
cache.WithStaleIfError(time.Minute), // serve last-good if the loader fails
cache.WithRefreshAhead(0.8), // refresh hot keys before they expire
)
// Multi-tenant isolation:
tenant := cache.Namespaced(c, "tenant:42")
// Portable distributed lock (works on any backend):
l := cache.NewLock(c, "cron:nightly", 30*time.Second)
if err := l.Acquire(ctx); err == nil {
defer l.Release(ctx)
// ... only one holder runs this ...
}c := cache.NewAuditLog(
cache.NewBulkhead(
cache.NewRetry(
cache.NewCircuitBreaker(rediscache.New(rdb)),
), 256),
auditSink)type Cache interface {
Get(ctx, key) ([]byte, error)
GetMulti(ctx, keys) (map[string][]byte, error)
Has(ctx, key) (bool, error)
TTL(ctx, key) (time.Duration, error)
Set(ctx, key, val, ttl) error
SetMulti(ctx, items map[string]Item) error
SetNX(ctx, key, val, ttl) (bool, error)
Expire(ctx, key, ttl) error
Touch(ctx, key) error
Incr(ctx, key, delta) (int64, error)
Decr(ctx, key, delta) (int64, error)
Del(ctx, keys ...string) error
DeleteByPrefix(ctx, prefix) error
Flush(ctx) error
Iterate(ctx, IterateOpts) Iterator
Ping(ctx) error
Close() error
Stats() Stats
}v, err := c.Get(ctx, "user:42")
if errors.Is(err, cache.ErrNotFound) {
// cache miss — load it
}From the cache README.
At a glance (from the README)
| Module | Backend |
|---|---|
cache-mem | in-memory, sharded — Adaptive W-TinyLFU (default) or LRU; snapshot + periodic checkpoint |
cache-redis | Redis 6+ (+ Pub/Sub invalidation) |
cache-pg | Postgres 14+ / SQLite (portable database/sql) |
cache-tiered | L1/L2(/L3) composer with read-promotion + cross-process invalidation |
cache-memcached | Memcached (partial — protocol-limited) |
cache-cluster | client-side sharding across nodes |
cache-cli | inspector binary |