initial version

This commit is contained in:
Sky Johnson 2025-05-24 09:29:51 -05:00
parent 222aa4da0d
commit ac292ced9b
3 changed files with 389 additions and 0 deletions

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.sharkk.net/Go/LRU
go 1.24.1

100
lru.go Normal file
View File

@ -0,0 +1,100 @@
package lru
import (
"sync"
)
type node struct {
key, value any
prev, next *node
}
type LRUCache struct {
capacity int
cache map[any]*node
head *node
tail *node
mu sync.RWMutex
}
func NewLRUCache(capacity int) *LRUCache {
head := &node{}
tail := &node{}
head.next = tail
tail.prev = head
return &LRUCache{
capacity: capacity,
cache: make(map[any]*node, capacity),
head: head,
tail: tail,
}
}
func (lru *LRUCache) Get(key any) (any, bool) {
lru.mu.Lock()
defer lru.mu.Unlock()
if node, exists := lru.cache[key]; exists {
lru.moveToHead(node)
return node.value, true
}
return nil, false
}
func (lru *LRUCache) Put(key, value any) {
lru.mu.Lock()
defer lru.mu.Unlock()
if node, exists := lru.cache[key]; exists {
node.value = value
lru.moveToHead(node)
return
}
newNode := &node{key: key, value: value}
lru.cache[key] = newNode
lru.addToHead(newNode)
if len(lru.cache) > lru.capacity {
removed := lru.removeTail()
delete(lru.cache, removed.key)
}
}
func (lru *LRUCache) moveToHead(node *node) {
lru.removeNode(node)
lru.addToHead(node)
}
func (lru *LRUCache) removeNode(node *node) {
node.prev.next = node.next
node.next.prev = node.prev
}
func (lru *LRUCache) addToHead(node *node) {
node.prev = lru.head
node.next = lru.head.next
lru.head.next.prev = node
lru.head.next = node
}
func (lru *LRUCache) removeTail() *node {
node := lru.tail.prev
lru.removeNode(node)
return node
}
func (lru *LRUCache) Len() int {
lru.mu.RLock()
defer lru.mu.RUnlock()
return len(lru.cache)
}
func (lru *LRUCache) Clear() {
lru.mu.Lock()
defer lru.mu.Unlock()
lru.cache = make(map[any]*node, lru.capacity)
lru.head.next = lru.tail
lru.tail.prev = lru.head
}

286
lru_test.go Normal file
View File

