Help us shape the future of Go! Take the 2026 Go Developer Survey

cache

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 11 Imported by: 0

README

cache

github.com/474420502/cache —— 生产可用的进程内缓存原语:后台定时/条件刷新 + 非阻塞读取 + 容量受限 LRU。

  • 纯 Go 标准库实现,零第三方依赖
  • 全部类型线程安全,泛型 API
  • 读路径无锁(一次原子加载),慢速数据源不影响读性能
  • 刷新失败保留旧值(stale-while-error),错误可见、可回调
  • 并发刷新自动合并(singleflight),防缓存击穿
  • go test -race / 96%+ 覆盖率 / 基准测试齐备

设计理念

  1. 读不阻塞:Get / Snapshot 只做一次 atomic.Pointer 加载,不抢锁、不发网络请求。
  2. 后台刷新:由唯一后台协程按 Interval 或手动触发刷新,读到的永远是"上一次刷新好的快照"。
  3. 失败保留旧值:刷新失败(含 Loader panic)时沿用上一次成功的数据,同时把错误写进快照并触发回调。
  4. 条件更新:SetParams 换数据源参数;Condition 决定本次自动刷新是否真的执行(例如只在工作时间刷新),跳过不污染成功时间。
  5. 合并并发拉取:同一时刻的多个刷新/加载请求合并成一次数据源调用。
  6. 生命周期确定:Close 幂等且可等待;Loader panic 不会打死后台协程;可选 WithAutoStop 在对象被 GC 时自动收尾。

本质:定期更新数据

整个库的核心只有一件事:把数据源按时间/条件定期拉回来,发布成一个不可变快照,让读侧随时能拿到"最近一次成功的数据",且读路径永不阻塞。

AsyncCache[T] 里的 T 就是数据形态,常见三种,不需要为它们各写一个类型:

数据形态 T 读法
单个值(配置对象、版本号、开关) Config c.Get()
列表 / 切片(榜单、全量行、页面内容) []Row c.Get()
集合(按 key 查,最常用) map[K]V rows := c.Get(); rows[k]

集合形态有两条约定,守住就是无锁无撕裂:

  1. Loader 每次返回一个全新的 map(不要复用、不要原地改),旧 map 在下一次发布后自然成为垃圾;
  2. 读侧只读不写。
c := cache.NewAsync(time.Minute, func(ctx *cache.Context[map[string]User]) (map[string]User, error) {
	return db.LoadAllUsers(ctx.Ctx()) // 一次拉回整张表
})
defer c.Close()
_ = c.WaitFirst(ctx)

users := c.Get() // 一份不可变快照
u, ok := users["u1"]

这套写法由 TestAsyncCache_DatasetSnapshot(8 个读协程 + 每秒刷新,-race 下验证)和 ExampleAsyncCache_dataset 保证。什么时候不该用它:需要按 key 淘汰 / 单 key TTL / 负缓存时,说明数据不是一个整体,改用 CacheLRU(或 AsyncCache 管全量 + CacheLRU 管按 ID 查询)。

环境

项 值
Go 版本 go 1.27.1(模块声明与本机工具链一致)
第三方依赖 无
测试 go test -race ./...
go get github.com/474420502/cache

两种缓存怎么选

AsyncCache[T] CacheLRU[K,V]
场景 定期/条件远程更新:爬虫页面、配置表、榜单、远程表数据 减少频繁操作:按 key 查询、去重、会话
数据形态 全局一份最新值 按 key 的多份值
刷新方式 后台定时/条件/手动 惰性:读时未命中才加载
读语义 永不阻塞,可能读到旧值 命中即返回;未命中会等加载
容量 不淘汰(只保留一份快照) 容量受限 + TTL

两者可以组合:用 AsyncCache 维护"热表全量",用 CacheLRU 缓存"按 ID 查询的结果"。


AsyncCache:定时/条件后台刷新

快速开始
c := cache.NewAsync(30*time.Second, func(ctx *cache.Context[[]Row]) ([]Row, error) {
	// ctx 带超时与取消,可直接传给 HTTP/DB
	return fetchRows(ctx.Ctx(), ctx.Params())
}, cache.WithTimeout(5*time.Second), cache.WithOnError(func(err error) {
	log.Println("刷新失败,继续用旧值:", err)
}))
defer c.Close()

_ = c.WaitFirst(ctx) // 可选:等首次刷新结束(成功或失败)

rows := c.Get() // 无锁读,永不阻塞;失败时是上一次成功的值
选项
选项 说明
WithInterval(d) 后台刷新间隔;<= 0 表示只做首次 + 手动刷新。也可以用 NewAsync(interval, ...) 的位置参数。固定延迟语义:下次刷新在本次结束后才计时,慢数据源不会堆积刷新
WithTimeout(d) 单次刷新超时,通过传给 Loader 的 context 实现
WithJitter(ratio) 间隔抖动比例 [0,1],避免多实例同时刷新(惊群)
WithLazyStart() 不立即刷新首次,等第一个间隔或手动触发
WithParams(p) 初始参数(SetParams 可在运行期替换)
WithCondition(f) 自动刷新前置条件,见下
WithOnError(f) 失败回调(简单版)
WithAutoStop() 对象被 GC 回收时自动关闭后台协程

也可以直接写匿名函数:NewAsync(d, loader, func(o *cache.Options) { o.Timeout = time.Second })。

Loader 与 Context
type Loader[T any] func(ctx *Context[T]) (T, error)
Context 方法 说明
Ctx() / Deadline() / Done() 本次刷新的 context,Loader 必须尊重它(否则超时和 Close 都无法及时生效)
Params() 当前参数
Current() / GetCurrent() 本次刷新开始前的值(做增量计算用)
HasCurrent() 之前是否已有成功的值
Snapshot() 本次刷新开始前的完整快照
Error() 本次刷新的错误,仅在错误回调中非 nil

Loader 在后台刷新协程中串行执行:不要在 Loader 或错误回调里调用 Close。

快照与统计
type Snapshot[T any] struct {
	Value       T
	Version     uint64    // 成功刷新次数
	Loaded      bool      // 是否至少成功过一次
	UpdatedAt   time.Time // 最近一次成功时间
	RefreshedAt time.Time // 最近一次尝试时间(失败也会更新)
	Err         error     // 最近一次刷新错误
}
  • OK():最近一次刷新成功(值是新鲜的)
  • Stale():曾经成功过,但最近一次失败(正在用旧值)
  • Stats():Refreshes / Failures / Skipped / ConditionErrors / ConsecutiveFailures / LastAttempt / LastSuccess / LastError / Closed
