sort.Slice 是默认首选,因其底层混合策略防退化、自动处理边界、编译器保障泛型安全;手写快排易因空切片、索引越界、非原地操作等导致 panic 或逻辑错误。

别手写快排,除非你清楚自己在绕过什么——sort.Slice 覆盖 99% 场景,安全、快、防退化;手写版本极易因切片边界、空切片、pivot 归位错误导致 panic 或逻辑错乱。
为什么 sort.Slice 是默认首选
它不是“够用就行”的备选,而是 Go 官方为绝大多数排序需求设计的正解:底层自动选择混合策略(快排 + 堆排 + 插入排序),避免最坏 O(n²);对 nil 切片或空切片直接 panic,强制你提前处理边界;泛型约束由编译器检查,不靠人肉校验。
常见错误现象:panic: runtime error: index out of range [0] with length 0 —— 这往往是你自己写的 partition 函数开头就用了 arr[len(arr)-1],而没判空。
- 必须提前检查:
if len(data) == 0 || data == nil { return } - 比较函数里禁止出现
less(i, i) == true,比如写成return a[i].X 就违规 - 字段含
*string或map时,less中必须显式判断 nil,否则一碰就崩
partition 函数里为什么不能用 len(arr) - 1 当索引
这不是风格问题,是运行时确定会 panic 的硬伤。空切片时 len(arr) 是 0,len(arr) - 1 就是 -1,Go 直接报 index out of range。
立即学习“go语言免费学习笔记(深入)”;
更隐蔽的坑在于:递归传入的是子切片(如 arr[lo:hi]),它的长度远小于原始切片,但右边界是 hi - 1,不是 len(arr) - 1。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确做法:函数签名带
low, high int参数,pivot 取arr[high],分区循环用for j := low; j - 错误写法:
pivot := arr[len(arr)-1],没检查len(arr) == 0 - 另一个坑:
for j := 0; j 在 <code>len(arr) == 1时循环不执行,pivot 没参与分区,最终位置错乱
手写快排时为什么 append 拼左右子数组永远排不对原切片
这不是算法错,是 Go 切片语义误解。append 在底层数组容量不足时会分配新数组,返回的新切片指向新内存,原变量完全没变。
后果很实在:你调用完自定义快排函数,再用 sort.Search 查数据,查不到——因为原切片根本没被排序。
- 典型错误:
left := append([]int{}, ...); right := append([]int{}, ...); return append(append(left, pivot), right...) - 空间爆炸:每次递归都新建底层数组,最坏空间复杂度从 O(1) 暴涨到 O(n²)
- 正确路径:只用
arr[i], arr[j] = arr[j], arr[i]原地交换,所有递归操作同一底层数组
非要手写,只保留这个最小可靠骨架
仅限算法题、教学、或嵌入式禁用标准库场景。以下版本已验证:空切片安全、pivot 归位正确、不新建底层数组、终止条件鲁棒。
关键点不在“炫技”,而在守住底线:原地、防 panic、不越界。
- 递归终止条件必须是
if low >= high,不是if len(arr) (后者在子切片中失效) - pivot 必须选
arr[high]或随机索引后 swap 到末尾,确保分区后能准确返回其最终位置 - 小数组(如
high - low )建议切插入排序,避免深度递归和最坏情况放大
真正容易被忽略的,是「你写的每一行交换,都在改写原始底层数组」这个事实——一旦用 append 或子切片构造新 slice 并赋值给局部变量,你就已经脱离了原地排序的轨道。

















