背景
container/ring 在 Go 标准库里不算高频,但它很值得专门读一遍,因为它代表了一个非常明确的数据结构思路:循环链表。
和常见切片、双向链表相比,它不是“通用默认选择”,但在固定窗口轮转、循环调度这类场景里很自然。
核心概念
创建一个长度为 3 的环:
1r := ring.New(3)
每个节点都有:
- 当前节点值
Value - 前驱节点
- 后继节点
但它和普通链表的最大区别是:
- 没有真正意义上的头尾
- 最后一个节点的下一个节点会回到第一个
为什么叫 Ring
因为它在逻辑上不是一条线,而是一个闭环。你从任意位置开始都可以遍历整个结构。
常用方法
Next / Prev
向后或向前移动:
1r = r.Next()
2r = r.Prev()
Move
一次移动多步:
1r = r.Move(2)
Link / Unlink
用于拼接两个环,或者从当前环里摘掉一段元素。
这是 ring.Ring 比单纯“循环遍历”更有意思的地方,因为它支持结构层面的局部重组。
示例代码
下面是一个最基本的环初始化和遍历:
1package main
2
3import (
4 "container/ring"
5 "fmt"
6)
7
8func main() {
9 r := ring.New(3)
10 for i := 0; i < 3; i++ {
11 r.Value = i + 1
12 r = r.Next()
13 }
14
15 r.Do(func(v any) {
16 fmt.Println(v)
17 })
18}
为什么 Do 很方便
因为环没有天然“终点”,如果你手写循环,需要自己控制遍历次数;Do 把这个常见需求直接封装掉了。
什么时候适合用它
我会把 ring.Ring 视为“有明确循环语义”的数据结构,而不是普通容器替代品。
比较合适的场景:
- 轮转调度
- 固定位置循环访问
- 某些缓存 / 滑动窗口逻辑的链式表达
不太适合的场景:
- 频繁随机访问
- 需要简单直接下标操作
- 普通列表处理
这些时候切片通常更合适。
一些容易忽略的点
Value 是 any
你需要自己保证放进去的数据类型和取出来时的断言一致。
ring 变量本身只是“当前指针”
同一个环里,r 指向哪个节点,会影响你后续从哪里开始观察这个环。
它不强调头节点
如果你的业务逻辑强依赖“头”和“尾”,那 ring.Ring 可能不是最自然的建模方式。
我的总结
ring.Ring是标准库里的循环链表实现- 它最适合带轮转语义的场景,而不是普通列表场景
Next、Move、Do是最常用入口- 真正使用时,要意识到“当前指针位置”本身就是状态的一部分
- 它不是高频容器,但很适合用来训练自己对数据结构语义的判断