条件更新
// 只在 09:30-15:00 之间自动刷新;首次刷新和手动 Refresh 不受限制
c := cache.NewAsync(time.Minute, loader, cache.WithCondition(func(info cache.RefreshInfo) bool {
	h, m, _ := time.Now().Clock()
	return (h == 9 && m >= 30) || (h > 9 && h < 15)
}))

Condition 返回 false 时:不调用 Loader、快照与成功时间不变、Stats().Skipped 递增。 运行期可用 SetCondition(nil) 取消限制,Condition 自身 panic 会被捕获并计入 Stats().ConditionErrors。

方法速查
方法 说明
Get() / GetWithOK() / ValueOr(def) / HasValue() 无锁读
Snapshot() / View(f) / GetSync(f) 读取快照或元信息
Stats() 运行统计
Trigger() 请求一次异步刷新(会合并、受 Condition 约束),返回是否被接受
Refresh(ctx) 同步刷新,不受 Condition 约束,并发调用会合并;关闭后返回 ErrClosed
ForceUpdate() Refresh(context.Background()) 的旧写法
WaitFirst(ctx) / WaitFirstUpdate(d) 等首次刷新尝试结束
WaitValue(ctx) 等第一个成功的值;一直失败则返回最后一次错误
Next(ctx) 等"下一次刷新完成",用于响应式更新本地视图
SetParams(p) / Params() 数据源参数
SetError(f) / SetErrorHandler(f) 错误回调(简单 / 带 Context)
SetCondition(f) / SetInterval(d) / Interval() / Timeout() / Jitter() 运行期调参
Close() / CloseWithContext(ctx) / Done() / Closed() 关闭与等待;Close 幂等
关闭与自动回收

后台协程不会自行退出,请在不再使用时 defer c.Close();Close 会等待后台协程退出(Loader 尊重 context 时是有界的),关闭后 Get 仍能读到最后一次快照。

WithAutoStop() 通过 runtime.SetFinalizer 在对象被 GC 时自动关闭协程,用于"忘记 Close"的兜底,但 finalizer 不保证在进程退出前执行,生产代码仍应显式 Close。


CacheLRU:容量受限 + TTL + 加载穿透

快速开始
lru := cache.NewCacheLRUWithTTL[int, User](1<<16, time.Minute)

lru.Set(1, u)             // 默认 TTL
lru.SetWithTTL(2, u, 0)   // 永不过期
v, ok := lru.Get(1)       // 命中会刷新 LRU 顺序
lru.Peek(1)               // 只看,不改变顺序
lru.Remove(1)
lru.Cleanup()             // 主动清理过期元素(可配合定时器)
  • 批量淘汰:容量溢出时一次性淘汰最久未用的 1/8(至少 1 个),用批量链表操作摊还成本。代价是元素个数会在 [Capacity-batch+1, Capacity] 之间波动,但永不超过容量。
  • 惰性过期:TTL 在访问时判定;长期不被访问的过期元素会占用容量直到被淘汰,需要精确回收就周期调用 Cleanup()。
  • 统计:Hits / Misses / Evictions / Expirations / Loads / LoadErrors,ResetStats() 清零。
GetOrLoad:防缓存击穿
user, err := lru.GetOrLoad(ctx, userID, func(ctx context.Context) (User, error) {
	return db.QueryUser(ctx, userID) // 同一 key 的并发请求只会执行一次
})
  • 同一 key 的并发加载合并为一次 loader 调用,结果(含 error)共享给所有等待者;
  • 等待者可以用自己的 ctx 超时退出,不会拖住共享加载;
  • loader panic 会被转成 error,不会让等待者永久阻塞;
  • loader 在锁外执行,但不要在 loader 内对同一 key 再次 GetOrLoad(会等待自己),也不要调用本缓存的其它方法;
  • 失败不写入缓存,下次调用会重试(需要负缓存时自行写入占位值 + TTL)。
分片:高并发下降锁竞争
lru := cache.NewShardedCacheLRU[int, User](1<<16, time.Minute, 16)
  • 按 key 哈希分片(hash/maphash,种子随机),同一 key 始终落在同一分片,GetOrLoad 的单飞语义不变;
  • 淘汰变成"分片内 LRU",容量平均分到各分片并向上取整,因此 Capacity() 可能略大于构造参数;
  • 分片数会向上取整到 2 的幂且不超过容量。
方法速查

Get Peek Set SetWithTTL SetIfAbsent Remove Clear Cleanup Len Size Capacity Shards Keys Values Range All GetOrLoad GetOrLoadWithTTL Stats ResetStats DefaultTTL

All() 返回标准迭代器(Go 1.23+),可以直接 range:

for key, value := range lru.All() {
	if key == target {
		break // 提前退出会正常释放锁
	}
}

Range / All 的循环体在持有分片锁时执行,里面不要再调用本缓存的其它方法。


性能(本机实测,AMD Ryzen 7 7700 / 16 线程)

go test -run XXX -bench . -benchtime=300ms .

基准 结果 说明
AsyncCache.Get(单协程) ≈ 1.0 ns/op 含一次 noinline 消费;读路径就是一次原子加载
AsyncCache.GetParallel ≈ 0.10 ns/op 16 线程并发读,近线性扩展(读路径无锁、无共享写)
AsyncCache.Snapshot ≈ 2.8 ns/op 读取完整快照结构体的拷贝成本
CacheLRU.GetOnly(单分片,并发) ≈ 89 ns/op 单锁竞争下的最坏情况
ShardedCacheLRU.GetOnly(16 分片,并发) ≈ 24 ns/op 分片后吞吐提升约 3.8 倍
CacheLRU.GetOrLoad(命中) ≈ 95 ns/op 与 Get 同价

另外,TestAsyncCache_ReadsNeverBlock 会在 Loader 阻塞期间连续读 5000 次,验证读路径不被慢数据源拖住。

