LRU Cache with TTL Expiry in Go

Combine a map with a doubly linked list to evict least-recently-used entries and expire stale keys on read and write.

GoData Fetching

Code

snippet
package main

import (
	"container/list"
	"fmt"
	"sync"
	"time"
)

type Stats struct {
	Hits   int
	Misses int
	Evicts int
}

type entry[K comparable, V any] struct {
	key       K
	value     V
	expiresAt time.Time
}

type LRU[K comparable, V any] struct {
	mu       sync.Mutex
	capacity int
	ttl      time.Duration
	now      func() time.Time
	items    map[K]*list.Element
	order    *list.List
	stats    Stats
}

func NewLRU[K comparable, V any](capacity int, ttl time.Duration) (*LRU[K, V], error) {
	if capacity < 1 {
		return nil, fmt.Errorf("capacity must be positive")
	}
	if ttl <= 0 {
		return nil, fmt.Errorf("ttl must be positive")
	}
	return &LRU[K, V]{
		capacity: capacity,
		ttl:      ttl,
		now:      time.Now,
		items:    make(map[K]*list.Element),
		order:    list.New(),
	}, nil
}

func (c *LRU[K, V]) Get(key K) (V, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()

	element, ok := c.items[key]
	if !ok {
		c.stats.Misses++
		var zero V
		return zero, false
	}
	item := element.Value.(*entry[K, V])
	if !c.now().Before(item.expiresAt) {
		c.removeElement(element)
		c.stats.Misses++
		var zero V
		return zero, false
	}
	c.order.MoveToFront(element)
	c.stats.Hits++
	return item.value, true
}

func (c *LRU[K, V]) Put(key K, value V) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if element, ok := c.items[key]; ok {
		item := element.Value.(*entry[K, V])
		item.value = value
		item.expiresAt = c.now().Add(c.ttl)
		c.order.MoveToFront(element)
		return
	}

	element := c.order.PushFront(&entry[K, V]{
		key:       key,
		value:     value,
		expiresAt: c.now().Add(c.ttl),
	})
	c.items[key] = element
	for len(c.items) > c.capacity {
		oldest := c.order.Back()
		if oldest == nil {
			break
		}
		c.removeElement(oldest)
		c.stats.Evicts++
	}
}

func (c *LRU[K, V]) removeElement(element *list.Element) {
	item := element.Value.(*entry[K, V])
	c.order.Remove(element)
	delete(c.items, item.key)
}

func (c *LRU[K, V]) Stats() Stats {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.stats
}

func (c *LRU[K, V]) Len() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return len(c.items)
}

func main() {
	cache, err := NewLRU[string, int](2, 50*time.Millisecond)
	if err != nil {
		fmt.Println(err)
		return
	}

	cache.Put("a", 1)
	cache.Put("b", 2)
	if value, ok := cache.Get("a"); ok {
		fmt.Println("hit a:", value)
	}
	cache.Put("c", 3)
	if _, ok := cache.Get("b"); !ok {
		fmt.Println("b evicted")
	}
	time.Sleep(60 * time.Millisecond)
	if _, ok := cache.Get("a"); !ok {
		fmt.Println("a expired")
	}
	fmt.Printf("len=%d stats=%+v\n", cache.Len(), cache.Stats())
}