Go语言入门实战-从0到1实现LRU

大家好,我是gopher_looklook,现任某独角兽企业后端开发工程师,喜欢钻研Go源码,发掘各项技术在大型Go微服务项目中的最佳实践,期待与各位牛友多多交流,一起进步!

LRU设计描述

设计和构建一个`最近最少使用(Least Recently Used)缓存,该缓存会删除最近最少使用的元素。缓存应该从键映射到值(允许插入和检索特定键对应的值),并在初始化时指定最大容量。该缓存应该支持以下操作: 获取数据Get(key)和写入数据Put(key, value)。可以假定缓存元素的键值都是string类型。获取数据时,如果Key存在,则获取其缓存值Value,如果不存在则返回空字符串,并给出exist标识。写入数据时,如果key不存在,则写入其数据值。当缓存容量达到上限时,应该在写入新数据之前删除最近最少使用的元素,从而为新的数据腾出空间。

思路分析

根据题目描述,这是一个新手入门级别的简易LRU实现。题目的关键在于创建一个LRU对象,并为这个对象实现一个Get的方法和Put的方法。这两个方法的函数签名如下:

func (*LRUCache) Get(key string) (string, bool)

对于Get方法,每次调用时需要传入一个key,并返回这个key对应的数据value。如果key不存在,应该给出一个bool标识,表示这个key并没有存在于缓存中。

func (*LRUCache) Put(key string, value string)

对于Put方法,每次调用时,需要传入一个key和一个value。当key已经存在于缓存当中时,则用新value值覆盖旧数据;当key不存在缓存当中时,则将新键值对插入到缓存当中。插入之前,需要识别到缓存是否已经达到最大容量(capacity)。如果达到最大容量,需要先删除掉最近最少使用的元素,腾出空间插入新的键值元素。

流程图

参考代码

  • lru.go
package lru

// Node 定义双向链表的节点
type Node struct {
    key   string
    value string
    prev  *Node
    next  *Node
}

// LRUCache 定义LRU缓存结构
type LRUCache struct {
    capacity int
    cache    map[string]*Node
    head     *Node
    tail     *Node
}

// NewLRUCache 初始化LRU缓存
func NewLRUCache(capacity int) *LRUCache {
    lru := &LRUCache{
       capacity: capacity,
       cache:    make(map[string]*Node),
       head:     &Node{key: "", value: ""}, // 虚拟头节点
       tail:     &Node{key: "", value: ""}, // 虚拟尾节点
    }
    // 初始化时,头尾节点互相连接
    lru.head.next = lru.tail
    lru.tail.prev = lru.head
    return lru
}

// removeNode 从链表中移除一个节点
func (lru *LRUCache) removeNode(node *Node) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

// addToHead 将节点添加到链表头部
func (lru *LRUCache) addToHead(node *Node) {
    node.prev = lru.head
    node.next = lru.head.next
    lru.head.next.prev = node
    lru.head.next = node
}

// moveToHead 将节点移动到链表头部
func (lru *LRUCache) moveToHead(node *Node) {
    lru.removeNode(node)
    lru.addToHead(node)
}

// removeTail 移除链表尾部的节点
func (lru *LRUCache) removeTail() *Node {
    node := lru.tail.prev
    lru.removeNode(node)
    return node
}

// Get 获取缓存中的值
func (lru *LRUCache) Get(key string) (string, bool) {
    if node, ok := lru.cache[key]; ok {
       lru.moveToHead(node)
       return node.value, true
    }
    return "", false
}

// Put 写入缓存
func (lru *LRUCache) Put(key string, value string) {
    if lru.capacity <= 0 {
       return // 如果容量为0,直接返回
    }

    if node, ok := lru.cache[key]; ok {
       node.value = value
       lru.moveToHead(node)
    } else {
       if len(lru.cache) == lru.capacity {
          tail := lru.removeTail()
          delete(lru.cache, tail.key)
       }
       newNode := &Node{key: key, value: value}
       lru.cache[key] = newNode
       lru.addToHead(newNode)
    }
}

单元测试

  • lru_test.go
package lru

import (
    "testing"
)

// TestLRUCache 测试LRU缓存的基本功能
func TestLRUCache(t *testing.T) {
    lru := NewLRUCache(2) // 初始化缓存容量为2

    // 测试1: 插入和获取数据
    lru.Put("key1", "value1")
    lru.Put("key2", "value2")

    value, exist := lru.Get("key1")
    if !exist || value != "value1" {
       t.Errorf("Get key1 failed, expected: value1, got: %s", value)
    }

    value, exist = lru.Get("key2")
    if !exist || value != "value2" {
       t.Errorf("Get key2 failed, expected: value2, got: %s", value)
    }

    // 测试2: 插入新数据,触发淘汰
    lru.Put("key3", "value3") // 插入key3,应淘汰key1

    value, exist = lru.Get("key1")
    if exist {
       t.Errorf("Get key1 should fail, but it exists with value: %s", value)
    }

    value, exist = lru.Get("key3")
    if !exist || value != "value3" {
       t.Errorf("Get key3 failed, expected: value3, got: %s", value)
    }

    // 测试3: 更新已存在的数据
    lru.Put("key2", "new_value2") // 更新key2的值
    value, exist = lru.Get("key2")
    if !exist || value != "new_value2" {
       t.Errorf("Get key2 failed, expected: new_value2, got: %s", value)
    }

    // 测试4: 插入新数据,触发淘汰
    lru.Put("key4", "value4") // 插入key4,应淘汰key3

    value, exist = lru.Get("key3")
    if exist {
       t.Errorf("Get key3 should fail, but it exists with value: %s", value)
    }

    value, exist = lru.Get("key4")
    if !exist || value != "value4" {
       t.Errorf("Get key4 failed, expected: value4, got: %s", value)
    }
}

// TestLRUCacheEdgeCases 测试LRU缓存的边界情况
func TestLRUCacheEdgeCases(t *testing.T) {
    // 测试1: 缓存容量为0
    lru := NewLRUCache(0)
    lru.Put("key1", "value1")
    value, exist := lru.Get("key1")
    if exist {
       t.Errorf("Get key1 should fail for zero capacity cache, but it exists with value: %s", value)
    }

    // 测试2: 缓存容量为1
    lru = NewLRUCache(1)
    lru.Put("key1", "value1")
    value, exist = lru.Get("key1")
    if !exist || value != "value1" {
       t.Errorf("Get key1 failed, expected: value1, got: %s", value)
    }

    lru.Put("key2", "value2") // 插入key2,应淘汰key1
    value, exist = lru.Get("key1")
    if exist {
       t.Errorf("Get key1 should fail, but it exists with value: %s", value)
    }

    value, exist = lru.Get("key2")
    if !exist || value != "value2" {
       t.Errorf("Get key2 failed, expected: value2, got: %s", value)
    }
}

总结

本文介绍了一个简易LRU缓存的Go语言实现。利用流程图和代码,从理论到实践,帮助各位小伙伴由浅入深地掌握LRU缓存的核心概念、设计思路以及实现细节。如果这篇博客对你有所启发,欢迎点赞+关注,你的支持是我创作的最大动力!

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务