背景

container/ring 在 Go 标准库里不算高频,但它很值得专门读一遍,因为它代表了一个非常明确的数据结构思路:循环链表。

和常见切片、双向链表相比,它不是“通用默认选择”,但在固定窗口轮转、循环调度这类场景里很自然。

核心概念

创建一个长度为 3 的环:

1r := ring.New(3)

每个节点都有:

  • 当前节点值 Value
  • 前驱节点
  • 后继节点

但它和普通链表的最大区别是:

  • 没有真正意义上的头尾
  • 最后一个节点的下一个节点会回到第一个

为什么叫 Ring

因为它在逻辑上不是一条线,而是一个闭环。你从任意位置开始都可以遍历整个结构。

常用方法

Next / Prev

向后或向前移动:

1r = r.Next()
2r = r.Prev()

Move

一次移动多步:

1r = r.Move(2)

用于拼接两个环,或者从当前环里摘掉一段元素。

这是 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 是标准库里的循环链表实现
  • 它最适合带轮转语义的场景,而不是普通列表场景
  • NextMoveDo 是最常用入口
  • 真正使用时,要意识到“当前指针位置”本身就是状态的一部分
  • 它不是高频容器,但很适合用来训练自己对数据结构语义的判断