生产实践建议

  1. 一定设置 WithTimeout:网络类 Loader 的兜底;否则下游挂起会让 Close 一直等待。
  2. Loader 尊重 ctx:把 ctx.Ctx() 透传给 HTTP/DB 客户端,超时与取消才能真正生效。
  3. T 视为只读:快照发布后不会再被缓存修改;如果 T 是 map/slice/指针,由 Loader 负责不并发修改(或在业务侧加锁/复制)。
  4. 错误回调要轻:它在刷新协程里同步执行,禁止阻塞、禁止调用 Close。
  5. 参数变更后主动刷新:SetParams(p) 之后调用 Trigger() 或 Refresh(ctx) 让新参数立即生效。
  6. AsyncCache 用于"全局一份最新值";按 key 的缓存请用 CacheLRU,避免自己维护 map 的并发与淘汰。

从旧版本迁移

旧 API 现状
cache.New(interval, UpdateMehtod) + Value() 改为 NewAsync(interval, Loader) + Get();支持泛型与 context
SetShare 改为 SetParams / Params
SetOnUpdateError 改为 SetError / SetErrorHandler / WithOnError
ForceUpdate() / GetUpdate() 改为 Refresh(ctx) / Snapshot().UpdatedAt
CacheBlock(惰性阻塞缓存) 用 CacheLRU.GetOrLoad 等价替代,且带 TTL 与单飞
AsyncSymbiosis / StartLoopWith 已删除(原实现是忙循环 + 阻塞式 Stop,会泄漏协程);后台循环内建,用 Close() 管理
NewAsyncCache(handler) 保留兼容,等价 NewAsync(DefaultInterval, handler, opts...);建议改用 NewAsync 明确间隔(旧实现无间隔、满速空转)
AsyncCacheContext[T] 仍是 Context[T] 的别名,GetCurrent() / Params() / Error() 保持可用

旧 CacheLRU 的批量淘汰在 Set 溢出路径上会误删刚插入的节点,并且淘汰后容量不可预期,这些问题在当前实现中已修复并有随机操作不变量测试覆盖。

目录结构

.
├── doc.go                # 包文档(设计理念 / 数据形态)
├── options.go            # AsyncCache 选项 / Condition
├── snapshot.go           # Snapshot / Stats
├── async_cache.go        # AsyncCache + 后台刷新循环 + 单飞合并
├── lru.go                # CacheLRU(分片 / TTL / GetOrLoad / 迭代器)
├── async_cache_test.go   # 定时语义用例(testing/synctest 假时钟)
├── lru_test.go           # LRU 用例(注入时钟 + 不变量 + 竞态)
├── example_test.go       # 可执行文档示例(go test 校验输出)
└── example/main.go       # 可运行的综合演示:go run ./example

测试与 Go 版本特性

特性 用在哪
testing/synctest(Go 1.24+ 稳定) 所有定时相关用例跑在假时钟里:时间只在气泡内所有协程阻塞时推进,于是"第几次刷新""抖动区间""超时耗时"都是精确断言,不用 sleep 轮询,也不会因机器负载 flaky。整套测试 < 2s。
iter.Seq2(Go 1.23+) CacheLRU.All() 支持 for k, v := range
Timer 新语义(Go 1.23+) 后台循环里 Stop/Reset 不再需要"读干"旧值
泛型类型别名 AsyncCacheContext[T] = Context[T],旧代码零改动
hash/maphash.Comparable(Go 1.24+) 分片 LRU 对任意 comparable key 做哈希

并发/竞态用例(-race)、GC/finalizer 用例、以及读路径的墙钟基准保留在真实时间与真实调度下运行。

开发
gofmt -l .                                    # 格式
go vet ./...                                  # 静态检查
go test -race -cover ./...                    # 测试 + 竞态 + 覆盖率 (96%+)
go test -race -count=5 .                      # 反复跑, 验证无 flaky
go test -run XXX -bench . -benchtime=300ms .  # 基准
go run ./example                              # 综合演示

Documentation

Overview

Package cache 提供生产可用的进程内缓存原语: 无第三方依赖, 全部类型线程安全.

设计理念

  1. 读不阻塞: 读路径只做一次原子加载, 不抢锁, 不等待网络; 慢速数据源不影响读性能.
  2. 后台刷新: 由后台协程按时间间隔或手动触发刷新, 读到的永远是"上一次刷新好的快照".
  3. 失败保留旧值: 刷新失败(含 Loader panic)时沿用上一次成功的数据, 同时把错误 写进快照 Snapshot.Err 并回调错误钩子, 即 stale-while-error.
  4. 条件更新: 通过 SetParams 改变数据源参数, 或用 Condition 决定本次是否真的拉取 (例如只在工作时间刷新), 跳过时快照保持不变, 也不会污染成功时间.
  5. 合并并发拉取: 同一时刻的多个刷新请求合并为一次 Loader 调用, 防止缓存击穿.
  6. 生命周期确定: Close 幂等且可等待; 可选 WithAutoStop 在对象被 GC 回收时自动关闭 后台协程(依赖 runtime finalizer, 不保证退出前一定执行, 推荐显式 Close).

两种缓存

  • AsyncCache: 面向"定期/条件远程更新"的场景(爬虫页面, 配置表, 榜单, 远程表数据). 后台按 Interval 刷新, 业务侧 Get 拿到最近一次成功的值, 从不阻塞.
  • CacheLRU: 面向"减少频繁操作"的场景(按 key 查询/去重/会话). 容量受限, 支持 TTL, GetOrLoad 提供带单飞(singleflight)的加载穿透.

数据形态

库的核心只有一件事: 定期/按条件把数据源拉回来, 发布成不可变快照, 读侧永不阻塞. AsyncCache 的类型参数 T 就是数据形态:

T = Config         单个值(配置对象/版本号)
T = []Row          列表(榜单/全量行/页面内容)
T = map[K]V        集合(按 key 查, 最常用)

集合形态请让 Loader 每次返回一个全新的 map 并保证读侧只读, 这样无需任何锁:

c := cache.NewAsync(time.Minute, func(ctx *cache.Context[map[string]User]) (map[string]User, error) {
	return db.LoadAllUsers(ctx.Ctx())
})
users := c.Get() // 不可变快照
u, ok := users["u1"]

需要按 key 淘汰/单 key TTL/负缓存时, 说明数据不是一个整体, 改用 CacheLRU.

快速开始

