liveBook · Manning

Caching

As explained in Learn Go with Pocket-Sized Projects

A cache is a data store that retains reusable results or data to reduce access time or computational cost.

A cache keeps previously computed or retrieved values under keys so they can be reused instead of obtained again. It helps when fetching a value is costly and an earlier result is still acceptable. Freshness matters: a stable fact may be safe to reuse, while an old exchange rate may mislead. section 7.0 section 7.6

Avoid repeated exchange-rate requests

A converter processing a file of amounts could otherwise make an identical HTTP request for every line. One proposed cache stores rates in a dated temporary file: fetch when the file is absent or too old, load it otherwise, and provide a flag to flush it. An in-memory cache could instead sit behind the converter’s rate-fetching interface, avoiding a network call when the rate is already known. section 6.6.1 section 6.5

For a reusable Go cache, start with a map from keys to values. A key must be comparable; a value can have any type. Initialize the map in a constructor before storing entries. The basic operations are Read, which reports whether a key was found; Upsert, which stores or replaces a value; and Delete, which removes one. Storing V rather than *V leaves callers free to choose a pointer type when they need one. section 7.1 section 7.1.3

Listing 7.2 cache.go
// New creates a usable Cache.
func New[K comparable, V any]() Cache[K, V] {
    return Cache[K, V]{
        data: make(map[K]V),
    }
}

The constructor calls make so a Cache begins with a usable map. section 7.1.3

A working map is only the first version. Decide how the cache behaves when:

  • Several goroutines use it: a sequential test does not establish thread safety. Test simultaneous calls, then protect shared state with a mutex. section 7.3 section 7.4.2
  • A value becomes stale: give entries an expiration time and treat an expired entry as absent. section 7.4 section 7.4.2
  • Entries accumulate: set a maximum size and choose an eviction policy. One implementation removes the oldest entry; replacing an entry makes it new again. Tracking insertion order requires extra state. section 7.4 section 7.4.2
Listing 7.20 cache.go: Upsert with the new helper functions
func (c *Cache[K, V]) Upsert(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()

    _, alreadyPresent := c.data[key]
    switch {
    case alreadyPresent:
        c.deleteKeyValue(key) #1
    case len(c.data) == c.maxSize: #2
        c.deleteKeyValue(c.chronologicalKeys[0])
    }

    c.addKeyValue(key, value) #3
}

Upsert shows the capacity rule in action: under a mutex, it removes an existing key or evicts the oldest key before adding the new entry. section 7.4.2

How Learn Go with Pocket-Sized Projects covers it

How deep it goes
Builds a generic map cache, then elaborates concurrency, expiration, and capacity decisions; presents a dated-file cache as a proposal.
Examples
Uses repeated currency-conversion requests, generic key-value operations, and a capacity test where updating a key changes which entry is evicted.
What it assumes
Assumes familiarity with writing Go code while introducing generics before the first cache implementation.
What you can do after
Build it from scratch as a Go key-value cache with lookup, updates, deletion, expiration, concurrency protection, and bounded size.
How it connects
Connects caching to Go generics, comparable keys, goroutines, mutexes, time-to-live expiration, and eviction policies.

Also explained in

Compare how 38 books explain caching →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Learn Go with Pocket-Sized Projects. The section links point to the parts of the book it draws on.