cache-obj
In-process cache that holds live Go objects by reference, with TTL and LRU and no serialization.
go get github.com/ubgo/cache-obj
- Language
- Go
- Go version
- 1.24
- Core deps
- 2 direct
- Latest tag
- v0.1.0
- License
- Apache-2.0
- Go modules
- 1
How it fits together
passesobjtestobjtest is the shared conformance suite: each one marked green has a test that runs it.
Highlights from the README
- Live objects, by reference.
Getreturns the same instance youSet— no serialization, no copy. The only cache in the family that can hold non-serializable values. - Generics.
Cache[T]storesTdirectly, nointerface{}boxing. - Per-entry TTL.
SetTTL(key, v, ttl);ttl <= 0means the entry never expires.WithDefaultTTLsets the TTLSetapplies. - LRU bound.
WithCapacity(n)evicts the least-recently-used entry when full. Omit it for an unbounded cache. - Value-bearing eviction hook.
WithOnEvict(func(key string, v T, cause cache.EvictionCause))fires when an entry is dropped involuntarily (capacity or expiry) and hands you the value — so you can close handles or release resources. - Single-flight
Remember. Get-or-load that collapses a thundering herd: N concurrent misses for the same key run the loader once and share the result. - Optional background sweeper.
WithSweepInterval(d)proactively evicts expired entries;Close()stops it. Default expiry is lazy (no goroutine). - Stats.
Stats()returns the sharedcache.Statsshape, so observability reads identically across the family.
Code from the README
import cacheobj "github.com/ubgo/cache-obj"
// Cache compiled regexes — a value that must stay live (it can't survive a codec).
re := cacheobj.New[*regexp.Regexp](cacheobj.WithCapacity(128))
re.Set(`\d+`, regexp.MustCompile(`\d+`))
if r, ok := re.Get(`\d+`); ok {
r.MatchString("abc123") // same compiled program, zero cost
}package main
import (
"fmt"
"regexp"
"time"
cacheobj "github.com/ubgo/cache-obj"
)
func main() {
// A bounded cache of compiled regexes with a 1-hour TTL.
re := cacheobj.New[*regexp.Regexp](
cacheobj.WithCapacity(1024),
cacheobj.WithDefaultTTL(time.Hour),
)
// Get-or-load with single-flight: compiles once, even under concurrency.
digits, _ := re.Remember(`\d+`, time.Hour, func() (*regexp.Regexp, error) {
return regexp.Compile(`\d+`)
})
fmt.Println(digits.MatchString("abc123")) // true
// A second Get returns the exact same instance.
again, _ := re.Get(`\d+`)
fmt.Println(again == digits) // true
fmt.Printf("%+v\n", re.Stats()) // {Hits:1 Misses:1 Sets:1 ...}
}type Cache[T any] interface {
Get(key string) (T, bool)
Set(key string, v T)
SetTTL(key string, v T, ttl time.Duration)
Del(key string)
Len() int
Purge()
Stats() cache.Stats
}
func New[T any](opts ...Option) *Store[T]
// On the concrete *Store[T] returned by New (kept off the minimal interface):
func (s *Store[T]) Remember(key string, ttl time.Duration, fn func() (T, error)) (T, error)
func (s *Store[T]) Close() // stops the background sweeper; idempotentc := cacheobj.New[*http.Client](
cacheobj.WithCapacity(1000), // LRU bound; omit for unbounded
cacheobj.WithDefaultTTL(10*time.Minute), // TTL applied by Set; SetTTL overrides
cacheobj.WithOnEvict(func(key string, v *http.Client, cause cache.EvictionCause) {
// cause is cache.EvictSize (capacity) or cache.EvictExpired (TTL); v is the evicted value
}),
cacheobj.WithSweepInterval(time.Minute), // background expiry sweeper (else lazy); call Close to stop
cacheobj.WithClock(myFakeClock), // deterministic TTL tests
)From the cache-obj README.