// 每 30 秒刷新一次远程表; 读永远不阻塞
c := cache.NewAsync(30*time.Second, func(ctx *cache.Context[[]Row]) ([]Row, error) {
	return fetchRows(ctx.Ctx(), ctx.Params())
}, cache.WithTimeout(5*time.Second))
defer c.Close()

rows := c.Get() // 拿到最近一次成功的快照, 失败时是旧值

更多示例见 example/ 目录与 readme.md.

Index

Examples

Constants

View Source
const DefaultInterval = time.Minute

DefaultInterval 是 NewAsyncCache 未显式指定间隔时使用的刷新间隔.

Variables

View Source
var ErrClosed = errors.New("cache: async cache closed")

ErrClosed 表示 AsyncCache 已经关闭.

Functions

This section is empty.

Types

type AsyncCache added in v1.3.0

type AsyncCache[T any] struct {
	// contains filtered or unexported fields
}

AsyncCache 是"后台刷新 + 非阻塞读取"的快照缓存.

读接口 Get/Snapshot 只做一次原子加载, 因此永远不会被慢速数据源阻塞; 后台按 Interval 刷新, 失败时保留上一次成功的值。

请使用 NewAsync(或兼容的 NewAsyncCache) 创建实例, 并在不再使用时 Close。

Example

ExampleAsyncCache 演示后台定时刷新 + 非阻塞读取。

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewAsync(time.Second, func(ctx *cache.Context[string]) (string, error) {
		return "v1", nil
	})
	defer c.Close()

	// 等首次刷新完成; 生产中也可以不等待, 直接读旧值/空值。
	_ = c.WaitFirst(context.Background())
	fmt.Println(c.Get())
	fmt.Println(c.Snapshot().Loaded)

}
Output:
v1
true
Example (Condition)

ExampleAsyncCache_condition 演示"条件更新": 只在条件满足时自动刷新。

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewAsync(10*time.Millisecond, func(ctx *cache.Context[int]) (int, error) {
		return 42, nil
	}, cache.WithCondition(func(info cache.RefreshInfo) bool {
		return false // 例如: 只在交易时段刷新
	}))
	defer c.Close()

	_ = c.WaitFirst(context.Background())
	deadline := time.Now().Add(2 * time.Second)
	for c.Stats().Skipped == 0 && time.Now().Before(deadline) {
		time.Sleep(time.Millisecond)
	}
	fmt.Println(c.Get())
	fmt.Println(c.Stats().Skipped > 0)
	fmt.Println(c.Snapshot().Version) // 条件为假 -> 版本不再增长

}
Output:
42
true
1
Example (Dataset)

ExampleAsyncCache_dataset 演示"定期更新一个数据集合"的推荐形态: T = map[K]V。

Loader 每次返回一个全新的 map, 读侧只读不写, 因此天生无锁、无撕裂; 旧的 map 在下一次快照发布后自然变成垃圾。

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/474420502/cache"
)

func main() {
	loadAllRows := func(ctx *cache.Context[map[string]int]) (map[string]int, error) {
		return map[string]int{"alice": 1, "bob": 2}, nil // 真实场景: 一次拉回整张表
	}

	c := cache.NewAsync(time.Minute, loadAllRows)
	defer c.Close()
	_ = c.WaitFirst(context.Background())

	rows := c.Get() // 一份不可变快照
	fmt.Println(len(rows), rows["bob"], rows["nobody"])

}
Output:
2 2 0

func NewAsync added in v1.3.0

func NewAsync[T any](interval time.Duration, loader Loader[T], opts ...Option) *AsyncCache[T]

NewAsync 创建一个后台刷新的快照缓存并立即启动刷新循环。

interval <= 0 表示不做定时刷新, 只做首次刷新与手动刷新(Trigger/Refresh)。
opts 中的 Interval 会覆盖 interval 参数。

后台协程不会自行结束, 除非调用 Close(或启用 WithAutoStop)。

func NewAsyncCache deprecated added in v1.3.0

func NewAsyncCache[T any](loader Loader[T], opts ...Option) *AsyncCache[T]

NewAsyncCache 等同于 NewAsync(DefaultInterval, loader, opts...), 用于兼容旧代码。

Deprecated: 请使用 NewAsync 明确指定刷新间隔(旧实现是"疯狂循环"刷新, 没有间隔)。

func (*AsyncCache[T]) Close added in v1.3.0

func (c *AsyncCache[T]) Close() error

Close 关闭后台刷新协程并等待其退出(幂等)。

关闭后 Get/Snapshot 仍可读到最后一次快照, Refresh 返回 ErrClosed。 若 Loader 不尊重 ctx 且未设置 Timeout, Close 会一直等待。 不要在 Loader 或错误回调里调用 Close。

func (*AsyncCache[T]) CloseWithContext added in v1.3.0

func (c *AsyncCache[T]) CloseWithContext(ctx context.Context) error

CloseWithContext 关闭后台刷新协程, 最多等待 ctx 结束。

func (*AsyncCache[T]) Closed added in v1.3.0

func (c *AsyncCache[T]) Closed() bool

Closed 报告是否已经 Close。

func (*AsyncCache[T]) Done added in v1.3.0

func (c *AsyncCache[T]) Done() <-chan struct{}

Done 返回的通道在后台刷新循环退出后关闭。

func (*AsyncCache[T]) ForceUpdate added in v1.3.0

func (c *AsyncCache[T]) ForceUpdate()

ForceUpdate 是 Refresh(context.Background()) 的旧 API 写法, 会阻塞到刷新完成。

func (*AsyncCache[T]) Get added in v1.3.0

func (c *AsyncCache[T]) Get() T

Get 返回最近一次成功刷新的值. 读路径无锁, 永不阻塞。 若从未成功刷新过, 返回 T 的零值(可用 HasValue 或 Snapshot 区分)。

func (*AsyncCache[T]) GetSync added in v1.3.0

func (c *AsyncCache[T]) GetSync(f func(current T))

GetSync 用当前值调用 f. 兼容旧 API; 值本身是只读快照, f 不会被缓存内部加锁。

func (*AsyncCache[T]) GetWithOK added in v1.3.0

func (c *AsyncCache[T]) GetWithOK() (T, bool)

GetWithOK 返回最近一次成功刷新的值以及是否有效(即是否成功刷新过)。

