container/ring 是双向循环链表,非环形缓冲区;new(n) 创建含n个nil节点的环,零值为单节点自环,link会持续扩容,next/prev单步移动,move为绝对偏移。

container/ring 不是“循环数组”或“环形缓冲区”,它就是一个双向循环链表——节点之间靠 Next 和 Prev 指针连成环,没有隐含容量、不自动丢弃、不维护读写位置。想靠它实现轮询、历史浏览这类“天然循环”的场景,没问题;想当 FIFO 缓冲区用,大概率会踩坑。
ring.New(n) 创建的是 n 个物理节点,不是逻辑容量
调用 ring.New(5) 得到一个含 5 个节点的环,每个节点的 Value 初始为 nil,Next 和 Prev 已正确连好。这是最安全的起点。
- 零值
new(ring.Ring)也是合法环(自环),但只有 1 个节点,容易误判 - 反复调用
r.Link(newNode)会不断追加节点,r.Len()会越来越大——它不阻止扩容 - 别把
ring.New(10)理解成“容量为 10 的 buffer”,它只是初始有 10 个空槽,后续行为完全由你控制
Next() / Prev() 是单步移动,Move(n) 是绝对偏移
Next() 和 Prev() 各走 1 步,语义清晰;而 Move(n) 是从当前节点出发,顺时针走 n 步(n > 0)或逆时针走 |n| 步(n ),且内部会做 <code>n % r.Len() 取模——但前提是 r.Len() 还有效。
-
r.Move(-1)≠r.Prev():若r.Len() == 3,r.Move(-1)等价于r.Move(2)(因为 -1 % 3 == 2),实际走了两步顺时针 - 如果中途用
Unlink()改了结构,再调Move()可能指向已断开的节点,导致遍历跳过或 panic - 需要索引访问?别硬套
Move(i),先确保i在[0, r.Len())内,且结构未被并发修改
Do() 遍历只适合只读,Link()/Unlink() 后不能再用旧指针遍历
Do() 会从当前节点开始,顺时针调用函数直到回到起点,自动闭合。但它不感知中间修改——这是最大陷阱。
- 在
Do()回调里调r.Unlink(1),会导致后续遍历漏节点甚至 panic - 想边遍历边删,必须手动用
Next()+ 临时保存下一个节点:next := r.Next(); /* 处理 r */; r.Unlink(1); r = next -
Do()内部不做并发保护,多 goroutine 同时调用需外层加锁
真正需要 ring buffer 时,别用 container/ring
如果你要的是固定大小、自动覆盖、O(1) 入队出队、缓存友好的环形缓冲区,container/ring 是错工具。它堆分配每个节点,Len() 是 O(n),无 head/tail 抽象,写满逻辑全得自己缝。
- 推荐用
[]T+ 两个int游标(read/write),容量设为 2 的幂,用& (cap - 1)替代取模 - 写入前判断是否满:
(write+1)&(cap-1) == read(预留一位避免空满同态) - 所有边界检查、原子更新、并发安全都封装在
Push()/Pop()方法里,比拼接Link+Unlink+ 手动游标可靠得多
真正容易被忽略的是:ring 的“循环”是结构层面的,不是语义层面的。它不替你记住哪几个节点是“有效数据”,也不拦着你把它用成一串越长越大的链表。用对场景,它轻快省事;用错地方,bug 会藏在 Move() 的绕圈里、Len() 的遍历开销里、以及 Unlink() 后悬空的指针里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











