使用Go语言实现LRU缓存的代码详解

 更新时间:2024年11月01日 09:43:37   作者:tatasix  
在日常开发中,缓存是提高系统性能的重要手段,LRU缓存是一种基于“最近最少使用”策略的缓存系统,其目的是在空间受限的情况下保留最新、最常用的数据,本文将详细讲解如何使用 Go 语言实现一个 LRU 缓存,需要的朋友可以参考下

引言

在日常开发中,缓存是提高系统性能的重要手段。LRU(Least Recently Used)缓存是一种基于“最近最少使用”策略的缓存系统,其目的是在空间受限的情况下保留最新、最常用的数据。当缓存空间不足时,LRU 缓存会优先淘汰最久未被使用的数据,从而保持缓存的时效性。本文将详细讲解如何使用 Go 语言实现一个 LRU 缓存。

LRU 缓存的关键特性

  • 快速访问缓存内容Get 操作的时间复杂度为 (O(1))。
  • 快速插入和更新缓存Put 操作的时间复杂度也为 (O(1))。
  • 淘汰最久未使用的数据:当缓存满时,移除最久未访问的数据。

数据结构选型

为了实现 LRU 缓存的上述特性,常用的数据结构组合为 哈希表 和 双向链表

  • 哈希表:用于快速访问缓存节点。
  • 双向链表:管理节点的访问顺序。每次访问时,将节点移动到链表头部;当缓存满时,移除链表尾部节点(即最久未访问的数据)。

通过这种组合,Get 和 Put 的时间复杂度均为 (O(1))。

LRU 缓存的结构设计

在 LRU 缓存的设计中,我们需要以下两个核心组件:

  1. 双向链表节点 Node

    • 存储缓存的 key 和 value
    • 通过 prev 和 next 指针指向前后节点。
  2. LRUCache 缓存结构

    • capacity:缓存的容量。
    • cache:使用 map[int]*Node 作为哈希表,存储键值对和链表节点的映射。
    • head 和 tail:虚拟头尾节点,用于链表的边界处理,避免在插入和删除操作时对边界条件进行额外判断。

操作流程图

下面是 LRU 缓存的整体操作流程概览:

代码实现

1. 定义节点和缓存结构

我们首先定义双向链表节点 Node 和 LRUCache 结构:

package main

import "fmt"

// 双向链表的节点
type Node struct {
	key, value int
	prev, next *Node
}

// LRUCache 缓存结构
type LRUCache struct {
	capacity   int
	cache      map[int]*Node // 哈希表,快速定位节点
	head, tail *Node         // 虚拟头尾节点
}

2. 初始化 LRU 缓存

在 Constructor 方法中,初始化缓存容量 capacity 和哈希表 cache,并创建 head 和 tail 虚拟节点。head 和 tail 之间没有数据节点,它们仅用于简化节点插入和删除的边界处理。此时,链表初始状态如下:

    head <--> tail

初始化代码如下:

// 构造函数
func Constructor(capacity int) LRUCache {
	cache := LRUCache{
		capacity: capacity,
		cache:    make(map[int]*Node),
		head:     &Node{},
		tail:     &Node{},
	}
	cache.head.next = cache.tail
	cache.tail.prev = cache.head
	return cache
}

3. 获取缓存值(Get 方法)

Get 方法根据 key 查找缓存中的数据。如果数据存在,则将该节点移到链表头部,标记为“最近使用”;如果数据不存在,则返回 -1。

// 获取缓存中的值
func (this *LRUCache) Get(key int) int {
	if node, found := this.cache[key]; found {
		this.moveToHead(node) // 访问后移至头部
		return node.value
	}
	return -1 // 如果不存在,返回 -1
}

在调用 moveToHead 方法时,节点被移动到链表头部。假设我们在链表中有节点顺序:head <-> A <-> B <-> tail,访问 B 后,链表状态变为:

head <--> B <--> A <--> tail

4. 更新或插入值(Put 方法)

Put 方法根据 key 更新值,或在缓存中插入新的键值对。如果缓存超过容量限制,则移除链表尾部的节点。

// 更新或插入值
func (this *LRUCache) Put(key int, value int) {
	if node, found := this.cache[key]; found {
		node.value = value
		this.moveToHead(node) // 已存在的节点移至头部
	} else {
		// 创建新节点并加入头部
		newNode := &Node{key: key, value: value}
		this.cache[key] = newNode
		this.addNode(newNode)

		// 超出容量时,删除尾节点
		if len(this.cache) > this.capacity {
			tail := this.popTail()
			delete(this.cache, tail.key)
		}
	}
}

当缓存满时,popTail 方法删除链表尾部节点。假设链表当前状态为:head <-> B <-> A <-> tail,插入新节点 C 后,链表状态变为:

    head <--> C <--> B <--> tail

节点 A 被淘汰,从而控制了缓存的空间限制。

5. 辅助方法

addNoderemoveNodemoveToHead 和 popTail 是缓存核心操作的辅助方法,用于管理链表中节点的插入、删除和移动。