func (*AsyncCache[T]) HasValue added in v1.3.0

func (c *AsyncCache[T]) HasValue() bool

HasValue 报告是否至少成功刷新过一次。

func (*AsyncCache[T]) Interval added in v1.3.0

func (c *AsyncCache[T]) Interval() time.Duration

Interval 返回当前刷新间隔。

func (*AsyncCache[T]) Jitter added in v1.3.0

func (c *AsyncCache[T]) Jitter() float64

Jitter 返回当前间隔抖动比例。

func (*AsyncCache[T]) Next added in v1.3.0

func (c *AsyncCache[T]) Next(ctx context.Context) (Snapshot[T], error)

Next 返回当前快照, 并阻塞到"下一次刷新完成"(成功或失败)或 ctx 结束。 返回的第二个 error 只表示等待本身失败(ctx 结束/缓存关闭), 刷新错误在快照里。

func (*AsyncCache[T]) Params added in v1.3.0

func (c *AsyncCache[T]) Params() any

Params 返回当前参数。

func (*AsyncCache[T]) Refresh added in v1.3.0

func (c *AsyncCache[T]) Refresh(ctx context.Context) (Snapshot[T], error)

Refresh 同步刷新一次并返回新快照, 不受 Condition 约束, 用于"强制拉取"。

并发调用会合并为一次 Loader 调用(单飞); 等待者若 ctx 先结束则返回 ctx.Err()。 缓存已关闭时返回 ErrClosed。

func (*AsyncCache[T]) SetCondition added in v1.3.0

func (c *AsyncCache[T]) SetCondition(cond Condition)

SetCondition 设置自动刷新的前置条件; 传 nil 表示不限制。

func (*AsyncCache[T]) SetError added in v1.3.0

func (c *AsyncCache[T]) SetError(handler func(ctx *Context[T]))

SetError 设置刷新失败回调(旧 API)。回调里可用 ctx.Error() 取错误。

func (*AsyncCache[T]) SetErrorHandler added in v1.3.0

func (c *AsyncCache[T]) SetErrorHandler(handler ErrorHandler[T])

SetErrorHandler 设置刷新失败回调, 回调在刷新协程中同步执行。 回调自身 panic 会被捕获, 不会影响后台刷新。

func (*AsyncCache[T]) SetInterval added in v1.3.0

func (c *AsyncCache[T]) SetInterval(interval time.Duration)

SetInterval 动态修改刷新间隔(<=0 表示停止定时刷新, 仍可手动刷新)。

func (*AsyncCache[T]) SetParams added in v1.3.0

func (c *AsyncCache[T]) SetParams(params any)

SetParams 替换传给 Loader 的参数. 下一次刷新即可看到新参数; 需要立刻生效时再调用 Trigger 或 Refresh。

func (*AsyncCache[T]) Snapshot added in v1.3.0

func (c *AsyncCache[T]) Snapshot() Snapshot[T]

Snapshot 返回当前快照(值 + 版本 + 时间 + 错误)。

func (*AsyncCache[T]) Stats added in v1.3.0

func (c *AsyncCache[T]) Stats() Stats

Stats 返回运行统计。

func (*AsyncCache[T]) Timeout added in v1.3.0

func (c *AsyncCache[T]) Timeout() time.Duration

Timeout 返回当前单次刷新超时。

func (*AsyncCache[T]) Trigger added in v1.3.0

func (c *AsyncCache[T]) Trigger() bool

Trigger 请求一次异步刷新并立即返回。

返回 false 表示请求已被合并(已有待处理的刷新)或缓存已关闭。 与手动 Refresh 不同, Trigger 触发的刷新仍然受 Condition 约束。

func (*AsyncCache[T]) ValueOr added in v1.3.0

func (c *AsyncCache[T]) ValueOr(def T) T

ValueOr 在已有值(无论是否过期)时返回缓存值, 否则返回 def。

func (*AsyncCache[T]) View added in v1.3.0

func (c *AsyncCache[T]) View(f func(s Snapshot[T]))

View 用当前快照调用 f, 可以看到 Version/UpdatedAt/Err 等元信息。

func (*AsyncCache[T]) WaitFirst added in v1.3.0

func (c *AsyncCache[T]) WaitFirst(ctx context.Context) error

WaitFirst 阻塞到首次刷新"尝试"完成(成功或失败都算), 用于拿到确定性的初值。

func (*AsyncCache[T]) WaitFirstUpdate added in v1.3.0

func (c *AsyncCache[T]) WaitFirstUpdate(waitTime time.Duration) error

WaitFirstUpdate 是 WaitFirst 带超时的旧 API, 兼容旧错误文案。

func (*AsyncCache[T]) WaitValue added in v1.3.0

func (c *AsyncCache[T]) WaitValue(ctx context.Context) (T, error)

WaitValue 阻塞到出现第一个成功的值, 返回该值与 nil。 期间刷新一直失败时返回最后一次错误; ctx 结束或缓存关闭也会返回。

type AsyncCacheContext added in v1.3.0

type AsyncCacheContext[T any] = Context[T]

AsyncCacheContext 是 Context 的旧名字, 保留以兼容旧代码.

type CacheLRU added in v1.3.0

type CacheLRU[K comparable, V any] struct {
	// contains filtered or unexported fields
}

CacheLRU 是线程安全、容量受限的 LRU 缓存, 支持 TTL 与带单飞的加载穿透。

实现要点:

  • 命中时把节点移到表尾(MRU); 容量溢出时一次性淘汰表头的 1/8(至少 1 个), 用批量淘汰摊还链表操作成本, 代价是容量会在 [Capacity-batch+1, Capacity] 波动。
  • TTL 采用惰性过期: 访问时判定, 另外可用 Cleanup 主动清理(适合配合定时器)。
  • GetOrLoad 用 per-key 单飞防止缓存击穿: 同一 key 的并发加载只调用一次 loader, 共享结果; 等待者可以自己用 ctx 超时退出。
  • 内部没有后台协程, 不会因为忘记释放而泄漏。

默认构造的是单分片(严格全局 LRU)。高并发场景可以用 NewShardedCacheLRU 按 key 分片, 把锁竞争拆开: 分片后每个分片独立淘汰, 容量被平均分到各分片, 因此淘汰顺序是 "分片内 LRU" 而不是全局 LRU, 总容量也会按分片向上取整而略大于请求值。 同一 key 始终落在同一分片, 因此 GetOrLoad 的单飞语义不受影响。

