go语言结构体排序

有了go语言中的排序接口, 可以很容易的对结构体的任意一个字段进行排序, 具体以哪个字段排序, 定义在该结构体Slice的Less方法中. 一个结构体的Slice仅可以实现一次sort接口, 如果一个结构体中有多个字段需要单独进行排序, 最粗鲁的办法只能是定义多个Slice类型, 为每个类型都编写不同的Less方法

约定关键字:

多字段单独排序: 一个结构体中, 需要有多个字段作为依据, 单独进行排序, 单个字段排序时不受其他字段排序策略的影响
多字段多因素排序: 一个结构体中, 对第一个字段排序时, 如果该字段的数值相等, 按其他字段再次进行排序操作(例如: 按年龄进行排序, 年龄相同的按体重排序)

结构体单字段排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"fmt"
"sort"
)

// 元素类型
type student struct {
name string
age uint
weight float32
}

// 数据集类型
type students []student

func (s students) Len() int {
return len(s)
}

func (s students) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

func (s students) Less(i, j int) bool {
return s[i].age < s[j].age
}

// 数据集
var ss = students{
{"d", 7, 28},
{"c", 3, 40},
{"b", 8, 35},
{"a", 5, 30},
}

func main() {
sort.Sort(ss)
fmt.Println(ss)
}

运行结果:

1
[{c 3 40} {a 5 30} {d 7 28} {b 8 35}]

结构体多字段单独排序

多字段需要排序的, 最粗鲁的办法是为每一个需要排序的字段单独声明一个数据集类型, 然后在每个数据集类型中实现其对应的Less方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import (
"fmt"
"sort"
)

type student struct {
name string
age uint
weight float32
}

type studentsSortByName []student

func (s studentsSortByName) Len() int {
return len(s)
}

func (s studentsSortByName) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

func (s studentsSortByName) Less(i, j int) bool {
return s[i].name < s[j].name
}

type studentsSortByAge []student

func (s studentsSortByAge) Len() int {
return len(s)
}

func (s studentsSortByAge) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

func (s studentsSortByAge) Less(i, j int) bool {
return s[i].age < s[j].age
}

type studentsSortByWeight []student

func (s studentsSortByWeight) Len() int {
return len(s)
}

func (s studentsSortByWeight) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

func (s studentsSortByWeight) Less(i, j int) bool {
return s[i].weight < s[j].weight
}

// 数据集
var ss = []student{
{"d", 7, 28},
{"c", 3, 40},
{"b", 8, 35},
{"a", 5, 30},
}

func main() {
sort.Sort(studentsSortByName(ss))
fmt.Println(ss)

sort.Sort(studentsSortByAge(ss))
fmt.Println(ss)

sort.Sort(studentsSortByWeight(ss))
fmt.Println(ss)
}

运行结果:

1
2
3
[{a 5 30} {b 8 35} {c 3 40} {d 7 28}]  // 按姓名排序
[{c 3 40} {a 5 30} {d 7 28} {b 8 35}] // 按年龄排序
[{d 7 28} {a 5 30} {b 8 35} {c 3 40}] // 按体重排序

这个排序的办法的劣势是需要写很多重复代码, 通过这个例子也可以看出, 按不动字段排序实际只是Less方法的实现不同

结构体多字段单独排序(推荐)

这里介绍一个更优雅的方式, 解决上面多字段排序需要编写n * 3方法的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package main

import (
"fmt"
"sort"
)

// 定义元素类型
type student struct {
name string
age uint
weight float32
}

// 定义数据集类型
// 在单字段排序中, 数据集类型仅仅是元素类型的Slice, 但是在此方法中, 元素类型的Slice作为数据集类型的一个字段出现
// 由于需要对多个字段排序, 且每个字段实现的`Less`方法不同, 这里将排序方法(函数)作为数据集类型的一个字段
type studentSorter struct {
students []student // 具体的数据集, 待排序的数据都会存放在这里
by func(i, j student) bool // 排序函数, 排序的依据, 具体排序的策略, 会被放置在这里
// 可以明显的看出, by 函数的入参与返回 与 `Less` 函数的入参与返回是一致的
}

