一、一个让我写出“笨代码”的下午
去年写一个 TODO 应用时,我遇到一个需求:用户拖拽任务列表里的某个任务,改变它的优先级,列表要重新排序。
后端存储就是一个切片 []Task,按优先级排序。
用户把优先级从“低”改成“高”后,任务应该从列表尾部移动到头部附近。
我当时写的代码是这样的:
// 假设 tasks 是按优先级排序的切片
oldIdx := findTaskIndex(tasks, taskID)
task := tasks[oldIdx]
task.Priority = newPriority
// 删除旧位置
tasks = slices.Delete(tasks, oldIdx, oldIdx+1)
// 找到新位置并插入
newIdx := findNewPosition(tasks, task)
tasks = slices.Insert(tasks, newIdx, task)
跑是能跑。但我看着这段代码,总觉得哪里不对劲。
两次内存移动,做了两遍复制,还多了一次切片长度变更。
我问自己:Go 标准库有 slices.Delete,有 slices.Insert,为什么没有一个“把元素从A位置移动到B位置”的操作?
直到我看到了这个提案:slices.Move。
2026年5月31日,开发者 abemedia 在 Go 官方 issue 里提交了一个提案:给 slices 包增加一个 Move 函数。
当前做法(Delete + Insert):
// 移动元素:从索引 from 移动到 to
v := s[from]
s = slices.Delete(s, from, from+1) // 第一次内存移动
s = slices.Insert(s, to, v) // 第二次内存移动
提案做法(Move):
func Move[S ~[]E, E any](s S, from, to int) {
if from == to {
return
}
v := s[from]
if from < to {
copy(s[from:to], s[from+1:to+1]) // 向左移动
} else {
copy(s[to+1:from+1], s[to:from]) // 向右移动
}
s[to] = v
}
核心优化:一次 copy 完成,而不是两次。
使用示例:
// 排序的切片里,某个元素的键变了,需要重新调整位置
oldIdx, _ := slices.BinarySearchFunc(records, r, byPK)
r.PK = newValue
newIdx, _ := slices.BinarySearchFunc(records, r, byPK)
if newIdx > oldIdx {
newIdx--
}
slices.Move(records, oldIdx, newIdx) // 一行搞定,一次内存移动
这个提案的讨论区很有意思,有几个观点特别能代表 Go 社区的文化。
观点1:Go 核心成员的质疑
“移动单个元素需要 O(n) 代价,这通常意味着你选错了数据结构。”
这是典型的“核心团队视角”——从算法复杂度出发,认为切片不适合频繁移动元素。
观点2:提案作者的反驳
“
slices.Insert和slices.Delete已经是 O(n) 了。如果 O(n) 就意味着错,那这两个函数也不该存在。Move只是填补空缺。”
这个反驳很犀利。它指出:Go 标准库已经有 O(n) 的切片操作了,为什么多一个 Move 就不行?
观点3:实用性辩护
“UI 列表重排、轻微变更的排序切片、小规模 LRU 缓存……这些场景里,切片是自然选择。CPU 缓存局部性带来的性能优势,远大于算法复杂度的理论劣势。”
这是“工程视角”——理论归理论,实际场景中切片就是好用。
观点4:rotate vs move
有人提出用 slices.Rotate 来替代 Move,还给了实现代码。但作者认为 Move 更直接、更符合直觉。
我觉得这个讨论很 Go:核心成员在乎理论正确性,社区开发者在乎实际痛点和代码可读性。 双方都没错,只是站位不同。
回到那个 TODO 应用。用户拖拽排序时,列表通常只有几十到几百个任务。Delete+Insert 确实能跑,但我心里一直有个“技术债”的感觉:每次移动都要写三行代码,还要小心处理切片覆盖。
更让我困扰的是,Delete 和 Insert 中间如果有其他操作(比如日志、验证),代码就变得更难读。
如果当时有 slices.Move:
slices.Move(tasks, oldIdx, newIdx)
一行,意图明确,性能翻倍。
这不是“能不能实现”的问题,而是“应该怎么表达”的问题。 代码是给人看的,Move 比 Delete+Insert 更清楚地表达了“我想移动一个元素”。
Go 1.21 引入 slices 包时,设计者已经很克制了:只加了最基础、最通用的函数。
Insert 和 Delete 入选了,因为它们覆盖了切片修改的常见模式。
Move 为什么没入选?可能就是因为 Alan 说的那个顾虑——怕开发者频繁在切片里移动元素,导致性能问题。
但这个提案告诉我们一个道理:工程实践的需求,有时会超过理论完美主义。
Move 不是让你处理百万级数据。它是让你在合适的场景下(小切片、UI 交互、排序调整),写出更清晰、更高效的代码。
我认为 slices.Move 应该被加入标准库。
理由有三:
- 填补 API 空缺:有
Delete、有Insert,自然应该有Move。 - 性能优势明显:一次内存移动 vs 两次,对重视性能的场景很实在。
- 代码可读性提升:
slices.Move(s, from, to)意图一目了然。
前面的担心不无道理,但 Go 的设计哲学里,“实用主义”一直是重要原则。切片在 Go 生态中的地位不可动摇,提供一个更高效的移动元素的方式,是对这种实用主义的延续。
这个提案的讨论区里,有一句评论我印象很深:
“
Move不是救世主,它是让你少写两行笨拙代码的语法糖。有时候,这就是标准库的职责。”
我觉得说得真好。
Go 以简洁著称,但简洁不等于简陋。当社区反复遇到同一个模式,标准库就有义务提供更好的原语。
slices.Move 不是什么革命性功能。但它能让你少写一次 Delete、少写一次 Insert,多一份代码清晰度,多一倍性能。
这,就值得加。
等这个提案通过后,我会回去重构那个 TODO 应用。把那三行笨代码,换成一行 slices.Move。然后告诉自己:这才是 Go 该有的样子。