Example

ExampleCacheLRU 演示基本读写与容量淘汰。

package main

import (
	"fmt"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewCacheLRU[int, string](4)

	for i := 1; i <= 5; i++ {
		c.Set(i, fmt.Sprintf("v%d", i))
	}

	_, ok := c.Get(1)
	fmt.Println("key 1 还在吗:", ok)
	vals := c.Values()
	fmt.Printf("最新 key 5 排第一: %s, 共 %d 个\n", vals[0], len(vals))

}
Output:
key 1 还在吗: false
最新 key 5 排第一: v5, 共 4 个

func NewCacheLRU added in v1.3.0

func NewCacheLRU[K comparable, V any](capacity int) *CacheLRU[K, V]

NewCacheLRU 创建一个容量为 capacity 的单分片 LRU 缓存, 元素永不过期。 capacity 必须大于 0, 否则 panic。

func NewCacheLRUWithTTL added in v1.3.0

func NewCacheLRUWithTTL[K comparable, V any](capacity int, ttl time.Duration) *CacheLRU[K, V]

NewCacheLRUWithTTL 创建一个容量为 capacity 的单分片 LRU 缓存, Set 写入的元素 默认在 ttl 后过期; ttl <= 0 表示不过期。可用 SetWithTTL 为单个元素指定 TTL。

func NewShardedCacheLRU added in v1.3.0

func NewShardedCacheLRU[K comparable, V any](capacity int, ttl time.Duration, shards int) *CacheLRU[K, V]

NewShardedCacheLRU 创建分片 LRU 缓存, 用于降低高并发下的锁竞争。

shards <= 1 时等价于单分片; 实际分片数会被向上取整到 2 的幂(便于位与取模), 并且不会超过 capacity。容量会平均分到各分片, 每个分片至少 1。

Example

ExampleNewShardedCacheLRU 演示分片容量分配。

package main

import (
	"fmt"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewShardedCacheLRU[int, string](64, 0, 4)
	defer c.Clear()
	fmt.Println("分片数:", c.Shards())
	fmt.Println("总容量:", c.Capacity())

}
Output:
分片数: 4
总容量: 64

func (*CacheLRU[K, V]) All added in v1.3.0

func (l *CacheLRU[K, V]) All() iter.Seq2[K, V]

All 返回标准迭代器, 与 range 语法配合 (Go 1.23+):

for key, value := range lru.All() {
	if key == target {
		break // 提前退出会正常释放锁
	}
}

顺序与 Range 一致; 迭代期间持有分片锁, 因此循环体内不要调用本缓存的其它方法。

Example

ExampleCacheLRU_All 演示 Go 1.23+ 的迭代器。

package main

import (
	"fmt"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewCacheLRU[int, string](8)
	c.Set(1, "one")
	c.Set(2, "two")

	for key, value := range c.All() {
		fmt.Println(key, value)
	}

}
Output:
2 two
1 one

func (*CacheLRU[K, V]) Capacity added in v1.3.0

func (l *CacheLRU[K, V]) Capacity() int

Capacity 返回容量上限. 分片时是各分片容量之和(按分片向上取整, 可能略大于构造参数)。

func (*CacheLRU[K, V]) Cleanup added in v1.3.0

func (l *CacheLRU[K, V]) Cleanup() int

Cleanup 主动删除所有已过期元素并返回删除数量, 适合配合定时器周期调用。

func (*CacheLRU[K, V]) Clear added in v1.3.0

func (l *CacheLRU[K, V]) Clear()

Clear 清空全部元素. 进行中的 GetOrLoad 不受影响, 其返回值仍会写入缓存。 统计计数不会被重置, 需要时调用 ResetStats。

func (*CacheLRU[K, V]) DefaultTTL added in v1.3.0

func (l *CacheLRU[K, V]) DefaultTTL() time.Duration

DefaultTTL 返回构造时指定的默认 TTL。

func (*CacheLRU[K, V]) Get added in v1.3.0

func (l *CacheLRU[K, V]) Get(key K) (V, bool)

Get 返回值并把该 key 标记为最近使用。已过期的元素视为不存在并被惰性删除。

func (*CacheLRU[K, V]) GetOrLoad added in v1.3.0

func (l *CacheLRU[K, V]) GetOrLoad(ctx context.Context, key K, loader func(ctx context.Context) (V, error)) (V, error)

GetOrLoad 先查缓存, 未命中时调用 loader 加载并写入缓存(TTL 取默认值)。

同一 key 的并发调用只会触发一次 loader; 等待者如果 ctx 先结束会返回 ctx.Err()。 加载失败(含 panic)不会写入缓存, 返回的 error 会共享给所有等待者。

loader 在锁外执行, 但不要在 loader 内对同一 key 再次调用 GetOrLoad(会等待自己), 也不要在 loader 内调用本缓存的其它方法。

Example

ExampleCacheLRU_GetOrLoad 演示加载穿透时的并发合并(singleflight)。

package main

import (
	"context"
	"fmt"
	"sync/atomic"
	"time"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewCacheLRUWithTTL[int, string](16, time.Minute)
	defer c.Clear()

	var queries atomic.Int64
	done := make(chan string, 8)
	for i := 0; i < 8; i++ {
		go func() {
			v, _ := c.GetOrLoad(context.Background(), 1, func(ctx context.Context) (string, error) {
				queries.Add(1)
				time.Sleep(20 * time.Millisecond)
				return "row", nil
			})
			done <- v
		}()
	}
	for i := 0; i < 8; i++ {
		<-done
	}

	fmt.Println("8 个并发只查询了", queries.Load(), "次")

}
Output:
8 个并发只查询了 1 次

func (*CacheLRU[K, V]) GetOrLoadWithTTL added in v1.3.0

func (l *CacheLRU[K, V]) GetOrLoadWithTTL(ctx context.Context, key K, ttl time.Duration, loader func(ctx context.Context) (V, error)) (V, error)

GetOrLoadWithTTL 与 GetOrLoad 相同, 但为本次加载指定 TTL。

func (*CacheLRU[K, V]) Keys added in v1.3.0

func (l *CacheLRU[K, V]) Keys() []K

Keys 返回全部未过期的 key。