// 在数据集类型之上实现排序接口的Len方法
func (s studentSorter) Len() int {
return len(s.students)
}

// 在数据集类型之上实现交换元素的Swap方法
func (s studentSorter) Swap(i, j int) {
s.students[i], s.students[j] = s.students[j], s.students[i]
}

// 在数据集类型之上实现比较元素大小的Less方法
// 由于多个字段需要排序, 这里的返回值, 返回的是一个排序函数
// 上面的数据集结构体中, 已经明确定义了一个 by 字段, 该字段用来保存具体的排序函数
// 所以这里的返回值并没有直接指定数据的大小的比较方法, 而是调用了保存在数据集by字段中的函数去处理
// 可以简单的理解为, 将比较数据大小的操作委托给了 数据集类型.by 的函数, 具体的排序策略也就交给了 by 对应的具体函数
func (s studentSorter) Less(i, j int) bool {
return s.by(s.students[i], s.students[j])
}

// 声明并赋值测试数据集
var students = []student{
{"d", 7, 28},
{"c", 3, 40},
{"b", 8, 35},
{"a", 5, 30},
}

// 使用工厂方法定义排序数据集, 并进行排序
// 上面的 students 仅仅是实际的待排序数据, 仅仅是数据集对象中的 studentSorter.students 字段的值
// 工厂方法就是用来根据实际的排序策略, 为数据集对象中的by赋值, 生成一个完整的数据集, 从而完成排序操作
type By func(i, j student) bool
func (b By) Sort (students []student) {
// 生成数据集
ss := studentSorter{
students: students,
by: b,
}
// 对数据集进行排序
sort.Sort(ss)
}

// 预定义排序策略函数, 这些函数都实现了sort接口中的less方法

// 预先定义的排序函数: 按照姓名排序
var name = func(i, j student) bool {
return i.name < j.name
}

// 预先定义的排序函数: 按照年龄排序
var age = func(i, j student) bool {
return i.age < j.age
}

// 预先定义的排序函数: 按照体重排序
var weight = func(i, j student) bool {
return i.weight < j.weight
}

func main() {
By(name).Sort(students)
fmt.Println("By name:", students)

By(age).Sort(students)
fmt.Println("By age:", students)

By(weight).Sort(students)
fmt.Println("By weight:", students)
}

运行结果:

1
2
3
By name: [{a 5 30} {b 8 35} {c 3 40} {d 7 28}]
By age: [{c 3 40} {a 5 30} {d 7 28} {b 8 35}]
By weight: [{d 7 28} {a 5 30} {b 8 35} {c 3 40}]

这个多字段单独排序的方法, 省去了需要重复编写Len()``Swap()方法的麻烦, 也是sort包官方推荐的多字段排序案例

结构体多字段单独排序2(推荐)

官方文档中还介绍了另一种排序方式, 通过Go语言独特的组合特性来实现sort接口的三个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"fmt"
"sort"
)

type student struct {
name string
age int
weight int
}

type students []student

func (s students) Len() int {
return len(s)
}
func (s students) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

type ByName struct {
students
}

func (s ByName) Less(i, j int) bool {
return s.students[i].name < s.students[j].name
}

type ByWeight struct {
students
}

func (s ByWeight) Less(i, j int) bool {
return s.students[i].weight < s.students[j].weight
}

var ss = []student{
{"d", 7, 28},
{"c", 3, 40},
{"b", 8, 35},
{"a", 5, 30},
}

func main() {
sort.Sort(ByWeight{ss})
fmt.Println("Organs by weight: ", ss)

sort.Sort(ByName{ss})
fmt.Println("Organs by name: ", ss)
}