// 添加节点至头部
func (this *LRUCache) addNode(node *Node) {
	node.prev = this.head
	node.next = this.head.next
	this.head.next.prev = node
	this.head.next = node
}

// 删除节点
func (this *LRUCache) removeNode(node *Node) {
	prev := node.prev
	next := node.next
	prev.next = next
	next.prev = prev
}

// 移动节点到头部
func (this *LRUCache) moveToHead(node *Node) {
	this.removeNode(node)
	this.addNode(node)
}

// 弹出尾部节点
func (this *LRUCache) popTail() *Node {
	tail := this.tail.prev
	this.removeNode(tail)
	return tail
}

插入节点到链表头部的图示

addNode 方法的核心步骤如下:假设链表初始状态为 head <-> A <-> B <-> tail,插入新节点 node 到 head 后,链表状态变为:

    head <--> node <--> A <--> B <--> tail

6. 单元测试代码

为验证实现正确性,可以使用以下测试:

import "testing"

func TestLRUCache(t *testing.T) {
	cache := Constructor(2)

	cache.Put(1, 1)
	cache.Put(2, 2)
	if cache.Get(1) != 1 {
		t.Errorf("Expected 1, got %d", cache.Get(1))
	}

	cache.Put(3, 3) // 淘汰 key 2
	if cache.Get(2) != -1 {
		t.Errorf("Expected -1, got %d", cache.Get(2))
	}

	cache.Put(4, 4) // 淘汰 key 1
	if cache.Get(1) != -1 {
		t.Errorf("Expected -1, got %d", cache.Get(1))
	}
	if cache.Get(3) != 3 {
		t.Errorf("Expected 3, got %d", cache.Get(3))
	}
	if cache.Get(4) != 4 {
		t.Errorf("Expected 4, got %d", cache.Get(4))
	}
}

总结

通过双向链表和哈希表的结合,实现了一个高效的 LRU 缓存,使 Get和 Put 操作在 O(1) 的时间内完成。双向链表和虚拟节点的设计简化了链表边界的处理,广泛适用于缓存系统中。

以上就是使用Go语言实现LRU缓存的代码详解的详细内容,更多关于Go实现LRU缓存的资料请关注脚本之家其它相关文章!

相关文章

  • gorm结构体零值不更新问题及解决方案

    gorm结构体零值不更新问题及解决方案

    在使用GORM进行更新操作时,如果结构体中的零值或空值不被更新,可以通过结合结构体和map来解决,具体方法是:在更新时,将需要更新的字段放入map中,并判断字段值是否为零值或空值,如果是,则将该字段值设置为空字符串或0,此外,更新单行数据时,必须在map中包含主键赋值
    2025-12-12
  • Go项目怎么使用枚举

    Go项目怎么使用枚举

    枚举是一种很重要的数据类型,本文主要介绍了Go项目怎么使用枚举,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Go语言实现百万级WebSocket连接架构设计及服务优化

    Go语言实现百万级WebSocket连接架构设计及服务优化

    本文将详细介绍如何在Go中构建一个能够支持百万级WebSocket连接的服务,包括系统架构设计、性能优化策略以及具体的实现步骤和代码示例
    2024-01-01
  • 解读unsafe.Pointer和uintptr的区别

    解读unsafe.Pointer和uintptr的区别

    这篇文章主要介绍了解读unsafe.Pointer和uintptr的区别及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • 协同开发巧用gitignore中间件避免网络请求携带登录信息

    协同开发巧用gitignore中间件避免网络请求携带登录信息

    这篇文章主要为大家介绍了协同开发巧用gitignore中间件避免网络请求携带登录信息
    2022-06-06
  • Go语言题解LeetCode268丢失的数字示例详解

    Go语言题解LeetCode268丢失的数字示例详解

    这篇文章主要为大家介绍了Go语言题解LeetCode268丢失的数字示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • 一文带你了解Go中的内存对齐

    一文带你了解Go中的内存对齐

    一旦涉及到较为底层的编程,特别是与硬件交互,内存对齐是一个必修的课题,所以这篇文章小编就想来和大家聊一聊Go语言中的内存对齐,希望对大家有所帮助
    2023-10-10
  • 一文教你如何用好GO语言变长参数

    一文教你如何用好GO语言变长参数

    对于函数重载相信编码过的 xdm 肯定不会陌生,那么我们一起分别来看看 C 语言,C++ 语言,GO 语言 如何去模拟和使用重载,感兴趣的可以学习一下
    2023-09-09
  • GO中GORM 使用教程

    GO中GORM 使用教程

    GORM是Go语言的ORM库,它将关系型数据库表与Go语言结构体映射,提供便捷的数据库操作,本文就来介绍有下GO中GORM 使用教程,具有一定的参考价值,感兴趣的可以了解一下
    2025-02-02
  • Golang JSON的进阶用法实例讲解

    Golang JSON的进阶用法实例讲解

    这篇文章主要给大家介绍了关于Golang JSON进阶用法的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用golang具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-09-09

最新评论