Go语言中LRU缓存机制的实现

 更新时间:2025年08月14日 10:53:00   作者:程序员爱钓鱼  
本文详解如何用Go语言实现LRU缓存,通过哈希表+双向链表组合确保Get和Put操作均为O(1),具有一定的参考价值,感兴趣的可以了解一下

在高性能服务开发中,缓存是提升访问速度和减少后端负载的重要手段。常见的缓存淘汰策略中,**LRU(Least Recently Used,最近最少使用)**是应用最广的一种。本篇我们用Go语言手写一个LRU缓存机制的模拟实现。

一、LRU缓存机制简介

1. 定义

LRU缓存是一种固定容量的缓存结构。当缓存已满时,它会淘汰最近最少使用的那个数据。简单理解:

谁最久没被访问,就先删除谁。

2. 使用场景

  • Web浏览器缓存
  • 数据库查询结果缓存
  • 操作系统页面置换

二、设计要求

LRU缓存应支持以下操作:

  1. Get(key):如果key存在,返回对应的value,并将该key标记为最近使用;否则返回-1。
  2. Put(key, value):插入或更新key。如果容量已满,需要删除最近最少使用的key。

要求两种操作都能在 O(1)  时间复杂度内完成。

三、核心数据结构

要实现 O(1) 操作,需要组合以下两种结构:

1. 哈希表(map)

  • 用于存储 key → 节点 的映射;
  • 可在 O(1) 时间内找到节点。

2. 双向链表

  • 用于维护数据访问的顺序;
  • 头部表示最近使用,尾部表示最久未使用;
  • 插入、删除节点都是 O(1)。

四、Go语言实现

1. 节点结构

type Node struct {
    key, value int
    prev, next *Node
}

2. LRU缓存结构

type LRUCache struct {
    capacity int
    cache    map[int]*Node
    head     *Node
    tail     *Node
}
  • head 和 tail 是哨兵节点(dummy),方便操作。

3. 初始化

func Constructor(capacity int) LRUCache {
    head := &Node{}
    tail := &Node{}
    head.next = tail
    tail.prev = head

    return LRUCache{
        capacity: capacity,
        cache:    make(map[int]*Node),
        head:     head,
        tail:     tail,
    }
}

4. 辅助方法

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

// removeNode:删除链表中的节点
func (l *LRUCache) removeNode(node *Node) {
    prev := node.prev
    next := node.next
    prev.next = next
    next.prev = prev
}

// addToHead:在头部插入节点
func (l *LRUCache) addToHead(node *Node) {
    node.prev = l.head
    node.next = l.head.next
    l.head.next.prev = node
    l.head.next = node
}

// removeTail:删除尾部节点并返回它
func (l *LRUCache) removeTail() *Node {
    node := l.tail.prev
    l.removeNode(node)
    return node
}

5. 核心操作

Get

func (l *LRUCache) Get(key int) int {
    if node, ok := l.cache[key]; ok {
        l.moveToHead(node)
        return node.value
    }
    return -1
}

Put

func (l *LRUCache) Put(key int, value int) {
    if node, ok := l.cache[key]; ok {
        node.value = value
        l.moveToHead(node)
    } else {
        newNode := &Node{key: key, value: value}
        l.cache[key] = newNode
        l.addToHead(newNode)

        if len(l.cache) > l.capacity {
            tail := l.removeTail()
            delete(l.cache, tail.key)
        }
    }
}

五、完整代码示例

package main

import "fmt"

type Node struct {
    key, value int
    prev, next *Node
}

type LRUCache struct {
    capacity int
    cache    map[int]*Node
    head     *Node
    tail     *Node
}

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

func (l *LRUCache) Get(key int) int {
    if node, ok := l.cache[key]; ok {
        l.moveToHead(node)
        return node.value
    }
    return -1
}

func (l *LRUCache) Put(key int, value int) {
    if node, ok := l.cache[key]; ok {
        node.value = value
        l.moveToHead(node)
    } else {
        newNode := &Node{key: key, value: value}
        l.cache[key] = newNode
        l.addToHead(newNode)
        if len(l.cache) > l.capacity {
            tail := l.removeTail()
            delete(l.cache, tail.key)
        }
    }
}

func (l *LRUCache) moveToHead(node *Node) {
    l.removeNode(node)
    l.addToHead(node)
}

func (l *LRUCache) removeNode(node *Node) {
    prev := node.prev
    next := node.next
    prev.next = next
    next.prev = prev
}

