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.
Code
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())
}
Report a problem with this exampleOpens a pre-filled issue on GitHub.