@ -0,0 +1,286 @@
package lru
import (
"sync"
"testing"
)
func TestNewLRUCache(t *testing.T) {
cache := NewLRUCache(5)
if cache.capacity != 5 {
t.Errorf("expected capacity 5, got %d", cache.capacity)
}
if cache.Len() != 0 {
t.Errorf("expected empty cache, got length %d", cache.Len())
}
}
func TestPutAndGet(t *testing.T) {
cache := NewLRUCache(3)
// Test Put and Get
cache.Put("a", 1)
cache.Put("b", 2)
cache.Put("c", 3)
val, exists := cache.Get("a")
if !exists || val != 1 {
t.Errorf("expected (1, true), got (%v, %v)", val, exists)
}
val, exists = cache.Get("b")
if !exists || val != 2 {
t.Errorf("expected (2, true), got (%v, %v)", val, exists)
}
// Test miss
val, exists = cache.Get("z")
if exists {
t.Errorf("expected (nil, false), got (%v, %v)", val, exists)
}
}
func TestUpdateExistingKey(t *testing.T) {
cache := NewLRUCache(2)
cache.Put("a", 1)
cache.Put("a", 10) // Update
val, exists := cache.Get("a")
if !exists || val != 10 {
t.Errorf("expected (10, true), got (%v, %v)", val, exists)
}
if cache.Len() != 1 {
t.Errorf("expected length 1, got %d", cache.Len())
}
}
func TestEviction(t *testing.T) {
cache := NewLRUCache(3)
cache.Put("a", 1)
cache.Put("b", 2)
cache.Put("c", 3)
cache.Put("d", 4) // Should evict "a"
_, exists := cache.Get("a")
if exists {
t.Error("expected 'a' to be evicted")
}
val, exists := cache.Get("b")
if !exists || val != 2 {
t.Errorf("expected (2, true), got (%v, %v)", val, exists)
}
}
func TestLRUOrdering(t *testing.T) {
cache := NewLRUCache(3)
cache.Put("a", 1)
cache.Put("b", 2)
cache.Put("c", 3)
// Access "a" to make it recently used
cache.Get("a")
// Add "d", should evict "b" (least recently used)
cache.Put("d", 4)
_, exists := cache.Get("b")
if exists {
t.Error("expected 'b' to be evicted")
}
// Verify others still exist
val, exists := cache.Get("a")
if !exists || val != 1 {
t.Errorf("expected (1, true), got (%v, %v)", val, exists)
}
val, exists = cache.Get("c")
if !exists || val != 3 {
t.Errorf("expected (3, true), got (%v, %v)", val, exists)
}
val, exists = cache.Get("d")
if !exists || val != 4 {
t.Errorf("expected (4, true), got (%v, %v)", val, exists)
}
}
func TestUpdateMakesRecent(t *testing.T) {
cache := NewLRUCache(3)
cache.Put("a", 1)
cache.Put("b", 2)
cache.Put("c", 3)
// Update "a" to make it recently used
cache.Put("a", 10)
// Add "d", should evict "b"
cache.Put("d", 4)
_, exists := cache.Get("b")
if exists {
t.Error("expected 'b' to be evicted")
}
val, exists := cache.Get("a")
if !exists || val != 10 {
t.Errorf("expected (10, true), got (%v, %v)", val, exists)
}
}
func TestClear(t *testing.T) {
cache := NewLRUCache(3)
cache.Put("a", 1)
cache.Put("b", 2)
cache.Put("c", 3)
cache.Clear()
if cache.Len() != 0 {
t.Errorf("expected empty cache after clear, got length %d", cache.Len())
}
_, exists := cache.Get("a")
if exists {
t.Error("expected cache to be empty after clear")
}
}
func TestSingleCapacity(t *testing.T) {
cache := NewLRUCache(1)
cache.Put("a", 1)
cache.Put("b", 2) // Should evict "a"
_, exists := cache.Get("a")
if exists {
t.Error("expected 'a' to be evicted")
}
val, exists := cache.Get("b")
if !exists || val != 2 {
t.Errorf("expected (2, true), got (%v, %v)", val, exists)
}
}
func TestDifferentTypes(t *testing.T) {
cache := NewLRUCache(3)
// Test with different key/value types
cache.Put(1, "one")
cache.Put("two", 2)
cache.Put(3.14, []int{1, 2, 3})
val, exists := cache.Get(1)
if !exists || val != "one" {
t.Errorf("expected ('one', true), got (%v, %v)", val, exists)
}
val, exists = cache.Get("two")
if !exists || val != 2 {
t.Errorf("expected (2, true), got (%v, %v)", val, exists)
}
val, exists = cache.Get(3.14)
if !exists {
t.Errorf("expected slice value, got (%v, %v)", val, exists)
}
}
func TestConcurrentAccess(t *testing.T) {
cache := NewLRUCache(100)
var wg sync.WaitGroup
// Concurrent puts
for i := range 50 {
wg.Add(1)
go func(i int) {
defer wg.Done()
cache.Put(i, i*10)
}(i)
}
// Concurrent gets
for i := range 50 {
wg.Add(1)
go func(i int) {
defer wg.Done()
cache.Get(i)
}(i)
}
// Concurrent updates
for i := range 25 {
wg.Add(1)
go func(i int) {
defer wg.Done()
cache.Put(i, i*100)
}(i)
}
// Concurrent len checks
for range 10 {
wg.Add(1)
go func() {
defer wg.Done()
cache.Len()
}()
}
// Concurrent clear
wg.Add(1)
go func() {
defer wg.Done()
cache.Clear()
}()
wg.Wait()
}
func TestRaceCondition(t *testing.T) {
cache := NewLRUCache(10)
done := make(chan bool)
go func() {
for i := range 1000 {
cache.Put(i%10, i)
}
done <- true
}()
go func() {
for i := range 1000 {
cache.Get(i % 10)
}
done <- true
}()
<-done
<-done
}
func BenchmarkPut(b *testing.B) {
cache := NewLRUCache(1000)
for i := 0; b.Loop(); i++ {
cache.Put(i%1000, i)
}
}
func BenchmarkGet(b *testing.B) {
cache := NewLRUCache(1000)
for i := range 1000 {
cache.Put(i, i)
}
for i := 0; b.Loop(); i++ {
cache.Get(i % 1000)
}
}