package lru// golang -lru// https://github.com/hashicorp/golang-lruimport ()// EvictCallback is used to get a callback when a cache entry is evictedtypeEvictCallback[ comparable, any] func(key , value )// LRU implements a thread-safe LRU with expirable entries.typeLRU[ comparable, any] struct { size int evictList *LruList[, ] items map[]*Entry[, ] onEvict EvictCallback[, ]// expirable options mu sync.Mutex ttl time.Duration done chanstruct{}// buckets for expiration buckets []bucket[, ]// uint8 because it's number between 0 and numBuckets nextCleanupBucket uint8}// bucket is a container for holding entries to be expiredtype bucket[ comparable, any] struct { entries map[]*Entry[, ] newestEntry time.Time}// noEvictionTTL - very long ttl to prevent evictionconst noEvictionTTL = time.Hour * 24 * 365 * 10// because of uint8 usage for nextCleanupBucket, should not exceed 256.// casting it as uint8 explicitly requires type conversions in multiple placesconst numBuckets = 100// NewLRU returns a new thread-safe cache with expirable entries.//// Size parameter set to 0 makes cache of unlimited size, e.g. turns LRU mechanism off.//// Providing 0 TTL turns expiring off.//// Delete expired entries every 1/100th of ttl value. Goroutine which deletes expired entries runs indefinitely.func [ comparable, any]( int, EvictCallback[, ], time.Duration) *LRU[, ] {if < 0 { = 0 }if <= 0 { = noEvictionTTL } := LRU[, ]{ttl: ,size: ,evictList: NewList[, ](),items: make(map[]*Entry[, ]),onEvict: ,done: make(chanstruct{}), }// initialize the buckets .buckets = make([]bucket[, ], numBuckets)for := 0; < numBuckets; ++ { .buckets[] = bucket[, ]{entries: make(map[]*Entry[, ])} }// enable deleteExpired() running in separate goroutine for cache with non-zero TTL // // Important: done channel is never closed, so deleteExpired() goroutine will never exit, // it's decided to add functionality to close it in the version later than v2.if .ttl != noEvictionTTL {gofunc( <-chanstruct{}) { := time.NewTicker(.ttl / numBuckets)defer .Stop()for {select {case<-:returncase<-.C: .deleteExpired() } } }(.done) }return &}// Purge clears the cache completely.// onEvict is called for each evicted key.func ( *LRU[, ]) () { .mu.Lock()defer .mu.Unlock()for , := range .items {if .onEvict != nil { .onEvict(, .Value) }delete(.items, ) }for , := range .buckets {for , := range .entries {delete(.entries, .Key) } } .evictList.Init()}// Add adds a value to the cache. Returns true if an eviction occurred.// Returns false if there was no eviction: the item was already in the cache,// or the size was not exceeded.func ( *LRU[, ]) ( , ) ( bool) { .mu.Lock()defer .mu.Unlock() := time.Now()// Check for existing itemif , := .items[]; { .evictList.MoveToFront() .removeFromBucket() // remove the entry from its current bucket as expiresAt is renewed .Value = .ExpiresAt = .Add(.ttl) .addToBucket()returnfalse }// Add new item := .evictList.PushFrontExpirable(, , .Add(.ttl)) .items[] = .addToBucket() // adds the entry to the appropriate bucket and sets entry.expireBucket := .size > 0 && .evictList.Length() > .size// Verify size not exceededif { .removeOldest() }return}// Get looks up a key's value from the cache.func ( *LRU[, ]) ( ) ( , bool) { .mu.Lock()defer .mu.Unlock()var *Entry[, ]if , = .items[]; {// Expired item checkiftime.Now().After(.ExpiresAt) {return , false } .evictList.MoveToFront()return .Value, true }return}// Contains checks if a key is in the cache, without updating the recent-ness// or deleting it for being stale.func ( *LRU[, ]) ( ) ( bool) { .mu.Lock()defer .mu.Unlock() _, = .items[]return}// Peek returns the key value (or undefined if not found) without updating// the "recently used"-ness of the key.func ( *LRU[, ]) ( ) ( , bool) { .mu.Lock()defer .mu.Unlock()var *Entry[, ]if , = .items[]; {// Expired item checkiftime.Now().After(.ExpiresAt) {return , false }return .Value, true }return}// Remove removes the provided key from the cache, returning if the// key was contained.func ( *LRU[, ]) ( ) bool { .mu.Lock()defer .mu.Unlock()if , := .items[]; { .removeElement()returntrue }returnfalse}// RemoveOldest removes the oldest item from the cache.func ( *LRU[, ]) () ( , , bool) { .mu.Lock()defer .mu.Unlock()if := .evictList.Back(); != nil { .removeElement()return .Key, .Value, true }return}// GetOldest returns the oldest entryfunc ( *LRU[, ]) () ( , , bool) { .mu.Lock()defer .mu.Unlock()if := .evictList.Back(); != nil {return .Key, .Value, true }return}func ( *LRU[, ]) () map[] { .mu.Lock()defer .mu.Unlock() := make(map[]) := time.Now()for := .evictList.Back(); != nil; = .PrevEntry() {if .After(.ExpiresAt) {continue } [.Key] = .Value// keys = append(keys, ent.Key) }return}// Keys returns a slice of the keys in the cache, from oldest to newest.// Expired entries are filtered out.func ( *LRU[, ]) () [] { .mu.Lock()defer .mu.Unlock() := make([], 0, len(.items)) := time.Now()for := .evictList.Back(); != nil; = .PrevEntry() {if .After(.ExpiresAt) {continue } = append(, .Key) }return}// Values returns a slice of the values in the cache, from oldest to newest.// Expired entries are filtered out.func ( *LRU[, ]) () [] { .mu.Lock()defer .mu.Unlock() := make([], 0, len(.items)) := time.Now()for := .evictList.Back(); != nil; = .PrevEntry() {if .After(.ExpiresAt) {continue } = append(, .Value) }return}// Len returns the number of items in the cache.func ( *LRU[, ]) () int { .mu.Lock()defer .mu.Unlock()return .evictList.Length()}// Resize changes the cache size. Size of 0 means unlimited.func ( *LRU[, ]) ( int) ( int) { .mu.Lock()defer .mu.Unlock()if <= 0 { .size = 0return0 } := .evictList.Length() - if < 0 { = 0 }for := 0; < ; ++ { .removeOldest() } .size = return}// Close destroys cleanup goroutine. To clean up the cache, run Purge() before Close().// func (c *LRU[K, V]) Close() {// c.mu.Lock()// defer c.mu.Unlock()// select {// case <-c.done:// return// default:// }// close(c.done)// }// removeOldest removes the oldest item from the cache. Has to be called with lock!func ( *LRU[, ]) () {if := .evictList.Back(); != nil { .removeElement() }}// removeElement is used to remove a given list element from the cache. Has to be called with lock!func ( *LRU[, ]) ( *Entry[, ]) { .evictList.Remove()delete(.items, .Key) .removeFromBucket()if .onEvict != nil { .onEvict(.Key, .Value) }}// deleteExpired deletes expired records from the oldest bucket, waiting for the newest entry// in it to expire first.func ( *LRU[, ]) () { .mu.Lock() := .nextCleanupBucket := time.Until(.buckets[].newestEntry)// wait for newest entry to expire before cleanup without holding lockif > 0 { .mu.Unlock()time.Sleep() .mu.Lock() }for , := range .buckets[].entries { .removeElement() } .nextCleanupBucket = (.nextCleanupBucket + 1) % numBuckets .mu.Unlock()}// addToBucket adds entry to expire bucket so that it will be cleaned up when the time comes. Has to be called with lock!func ( *LRU[, ]) ( *Entry[, ]) { := (numBuckets + .nextCleanupBucket - 1) % numBuckets .ExpireBucket = .buckets[].entries[.Key] = if .buckets[].newestEntry.Before(.ExpiresAt) { .buckets[].newestEntry = .ExpiresAt }}// removeFromBucket removes the entry from its corresponding bucket. Has to be called with lock!func ( *LRU[, ]) ( *Entry[, ]) {delete(.buckets[.ExpireBucket].entries, .Key)}// Cap returns the capacity of the cachefunc ( *LRU[, ]) () int {return .size}// Entry is an LRU EntrytypeEntry[ comparable, any] struct {// Next and previous pointers in the doubly-linked list of elements. // To simplify the implementation, internally a list l is implemented // as a ring, such that &l.root is both the next element of the last // list element (l.Back()) and the previous element of the first list // element (l.Front()). next, prev *Entry[, ]// The list to which this element belongs. list *LruList[, ]// The LRU Key of this element. Key // The Value stored with this element. Value // The time this element would be cleaned up, optional ExpiresAt time.Time// The expiry bucket item was put in, optional ExpireBucket uint8}// PrevEntry returns the previous list element or nil.func ( *Entry[, ]) () *Entry[, ] {if := .prev; .list != nil && != &.list.root {return }returnnil}// LruList represents a doubly linked list.// The zero Value for LruList is an empty list ready to use.typeLruList[ comparable, any] struct { root Entry[, ] // sentinel list element, only &root, root.prev, and root.next are used len int// current list Length excluding (this) sentinel element}// Init initializes or clears list l.func ( *LruList[, ]) () *LruList[, ] { .root.next = &.root .root.prev = &.root .len = 0return}// NewList returns an initialized list.func [ comparable, any]() *LruList[, ] { returnnew(LruList[, ]).Init() }// Length returns the number of elements of list l.// The complexity is O(1).func ( *LruList[, ]) () int { return .len }// Back returns the last element of list l or nil if the list is empty.func ( *LruList[, ]) () *Entry[, ] {if .len == 0 {returnnil }return .root.prev}// lazyInit lazily initializes a zero List Value.func ( *LruList[, ]) () {if .root.next == nil { .Init() }}// insert inserts e after at, increments l.len, and returns e.func ( *LruList[, ]) (, *Entry[, ]) *Entry[, ] { .prev = .next = .next .prev.next = .next.prev = .list = .len++return}// insertValue is a convenience wrapper for insert(&Entry{Value: v, ExpiresAt: ExpiresAt}, at).func ( *LruList[, ]) ( , , time.Time, *Entry[, ]) *Entry[, ] {return .insert(&Entry[, ]{Value: , Key: , ExpiresAt: }, )}// Remove removes e from its list, decrements l.lenfunc ( *LruList[, ]) ( *Entry[, ]) { .prev.next = .next .next.prev = .prev .next = nil// avoid memory leaks .prev = nil// avoid memory leaks .list = nil .len--return .Value}// move moves e to next to at.func ( *LruList[, ]) (, *Entry[, ]) {if == {return } .prev.next = .next .next.prev = .prev .prev = .next = .next .prev.next = .next.prev = }// PushFront inserts a new element e with value v at the front of list l and returns e.func ( *LruList[, ]) ( , ) *Entry[, ] { .lazyInit()return .insertValue(, , time.Time{}, &.root)}// PushFrontExpirable inserts a new expirable element e with Value v at the front of list l and returns e.func ( *LruList[, ]) ( , , time.Time) *Entry[, ] { .lazyInit()return .insertValue(, , , &.root)}// MoveToFront moves element e to the front of list l.// If e is not an element of l, the list is not modified.// The element must not be nil.func ( *LruList[, ]) ( *Entry[, ]) {if .list != || .root.next == {return }// see comment in List.Remove about initialization of l .move(, &.root)}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.