|claim|login
RepoCritics — Review. Share. Archive. Every open-source repo.

patrickmn/go-cache

Wiki: patrickmn/go-cache

Source: https://github.com/patrickmn/go-cache

Last synced 2026-07-18 · 1179 words · Edit wiki on GitHub →

patrickmn/go-cache

> A thread-safe in-memory key:value cache with expiration for Go — a > map[string]interface{} with TTLs, and one of Go's most-depended-on > libraries despite being effectively frozen since 2017.

GitHub repo · Official website · License: MIT

Overview

go-cache is a single-machine, in-process cache library for Go, started by Patrick Mylund Nielsen in January 20121. The README's own framing is accurate and modest: it is essentially a thread-safe map[string]interface{} with per-item expiration times2. Because it lives inside your process, there is no serialization, no network hop, and no separate daemon — the trade being that the cache dies with the process and cannot be shared across machines.

At ~8.8k stars and ~900 forks it remains one of the most-referenced Go caching libraries, but the numbers deserve interpretation: the last tagged release is v2.1.0 from 2017, the last commit to master landed in November 2023, and 76 issues/PRs sit open without triage. It is abandonware in practice — stable abandonware, since the code is small (~1,200 lines) and the problem it solves does not move. Many teams vendor it or copy the pattern rather than depend on it. The defining tension: it is the simplest possible correct answer to "I need a TTL map", and simultaneously the wrong answer for any workload with bounded memory requirements, high write concurrency, or millions of entries.

A modules-era footgun follows from its age: the repo tagged v2.x before Go modules existed and never added a /v2 module path, so go get resolves it as v2.1.0+incompatible3.

Getting Started

go get github.com/patrickmn/go-cache   # resolves to v2.1.0+incompatible
package main

import (
	"fmt"
	"time"

	"github.com/patrickmn/go-cache"
)

func main() {
	// Default TTL 5 min; expired items purged every 10 min.
	c := cache.New(5*time.Minute, 10*time.Minute)

	c.Set("foo", "bar", cache.DefaultExpiration)
	c.Set("meaning", 42, cache.NoExpiration) // lives until deleted

	if x, found := c.Get("foo"); found {
		s := x.(string) // values are interface{}; assertion required
		fmt.Println(s)
	}
}

The library predates Go generics (1.18), so every read requires a type assertion. The README's own advice for performance is "store pointers"2 — storing large structs by value costs a copy plus an allocation per Set.

Architecture / How It Works

The entire library is one package with a handful of files. The core is:

  • One map, one lock. map[string]Item guarded by a single

sync.RWMutex. Item is {Object interface{}; Expiration int64}, with expiration stored as a Unix-nanosecond timestamp; 0 means no expiration. Reads take the read lock; every Set, Delete, and Increment takes the write lock. There is no sharding in the public API (an unexported sharded variant exists in the repo but was never stabilized).

  • Lazy + background expiration. Get compares time.Now().UnixNano()

against the item's expiration, so expired items are never returned. Actual memory reclamation happens in a "janitor" goroutine that calls DeleteExpired() on the cleanup interval passed to New.

  • The finalizer trick. The janitor goroutine would normally pin the cache

and leak. New returns an outer *Cache wrapping an inner cache struct; the janitor references only the inner struct, and runtime.SetFinalizer on the outer wrapper stops the janitor when the last user reference is dropped4. This is the one genuinely clever piece of the codebase and a frequently-cited example of finalizer use in Go.

  • Typed increments. Increment/Decrement plus a family of

IncrementInt, IncrementFloat64, etc., because interface{} arithmetic requires a type switch per numeric type.

  • Optional persistence. Items() + NewFrom() (and deprecated

SaveFile/LoadFile using encoding/gob) allow snapshot/restore for warm restarts. The docs themselves flag the caveats: gob needs concrete type registration and the mechanism is not a durability story2.

There are no dependencies outside the standard library, which is a large part of why the library has survived nine years without maintenance.

Production Notes

  • No size bound, no eviction policy. Expiration is the only way entries

leave. There is no LRU/LFU, no max-entries, no cost accounting. A cache keyed by unbounded input (user IDs, URLs, query strings) grows until OOM. This is the most common production incident with go-cache.

  • Write-lock contention. The single RWMutex is fine up to moderate

concurrency, but write-heavy workloads on many cores serialize on the one lock. Benchmarks in the bigcache/freecache literature exist precisely because of this ceiling5.

  • GC pressure at scale. Millions of interface{} values and string keys

mean millions of pointers for the garbage collector to scan each cycle. At large entry counts, GC pause impact — not lock contention — is usually the first symptom. bigcache and freecache were designed specifically to sidestep this by storing serialized bytes5.

  • Cleanup interval is a stop-the-world-ish scan. DeleteExpired walks

the whole map under the write lock. With very large maps and short cleanup intervals, janitor runs cause periodic latency spikes on writers.

  • No Get-with-TTL-refresh, no singleflight. Cache stampede protection

(many goroutines missing the same key simultaneously and all recomputing) must be layered on with golang.org/x/sync/singleflight.

  • Unmaintained means unmerged. Generics support, sharding, and eviction

PRs have been open for years. Pin the version and treat the code as yours; do not expect upstream fixes.

When to Use / When Not

Use when:

  • You need a TTL map for a small-to-medium working set (thousands to low

hundreds of thousands of entries) in a single process.

  • Key cardinality is naturally bounded, so absence of eviction is safe.
  • You want zero dependencies and code simple enough to audit in one sitting.

Avoid when:

  • Memory must be bounded — you need LRU/cost-based eviction (ristretto,

golang-lru, otter).

  • You cache millions of entries and GC pauses matter (bigcache, freecache).
  • You want type safety without assertions — post-1.18 generic caches exist.
  • You need cross-process or cross-machine sharing — that is Redis/memcached

territory, not an in-process cache.

Alternatives

  • dgraph-io/ristretto — use instead when you need bounded memory with a

high hit ratio; TinyLFU admission and cost-based eviction.

  • allegro/bigcache — use instead for millions of entries; stores serialized

bytes to keep entries out of GC scan scope.

  • coocood/freecache — same zero-GC-overhead goal as bigcache with a strict

preallocated memory bound.

  • hashicorp/golang-lru — use instead when you want a simple size-bounded LRU

rather than TTL semantics; actively maintained.

  • jellydator/ttlcache — use instead for the same TTL-map semantics with

generics and active maintenance.

History

VersionDateNotes
initial2012-01First commit; GOPATH-era library1.
v2.0.02015API cleanup; time.Duration expirations; v2 tag predates modules.
v2.1.02017-06Last tagged release; resolves as +incompatible under modules3.
2023-11Last commit to master; no release cut. Project dormant since.

References

  1. ^ GitHub repository metadata — created 2012-01-02, last push 2023-11-20. https://github.com/patrickmn/go-cache
  2. ^ go-cache README. https://github.com/patrickmn/go-cache#readme
  3. ^ Go Modules reference, "+incompatible versions" — v2+ tags without a go.mod major-version suffix. https://go.dev/ref/mod#incompatible-versions
  4. ^ go-cache API documentation (janitor and New semantics). https://pkg.go.dev/github.com/patrickmn/go-cache
  5. ^ allegro engineering, "Writing a very fast cache service in Go" — rationale for bigcache's GC-avoiding design. https://blog.allegro.tech/2016/03/writing-fast-cache-service-in-go.html

Tags

go, caching, in-memory, key-value, ttl, concurrency, standard-library-only, single-machine