Go手写跳表必须遵守三层契约:层级生成用独立*rand.Rand且设上限4~6;插入删除必须保存每层prev指针;compare函数须为纯函数,Range遍历时需加锁防竞态。

Go 标准库没有 skiplist,手写时别先画图,先定三层契约:层级生成必须独立随机源、指针更新必须记录每层 prev、并发写必须锁住整个查找+更新路径——否则不是慢,是随机 panic 或数据丢失。
randomLevel() 为什么不能直接用 rand.Float64()
全局 rand 包在并发下会 panic,且概率分布坍缩(大量节点卡在 level 1),导致查找退化为 O(n)。更隐蔽的问题是:没设最大层数上限,某次插入意外生成 16 层指针,单个 node 占用内存翻倍,GC 压力飙升。
- 必须用独立
*rand.Rand:r := rand.New(rand.NewSource(time.Now().UnixNano())) - 层级上限建议设为 4~6:
for lvl ,<code>maxLevel=4足够覆盖 99% 场景 - 避免写成
for r.Float64() ——没上限,压测时 heap profile 里 <code>*skiplist.Node瞬间占满
Insert/Find/Delete 时为什么必须保存每层 prev 指针
跳表不是从头暴力遍历,而是“逐层定位再下降”。如果只记最终位置,删除时无法修复上层链表,两个 goroutine 可能同时改同一层 prev.next[lvl],链表断裂或节点悬空。
- 查找过程必须返回
[]*node(每层最后一个小于目标的节点),不只是目标节点本身 - 插入时:从最高层开始,每层找到
prev,然后原子更新prev.next[lvl] = newNode - 删除时:同样依赖该
prev数组,逐层置空对应指针,不能只删底层 - 错误示范:
node.next = node.next.next—— 这只动了 level 0,上层索引全失效
compare 函数里藏着最危险的竞态
第三方跳表库(如 github.com/huandu/skiplist)本身无锁,但所有 goroutine 会并发调你的 compare 函数。它必须是纯函数,任何副作用都会炸。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 禁止读写全局变量:
counter++→ 竞态 + 数据错乱 - 禁止类型断言不加检查:
a.(int)→ 传入nil或其他类型直接 panic - 推荐写法:用
switch a := a.(type)分支处理,或提前 encode 成[]byte后做字节比较 - 绝对不要在
compare里调time.Now()、rand.Intn()或访问map
Range 查询为什么边遍历边删会漏数据或 panic
跳表的 Range(start, end) 返回的是实时迭代器,不是快照。如果遍历时另一个 goroutine 删除了当前节点的后继,next 指针可能已变,下一次 node = node.next[lvl] 就 panic;更糟的是,删除操作可能跳过本该被遍历的节点。
- 生产环境慎用无锁遍历 + 删除组合,除非你明确实现了 snapshot 语义
- 简单方案:遍历前对整张表加读锁(或用
sync.RWMutex的RLock()),删节点时升级为写锁 - 高吞吐场景:改用带版本号的节点结构,遍历时按版本过滤,但实现成本陡增
- 记住:Redis ZSet 的
ZRANGEBYSCORE是原子快照,你手写的大概率不是
真正难的不是写完 Insert,是让 Delete 在 100 个 goroutine 并发时既不丢节点也不卡死——这时候你会发现,锁的粒度、prev 数组生命周期、以及 compare 里那行看似无害的 fmt.Println,全在等你踩。

