func (l *LRUCache) addToHead(node *Node) {
    node.prev = l.head
    node.next = l.head.next
    l.head.next.prev = node
    l.head.next = node
}

func (l *LRUCache) removeTail() *Node {
    node := l.tail.prev
    l.removeNode(node)
    return node
}

func main() {
    cache := Constructor(2)
    cache.Put(1, 1)
    cache.Put(2, 2)
    fmt.Println(cache.Get(1)) // 1
    cache.Put(3, 3)           // 淘汰 key=2
    fmt.Println(cache.Get(2)) // -1
    cache.Put(4, 4)           // 淘汰 key=1
    fmt.Println(cache.Get(1)) // -1
    fmt.Println(cache.Get(3)) // 3
    fmt.Println(cache.Get(4)) // 4
}

六、复杂度分析

  • 时间复杂度:O(1),Get 和 Put 都只涉及哈希表查找和链表操作。
  • 空间复杂度:O(capacity),存储固定大小的map和链表节点。

七、工程实践与优化

  1. 1. 线程安全
    在多协程环境中,需使用 sync.Mutex 或 sync.RWMutex 保证安全。
  2. 2. 泛型支持(Go1.18+)
    可以用泛型实现支持任意类型的key/value。
  3. 3. 监控统计
    可增加命中率统计、淘汰计数。

八、应用场景

  • 数据库缓存:Redis内部就支持LRU策略;
  • 浏览器缓存:网页资源加载优化;
  • API限速器:存储用户最近访问记录。

九、总结

  • LRU缓存结合了 哈希表 + 双向链表
  • 关键是 O(1) 时间内完成访问和淘汰
  • 该思想可扩展到 LFU、ARC 等高级缓存策略。

到此这篇关于Go语言中LRU缓存机制的实现的文章就介绍到这了,更多相关Go语言 LRU缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • golang中snappy的使用场合实例详解

    golang中snappy的使用场合实例详解

    在java 和go语言 大字符传达的时候, 采用snappy 压缩 解压缩是最好的方案。下面这篇文章主要给大家介绍了关于golang中snappy使用场合的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下。
    2017-12-12
  • go kratos源码及配置解析

    go kratos源码及配置解析

    这篇文章主要为大家介绍了go kratos源码及配置解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • GOPROXY:解决go get golang.org/x包失败问题

    GOPROXY:解决go get golang.org/x包失败问题

    这篇文章主要介绍了GOPROXY:解决go get golang.org/x包失败问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Go 文件读取和写入操作全面讲解

    Go 文件读取和写入操作全面讲解

    这篇文章主要为大家介绍了Go文件的读取和写入操作示例的全面详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • Go 中 time.After 可能导致的内存泄露问题解析

    Go 中 time.After 可能导致的内存泄露问题解析

    这篇文章主要介绍了Go 中 time.After 可能导致的内存泄露,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • golang 连接mongoDB的方法示例

    golang 连接mongoDB的方法示例

    这篇文章主要介绍了golang 连接mongoDB的方法示例,详细的介绍了golang的基础知识和连接mongoDB的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-11-11
  • Go中字符串处理 fmt.Sprintf与string.Builder的区别对比分析

    Go中字符串处理 fmt.Sprintf与string.Builder的区别对比分析

    在Go语言中,我们通常会遇到两种主要的方式来处理和操作字符串:使用fmt.Sprintf函数和string.Builder类型,本文给大家介绍它们在性能和用法上有一些关键区别,感兴趣的朋友跟随小编一起看看吧
    2023-11-11
  • Golang中的信号(Signal)机制详解

    Golang中的信号(Signal)机制详解

    Signal 是一种操作系统级别的事件通知机制,进程可以响应特定的系统信号,这些信号用于指示进程执行特定的操作,如程序终止、挂起、恢复等,Golang 的标准库 os/signal 提供了对信号处理的支持,本文将详细讲解 Golang 是如何处理和响应系统信号的,需要的朋友可以参考下
    2024-01-01
  • 如何在 ubuntu linux 上配置 go 语言的 qt 开发环境

    如何在 ubuntu linux 上配置 go 语言的 qt 开发环境

    这篇文章主要介绍了如何在 ubuntu linux 上配置 go 语言的 qt 开发环境,本文分步骤通过实例代码相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-04-04
  • Golang并发绕不开的重要组件之Channel详解

    Golang并发绕不开的重要组件之Channel详解

    Channel是一个提供可接收和发送特定类型值的用于并发函数通信的数据类型,也是Golang并发绕不开的重要组件之一,本文就来和大家深入聊聊Channel的相关知识吧
    2023-06-06

最新评论