单分片时按最近使用到最久未用的顺序返回; 分片时按分片拼接, 全局顺序无意义。

func (*CacheLRU[K, V]) Len added in v1.3.0

func (l *CacheLRU[K, V]) Len() int

Len 返回当前元素个数。

func (*CacheLRU[K, V]) Peek added in v1.3.0

func (l *CacheLRU[K, V]) Peek(key K) (V, bool)

Peek 与 Get 相同, 但不改变 LRU 顺序. 命中/未命中同样计入统计。

func (*CacheLRU[K, V]) Range added in v1.3.0

func (l *CacheLRU[K, V]) Range(f func(key K, value V) bool)

Range 遍历未过期元素, f 返回 false 时停止。

单分片时按最近使用到最久未用的顺序遍历; 分片时按分片遍历, 全局顺序无意义。 f 在持有分片锁的情况下执行, 因此 f 内不要调用本缓存的其它方法。

func (*CacheLRU[K, V]) Remove added in v1.3.0

func (l *CacheLRU[K, V]) Remove(key K) (V, bool)

Remove 删除元素并返回旧值与是否存在。

func (*CacheLRU[K, V]) ResetStats added in v1.3.0

func (l *CacheLRU[K, V]) ResetStats()

ResetStats 把命中/淘汰等计数清零(不影响缓存内容)。

func (*CacheLRU[K, V]) Set added in v1.3.0

func (l *CacheLRU[K, V]) Set(key K, value V)

Set 写入元素, TTL 取构造时的默认值, 并把该 key 标记为最近使用。

func (*CacheLRU[K, V]) SetIfAbsent added in v1.3.0

func (l *CacheLRU[K, V]) SetIfAbsent(key K, value V) bool

SetIfAbsent 仅在 key 不存在(或已过期)时写入, 返回是否写入成功。

func (*CacheLRU[K, V]) SetWithTTL added in v1.3.0

func (l *CacheLRU[K, V]) SetWithTTL(key K, value V, ttl time.Duration)

SetWithTTL 写入元素并指定 TTL; ttl <= 0 表示永不过期。

func (*CacheLRU[K, V]) Shards added in v1.3.0

func (l *CacheLRU[K, V]) Shards() int

Shards 返回分片数。

func (*CacheLRU[K, V]) Size added in v1.3.0

func (l *CacheLRU[K, V]) Size() int

Size 是 Len 的旧名字, 保留以兼容旧代码。

func (*CacheLRU[K, V]) Stats added in v1.3.0

func (l *CacheLRU[K, V]) Stats() LRUStats

Stats 返回累计统计。

func (*CacheLRU[K, V]) Values added in v1.3.0

func (l *CacheLRU[K, V]) Values() []V

Values 返回全部未过期的值, 顺序语义同 Keys。

type Condition added in v1.3.0

type Condition func(info RefreshInfo) bool

Condition 决定一次自动刷新是否真的执行.

type Context added in v1.3.0

type Context[T any] struct {
	// contains filtered or unexported fields
}

Context 是一次刷新的上下文. 所有方法都可以安全并发调用.

func (*Context[T]) Ctx added in v1.3.0

func (c *Context[T]) Ctx() context.Context

Ctx 返回本次刷新的 context, 可直接传给 HTTP/DB 等调用.

func (*Context[T]) Current added in v1.3.0

func (c *Context[T]) Current() T

Current 返回本次刷新开始前的值; 首次刷新(或从未成功过)时是 T 的零值.

func (*Context[T]) Deadline added in v1.3.0

func (c *Context[T]) Deadline() (time.Time, bool)

Deadline 等价于 c.Ctx().Deadline().

func (*Context[T]) Done added in v1.3.0

func (c *Context[T]) Done() <-chan struct{}

Done 等价于 c.Ctx().Done().

func (*Context[T]) Error added in v1.3.0

func (c *Context[T]) Error() error

Error 返回本次刷新的错误, 仅在错误回调中非 nil.

func (*Context[T]) GetCurrent added in v1.3.0

func (c *Context[T]) GetCurrent() T

GetCurrent 是 Current 的旧名字, 保留以兼容旧代码.

func (*Context[T]) HasCurrent added in v1.3.0

func (c *Context[T]) HasCurrent() bool

HasCurrent 报告本次刷新开始前是否已经有成功的值.

func (*Context[T]) Params added in v1.3.0

func (c *Context[T]) Params() any

Params 返回当前参数(由 SetParams / WithParams 设置).

func (*Context[T]) Snapshot added in v1.3.0

func (c *Context[T]) Snapshot() Snapshot[T]

Snapshot 返回本次刷新开始前的快照.

type ErrorHandler added in v1.3.0

type ErrorHandler[T any] func(ctx *Context[T], err error)

ErrorHandler 是刷新失败时的回调, err 同时可以通过 ctx.Error() 获取.

type LRUStats added in v1.3.0

type LRUStats struct {
	Len         int    // 当前元素个数
	Capacity    int    // 容量上限(分片时是各分片之和)
	Shards      int    // 分片数
	Hits        uint64 // Get/Peek/GetOrLoad 命中
	Misses      uint64 // 未命中(含已过期)
	Evictions   uint64 // 因容量淘汰
	Expirations uint64 // 因 TTL 过期被惰性删除
	Loads       uint64 // GetOrLoad 成功加载
	LoadErrors  uint64 // GetOrLoad 失败(含 panic)
}

LRUStats 是 CacheLRU 的累计统计(自创建或上次 ResetStats 起)。

type Loader added in v1.3.0

type Loader[T any] func(ctx *Context[T]) (T, error)

Loader 是刷新数据的函数.

ctx 携带本次刷新的上下文: Cancel/Deadline、参数 Params、上一次的值 Current 与 上一次的快照 Snapshot。返回的 error 非 nil 时不会覆盖旧值, 只会记录错误。

Loader 会在后台刷新协程中串行执行, 因此实现里不要调用 Close; 另外应当尊重 ctx 的取消与超时(WithTimeout), 否则 Close 与超时都无法及时生效。

也不要在 Loader 内调用 Refresh/ForceUpdate: 那会等待自己发起的那次刷新。

type Option added in v1.3.0

type Option func(*Options)

Option 用于配置 AsyncCache:

c := cache.NewAsync(time.Minute, loader,
	cache.WithTimeout(5*time.Second),
	cache.WithJitter(0.2),
)

也可以直接写匿名函数:

c := cache.NewAsync(time.Minute, loader, func(o *cache.Options) {
	o.Timeout = 5 * time.Second
})

func WithAutoStop added in v1.3.0

func WithAutoStop() Option

WithAutoStop 表示对象被 GC 回收时自动关闭后台协程.

func WithCondition added in v1.3.0

func WithCondition(cond Condition) Option

WithCondition 设置自动刷新的前置条件.

func WithInterval added in v1.3.0

func WithInterval(d time.Duration) Option

WithInterval 设置后台刷新间隔.

func WithJitter added in v1.3.0

func WithJitter(ratio float64) Option

WithJitter 设置间隔抖动比例, 取值 [0,1].

func WithLazyStart added in v1.3.0

func WithLazyStart() Option

WithLazyStart 表示构造后不立即刷新, 等第一个间隔或手动触发.

func WithOnError added in v1.3.0

func WithOnError(f func(err error)) Option

WithOnError 设置刷新失败回调.

func WithParams added in v1.3.0

func WithParams(params any) Option

WithParams 设置传给 Loader 的初始参数.

func WithTimeout added in v1.3.0

func WithTimeout(d time.Duration) Option

WithTimeout 设置单次刷新的超时.

type Options added in v1.3.0

type Options struct {
	// Interval 是后台自动刷新的间隔. <=0 表示不做定时刷新, 只做首次刷新与手动刷新,
	// 适合"按事件/条件驱动"的场景.
	//
	// 语义是"固定延迟": 下一次刷新在本次刷新结束之后才开始计时, 因此慢数据源不会
	// 造成刷新堆积; 若一次刷新耗时超过间隔, 实际间隔就是刷新耗时。
	Interval time.Duration

	// Timeout 是单次刷新的超时时间, <=0 表示不限制.
	// 超时通过传给 Loader 的 context 实现, 因此 Loader 必须尊重 ctx.
	Timeout time.Duration

	// Jitter 是间隔抖动比例, 取值 [0,1]. 实际间隔在
	// [Interval*(1-Jitter/2), Interval*(1+Jitter/2)) 之间随机,
	// 用于避免大量实例在同一时刻集中刷新(惊群).
	Jitter float64

	// Lazy 为 true 时不立即刷新首次, 等到第一个间隔或手动触发.
	Lazy bool

	// AutoStop 为 true 时, AsyncCache 被 GC 回收时自动关闭后台协程.
	// 依赖 runtime.SetFinalizer, 不保证进程退出前一定执行, 推荐显式 Close.
	AutoStop bool

	// Params 是传给 Loader 的初始参数, 运行期可用 SetParams 替换.
	Params any

	// Condition 只在自动刷新(定时/Trigger 之外的后台刷新)时生效:
	// 返回 false 表示本次跳过, 快照与成功时间保持不变. 手动 Refresh 不受其约束.
	Condition Condition

	// OnError 在刷新失败(含 Loader panic)时回调. 回调在刷新协程中同步执行,
	// 不要在其中做阻塞操作, 也不要调用 Close.
	OnError func(err error)
}

Options 是 AsyncCache 的配置集合. 零值语义: Interval=0(不自动刷新, 仅首次+手动), Timeout=0(不限制), Jitter=0, Lazy=false(立即刷新首次), AutoStop=false, Params=nil, Condition=nil, OnError=nil.

type RefreshInfo added in v1.3.0

type RefreshInfo struct {
	Now                 time.Time     // 当前时间
	Params              any           // 当前参数
	Interval            time.Duration // 当前刷新间隔
	Version             uint64        // 已成功刷新次数
	Loaded              bool          // 是否已有值
	UpdatedAt           time.Time     // 上次成功时间
	RefreshedAt         time.Time     // 上次尝试时间
	Err                 error         // 上次刷新错误
	ConsecutiveFailures uint64        // 连续失败次数
}

RefreshInfo 描述一次自动刷新之前的缓存状态, 供 Condition 判断.

type Snapshot added in v1.3.0

type Snapshot[T any] struct {
	Value       T         // 最近一次成功刷新的值
	Version     uint64    // 成功刷新的次数, 从 0 开始递增
	Loaded      bool      // 是否至少成功刷新过一次(Value 是否有效)
	UpdatedAt   time.Time // 最近一次成功刷新的时间
	RefreshedAt time.Time // 最近一次尝试刷新的时间(即使失败也会更新)
	Err         error     // 最近一次刷新的错误; 成功时为 nil
}

Snapshot 是 AsyncCache 在某个时刻对外的只读视图.

快照一旦发布就不会再被修改; 若 T 是引用类型(切片/指针/map), 其内部状态由 Loader 负责保证不可变, 或者由调用方自行加锁.

func (Snapshot[T]) Age added in v1.3.0

func (s Snapshot[T]) Age(now time.Time) time.Duration

Age 返回自最近一次成功刷新以来经过的时间. 从未成功刷新过时返回 0.

func (Snapshot[T]) OK added in v1.3.0

func (s Snapshot[T]) OK() bool

OK 报告快照是否持有有效且未过期的值, 即最近一次刷新成功.

func (Snapshot[T]) Stale added in v1.3.0

func (s Snapshot[T]) Stale() bool

Stale 报告当前是否正在使用旧值: 曾经成功过, 但最近一次刷新失败了.

type Stats added in v1.3.0

type Stats struct {
	Version             uint64    // 当前成功刷新次数
	Refreshes           uint64    // 成功刷新次数
	Failures            uint64    // 失败刷新次数(含 Loader panic)
	Skipped             uint64    // 被 Condition 跳过的自动刷新次数
	ConditionErrors     uint64    // Condition panic 次数
	ConsecutiveFailures uint64    // 连续失败次数, 成功后归零
	LastAttempt         time.Time // 最近一次尝试刷新
	LastSuccess         time.Time // 最近一次成功刷新
	LastError           error     // 最近一次刷新错误
	Closed              bool      // 是否已 Close
}

Stats 是 AsyncCache 的运行统计. 字段都在一次原子读中获取, 可能存在极小的 时间偏差.

Directories

Path Synopsis
Command example 演示 cache 的典型用法。
Command example 演示 cache 的典型用法。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL