liveBook · Manning

Upsert

As explained in Learn Go with Pocket-Sized Projects

Upsert is the example cache operation that inserts a key-value entry or replaces the existing entry for that key.

Upsert combines insert and update for a key-value cache. Give it a key and value: if the key is absent, the cache adds it; if present, the cache silently replaces its older value. A subsequent Read finds the specified value. This policy suits a cache where newer values are usually more useful, but it does not suit every storage operation. section 7.1.3

Listing 7.4 cache.go: Implementing the Upsert function on the cache
// Upsert overrides the value for a given key.
func (c *Cache[K, V]) Upsert(key K, value V) error {
    c.data[key] = value

    // Do not return an error for the moment,
    // but it can happen in the near future.
    return nil
}

The initial Go implementation assigns directly to the map. Its error return is reserved for possible future failures, such as a capacity limit; this version always returns nil. section 7.1.3

A size-limited cache needs more than map assignment. It tracks keys in insertion order and treats an overwrite as a fresh insertion. Under a mutex, Upsert removes an existing key from that order; for a new key at capacity, it removes the oldest entry instead. It then adds the value and key together. The helper also sets the entry’s expiration time, so an overwrite refreshes its TTL. This later version has no error return, unlike the initial method. section 7.4.2 section 7.1.3

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
}

Follow the two branches before the final add: an existing key is requeued, while a new key evicts the oldest only when the cache is full. section 7.4.2

Check the eviction order

With capacity three, Upsert keys 1, 2, and 3. Upsert key 1 again with a new value, then Upsert key 4. Key 2—not key 1—is now the oldest and is discarded. The capacity test checks this behavior with Read. section 7.4.2

Choose the operation name to match the storage rule. A game repository instead separates Add from Update: Add rejects an existing game, and Update is intended to reject an absent one. A single Upsert would erase that distinction. section 8.4.2

How Learn Go with Pocket-Sized Projects covers it

How deep it goes
Shows direct map assignment, then extends Upsert with locking, expiration refresh, insertion ordering and oldest-entry eviction.
Examples
Tests a three-entry cache where updating key 1 makes key 2 the eviction candidate when key 4 arrives.
What it assumes
Assumes familiarity with Go methods and maps while showing how to implement the cache operation.
What you can do after
Build it from scratch as a Go cache method, including overwrite behavior and oldest-entry eviction at capacity.
How it connects
Connects Upsert to Go maps, generics, mutexes, TTL, cache eviction and repository Add/Update semantics.

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.