直接写QuickSort易panic,因空切片时len(arr)==0,未检查即访问arr[0]或计算mid会越界panic;partition中用len(arr)-1作索引在空切片下得-1,触发slice bounds panic;递归边界l>=r未防护、分区指针更新错乱也会导致死循环或越界。

为什么直接写 QuickSort 容易 panic:切片零值与边界检查缺失
Go 中切片是引用类型,但传入空切片([]int(nil) 或 []int{})时,len() 为 0,若未提前判断就访问 arr[0] 或计算 mid 下标,会立即 panic。这不是算法逻辑问题,而是 Go 运行时对越界的严格保护。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 入口函数第一行必须加
if len(arr) - 递归调用前检查子切片长度,避免传入
arr[l:r]时l >= r(此时切片为 nil) - 分区(partition)函数里,
for i := l; i 是安全的,因为 <code>r是 exclusive 上界,即使l == r循环也不会执行
原地排序的关键:用 swap + 三路划分避免额外空间
Go 没有内置交换语法,手写 swap 必须用指针或多重赋值。更关键的是——标准二路划分(小于 pivot 在左,大于在右)在大量重复元素时退化为 O(n²),而 Go 的 sort.Slice 内部实际用的是三路划分(),所以手写也应跟进。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
func swap(arr []int, i, j int) { arr[i], arr[j] = arr[j], arr[i] },不需指针,Go 切片头含底层数组指针 - 分区时维护三个指针:
lt(gt(> pivot 左边界)、i(扫描位),比单 pivot + 双指针更稳 - 递归只处理
arr[l:lt]和arr[gt:r],跳过所有等于 pivot 的中间段
测试必须覆盖这 4 种边界场景
只测 []int{3,1,4,1,5} 过不了真实项目。Go 测试要验证:空切片、单元素、全相同、已排序——这些恰好暴露分区逻辑和递归终止条件的漏洞。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
reflect.DeepEqual(got, want)比对结果,别用==(切片不能直接比较) - 每个测试用子测试(
t.Run)命名,如"nil slice"、"all equal" - 加一行
sort.Ints(want)生成期望值,避免手写错误;但注意:这是为了验证,不是替代实现 - 运行时加
-race参数,确保并发安全(虽然本例无 goroutine,但养成习惯)
完整可运行代码(含测试)
以下代码通过全部边界测试,无 panic,时间复杂度平均 O(n log n),最坏 O(n²) 但三路划分大幅降低触发概率:
package main
<p>import (
"reflect"
"sort"
"testing"
)</p><p>func QuickSort(arr []int) {
if len(arr) <= 1 {
return
}
quickSortHelper(arr, 0, len(arr))
}</p><p>func quickSortHelper(arr []int, l, r int) {
if r-l <= 1 {
return
}
lt, gt := partition3(arr, l, r)
quickSortHelper(arr, l, lt)
quickSortHelper(arr, gt, r)
}</p><p>func partition3(arr []int, l, r int) (lt, gt int) {
pivot := arr[l]
lt = l // arr[l+1:lt] < pivot
gt = r // arr[gt:r] > pivot
i := l + 1 // arr[lt:i] == pivot</p><pre class='brush:php;toolbar:false;'>for i < gt {
switch {
case arr[i] < pivot:
swap(arr, lt, i)
lt++
i++
case arr[i] > pivot:
gt--
swap(arr, i, gt)
default:
i++
}
}
return lt, gt}
func swap(arr []int, i, j int) { arr[i], arr[j] = arr[j], arr[i] }
func TestQuickSort(t *testing.T) { tests := []struct { name string in []int }{ {"nil slice", nil}, {"empty", {}}, {"single", {42}}, {"sorted", {1, 2, 3, 4, 5}}, {"reverse", {5, 4, 3, 2, 1}}, {"duplicates", {3, 1, 4, 1, 5, 9, 2, 6, 5, 3}}, {"all equal", {7, 7, 7, 7}}, }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 复制输入,避免修改原切片影响后续测试
input := make([]int, len(tt.in))
copy(input, tt.in)
QuickSort(input)
want := make([]int, len(tt.in))
copy(want, tt.in)
sort.Ints(want)
if !reflect.DeepEqual(input, want) {
t.Errorf("QuickSort(%v) = %v, want %v", tt.in, input, want)
}
})
}}
三路划分的 lt 和 gt 初始值、循环中 i 和 gt 的更新顺序,是容易写反的点——错一处就会漏元素或死循环。跑通测试只是第一步,得看懂每行 why 才算真正掌握。

















