$ git clone https://github.com/angvp/tango $ cd tango/examples/cache $ go run .
A cache for a recomputable read
GET /rates/EUR/ works out an exchange rate once and serves it from a bounded in-process cache for 30 seconds, answering X-Cache: miss the first time and X-Cache: hit afterwards. It shows the whole of what the cache package is for:
cache.FetchJSON at the call site. The cache is a cache.Store passed in; nothing is global. A hit skips the loader, a miss runs it and stores the result, and a loader error (an unknown currency) is returned and never cached.
A bounded store.cache/local holds at most 1000 entries and 1 MiB.
A versioned prefix for bulk invalidation. Entries live under rates:v1:. Set CACHE_VERSION=2 and restart, and every older entry becomes unreachable and expires by its TTL. There is no Clear.
Fail-open. If the cache is down, the request still succeeds: FetchJSON computes the rate and reports each cache failure through OnError, which this app logs. The test uses a tiny Store whose every call fails to prove it.
It caches recomputable reads only. It is not ORM caching or response caching, and nothing invalidates the cache when the underlying data changes: a stale answer lasts at most the TTL.
Run it
git clone https://github.com/angvp/tango
cd tango/examples/cache
go run .
curl -i localhost:8000/rates/EUR/ # X-Cache: misscurl -i localhost:8000/rates/EUR/ # X-Cache: hit
Things to know
cache/local is per process. For more than one instance use cache/redis or cache/memcache (each its own module) and change only newStore in main.go. See the caching guide.
go test . advances the cache's clock by hand, so expiry is tested without sleeping.
examples/cache/main.go
// Command cache shows what the cache package is for: speeding up a read that// is expensive to compute and safe to compute again. GET /rates/EUR/ computes// an exchange rate once and serves it from a bounded in-process cache for 30// seconds, reporting X-Cache: hit or miss.//// It caches recomputable reads only. It is not ORM or response caching, and// nothing here invalidates the cache when data changes: every value has a// TTL, a stale answer is bounded by it, and bulk invalidation is a new key// prefix (set CACHE_VERSION=2 and restart).packagemainimport("context""flag""fmt""os""os/signal""syscall""time""github.com/angvp/tango""github.com/angvp/tango/cache""github.com/angvp/tango/cache/local""github.com/angvp/tango/db")funcmain(){os.Exit(run())}funcrun()int{check:=flag.Bool("check",false,"validate app registration and exit")flag.Parse()store,err:=newStore(os.Getenv("CACHE_VERSION"))iferr!=nil{fmt.Fprintln(os.Stderr,err)return1}config:=appConfig(store,&calculator{now:time.Now})if*check{iferr:=tango.Check(config);err!=nil{fmt.Fprintln(os.Stderr,"check failed:",err)return1}fmt.Println("check passed")return0}ctx,stop:=signal.NotifyContext(context.Background(),os.Interrupt,syscall.SIGTERM)deferstop()fmt.Println("listening on",config.Addr)iferr:=tango.ServeContext(ctx,config,nil,db.SQLite);err!=nil{fmt.Fprintln(os.Stderr,err)return1}return0}// newStore is the cache this process uses: bounded in memory, namespaced by a// version so that bumping it makes every older entry unreachable.funcnewStore(versionstring)(cache.Store,error){ifversion==""{version="1"}base,err:=local.New(local.Config{MaxEntries:1000,MaxBytes:1<<20})iferr!=nil{returnnil,err}returncache.Prefix(base,"rates:v"+version+":"),nil}funcappConfig(storecache.Store,calc*calculator)tango.Config{config:=tango.LoadConfigFromEnv(tango.WithPortFromEnv())config.InstalledApps=[]tango.App{ratesApp(store,calc)}returnconfig}