go 结构体根据某个字段进行排序

前言

在任何编程语言中,关乎到数据的排序都会有对应的策略,我们来看下 Golang 是怎样对数据进行排序,以及我们如何优化处理使用

go 排序

go 可以针对任何对象排序,虽然很多情况下是一个 slice 切片,或者是包含 slice 的一个对象。

go 排序(接口)有三个要素:

  1. 待排序元素个数 n ;
  2. 第 i 和第 j 个元素的比较函数 cmp ;
  3. 第 i 和 第 j 个元素的交换 swap ;

基本类型 int 、 float64 和 string 的排序

升序排序

对于 intfloat64string 数组或切片的排序,Golang 分别提供了 sort.Ints()sort.Float64s()sort.Strings()函数,默认都是从小到大排序。

复制代码
package main

import (
	"fmt"
	"sort"
)

func main() {
	intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0}
	float8List := []float64{4.2, 5.9, 12.3, 10.0, 50.4, 99.9, 31.4, 27.81828, 3.14}
	stringList := []string{"a", "c", "b", "d", "f", "i", "z", "x", "w", "y"}

	sort.Ints(intList)
	sort.Float64s(float8List)
	sort.Strings(stringList)

	fmt.Printf("%v\n%v\n%v\n", intList, float8List, stringList)
}

输出

复制代码
[0 1 2 3 4 5 6 7 8 9]
[3.14 4.2 5.9 10 12.3 27.81828 31.4 50.4 99.9]
[a b c d f i w x y z]

降序排序

对于 intfloat64string 都有默认的升序排序函数,现在问题是怎样才能降序呢?

go 中的 sort 包有一个 sort.Interface 接口,该接口有三个方法 Len()Less()Swap(i, j)。通用排序函数 sort.Sort 可以排序任何实现了 sort.Interface 接口的对象(变量)。对于 []int[]float64[]string 除了使用特殊指定的函数外,还可以使用改装过的类型 IntScliceFloat64SliceStringSlice,然后直接调用它们对应的 Sort() 方法,因为这三种类型也实现了 sortInterface接口,所以可以通过 sort.Reverse 来转换这三种类型的 Interface.Less 方法来实现逆向排序。

复制代码
package main

import (
	"fmt"
	"sort"
)

func main() {
	intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0}
	float8List := []float64{4.2, 5.9, 12.3, 10.0, 50.4, 99.9, 31.4, 27.81828, 3.14}
	stringList := []string{"a", "c", "b", "d", "f", "i", "z", "x", "w", "y"}

	sort.Sort(sort.Reverse(sort.IntSlice(intList)))
	sort.Sort(sort.Reverse(sort.Float64Slice(float8List)))
	sort.Sort(sort.Reverse(sort.StringSlice(stringList)))

	fmt.Printf("%v\n%v\n%v\n", intList, float8List, stringList)
}

输出

复制代码
[9 8 7 6 5 4 3 2 1 0]
[99.9 50.4 31.4 27.81828 12.3 10 5.9 4.2 3.14]
[z y x w i f d c b a]

下面使用一个自定义的 Reverse 结构体,而不是 sort.Reverse 函数,来实现逆向排序。

复制代码
package main

import (
	"fmt"
	"sort"
)

// 自定义的 Reverse 类型
type Reverse struct {
	sort.Interface // 这样, Reverse 可以接纳任何实现了 sort.Interface (包括 Len, Less, Swap 三个方法) 的对象
}

// Reverse 只是将其中的 Inferface.Less 的顺序对调了一下
func (r Reverse) Less(i, j int) bool {
	return r.Interface.Less(j, i)
}

func main() {
	doubles := []float64{3.5, 4.2, 8.9, 100.98, 20.14, 79.32}

	fmt.Printf("doubles is asc ? %v\n", sort.Float64sAreSorted(doubles))

	//sort.Float64s(doubles) // float64 正序排序 方法1

	//sort.Sort(sort.Float64Slice(doubles))    // float64 正序排序 方法2

	(sort.Float64Slice(doubles)).Sort() // float64 排序方法 方法3
	fmt.Println("after sort by Sort:\t", doubles)

	sort.Sort(Reverse{sort.Float64Slice(doubles)}) // float64 逆序排序
	fmt.Println("after sort by Reversed Sort:\t", doubles)
}

输出

复制代码
doubles is asc ? false
after sort by Sort:      [3.5 4.2 8.9 20.14 79.32 100.98]
after sort by Reversed Sort:     [100.98 79.32 20.14 8.9 4.2 3.5]

以上只是说明了基础类型怎样进行排序。

结构体类型排序

在实际应用中,结构体排序我们会用得更多。结构体类型排序是通过使用 sort.Sort(slice) 实现的,只要 slice 实现了 sort.Interface 的三个方法就可以完成排序。但针对不同排序的要求,排序的方式有好几种。

第一种

第一种模拟排序 []int 构造对应的 IntSlice 类型,然后对 IntSlice 类型实现 Interface 的三个方法。

复制代码
package main

import (
	"fmt"
	"sort"
)

type Person struct {
	Name string // 姓名
	Age  int    // 年纪
}

// 按照 Person.Age 从大到小排序
type PersonSlice []Person

func (a PersonSlice) Len() int { // 重写 Len() 方法
	return len(a)
}
func (a PersonSlice) Swap(i, j int) { // 重写 Swap() 方法
	a[i], a[j] = a[j], a[i]
}
func (a PersonSlice) Less(i, j int) bool { // 重写 Less() 方法, 从大到小排序
	return a[j].Age < a[i].Age
}

func main() {
	people := []Person{
		{"zhang san", 12},
		{"li si", 30},
		{"wang wu", 52},
		{"zhao liu", 26},
	}

	fmt.Println(people)

	sort.Sort(PersonSlice(people)) // 按照 Age 的逆序排序
	fmt.Println(people)

	sort.Sort(sort.Reverse(PersonSlice(people))) // 按照 Age 的升序排序
	fmt.Println(people)
}

输出

复制代码
[{zhang san 12} {li si 30} {wang wu 52} {zhao liu 26}]
[{wang wu 52} {li si 30} {zhao liu 26} {zhang san 12}]
[{zhang san 12} {zhao liu 26} {li si 30} {wang wu 52}]

第二种

第一种排序的缺点是:根据 Age 排序需要重新定义 PersonSlice 方法,绑定 LenLessSwap 方法,如果需要根据 Name 排序,又需要重新写三个函数。如果结构体有 4 个字段,就会有 4 种排序,那么就要写 3 * 4 = 12 个方法,,即使有一些完全是多余的(O__O"…),仔细想一下,根据不同的标准 Age 或是 Name,真正的不同体现在 Less 方法上,所以我们可以将 Less 抽象出来,每种排序的 Less 让其变成动态的。

复制代码
package main

import (
	"fmt"
	"sort"
)

type Person struct {
	Name string // 姓名
	Age  int    // 年纪
}

type PersonWrapper struct {
	people []Person
	by     func(p, q *Person) bool
}

func (pw PersonWrapper) Len() int { // 重写 Len() 方法
	return len(pw.people)
}
func (pw PersonWrapper) Swap(i, j int) { // 重写 Swap() 方法
	pw.people[i], pw.people[j] = pw.people[j], pw.people[i]
}
func (pw PersonWrapper) Less(i, j int) bool { // 重写 Less() 方法
	return pw.by(&pw.people[i], &pw.people[j])
}

func main() {
	people := []Person{
		{"zhang san", 12},
		{"li si", 30},
		{"wang wu", 52},
		{"zhao liu", 26},
	}

	fmt.Println(people)

	sort.Sort(PersonWrapper{people, func(p, q *Person) bool {
		return q.Age < p.Age // Age 递减排序
	}})

	fmt.Println(people)
	sort.Sort(PersonWrapper{people, func(p, q *Person) bool {
		return p.Name < q.Name // Name 递增排序
	}})

	fmt.Println(people)
}

输出

复制代码
[{zhang san 12} {li si 30} {wang wu 52} {zhao liu 26}]
[{wang wu 52} {li si 30} {zhao liu 26} {zhang san 12}]
[{li si 30} {wang wu 52} {zhang san 12} {zhao liu 26}]

[]Person 和比较的准则 cmp 封装到一起,形成了 PersonWrapper 结构体,然后在其上绑定 LenLessSwap 方法。实际上 sort.Sort(pw) 排序的是 pw 中的 people,也就是 go 排序的对象可以是数组或者切片。

第三种

第二种排序方法已经很不错啦,唯一一个缺点是,在 main 中使用的时候暴露了 sort.Sort 的使用,还有就是 PersonWrapper 的构造。为了让 main 中使用起来更为方便,我们可以再简单的封装一下,构造一个 SortPerson 方法。

复制代码
package main

import (
	"fmt"
	"sort"
)

type Person struct {
	Name string // 姓名
	Age  int    // 年纪
}

type PersonWrapper struct {
	people []Person
	by     func(p, q *Person) bool
}

type SortBy func(p, q *Person) bool

func (pw PersonWrapper) Len() int { // 重写 Len() 方法
	return len(pw.people)
}
func (pw PersonWrapper) Swap(i, j int) { // 重写 Swap() 方法
	pw.people[i], pw.people[j] = pw.people[j], pw.people[i]
}
func (pw PersonWrapper) Less(i, j int) bool { // 重写 Less() 方法
	return pw.by(&pw.people[i], &pw.people[j])
}

func SortPerson(people []Person, by SortBy) { // SortPerson 方法
	sort.Sort(PersonWrapper{people, by})
}

func main() {
	people := []Person{
		{"zhang san", 12},
		{"li si", 30},
		{"wang wu", 52},
		{"zhao liu", 26},
	}

	fmt.Println(people)

	sort.Sort(PersonWrapper{people, func(p, q *Person) bool {
		return q.Age < p.Age // Age 递减排序
	}})

	fmt.Println(people)

	SortPerson(people, func(p, q *Person) bool {
		return p.Name < q.Name // Name 递增排序
	})

	fmt.Println(people)
}

输出

复制代码
[{zhang san 12} {li si 30} {wang wu 52} {zhao liu 26}]
[{wang wu 52} {li si 30} {zhao liu 26} {zhang san 12}]
[{li si 30} {wang wu 52} {zhang san 12} {zhao liu 26}]

在方法二的基础上构建了 SortPerson 函数,使用的时候只需要传入一个 []Person 和一个 cmp 函数。

第四种

其实是另外一种实现思路,可以说是方法一、方法二的变体。

复制代码
package main

import (
	"fmt"
	"sort"
)

type Person struct {
	Name   string
	Weight int
}

type PersonSlice []Person

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

type ByName struct{ PersonSlice } // 将 PersonSlice 包装起来到 ByName 中

func (s ByName) Less(i, j int) bool { return s.PersonSlice[i].Name < s.PersonSlice[j].Name } // 将 Less 绑定到 ByName 上

type ByWeight struct{ PersonSlice }   // 将 PersonSlice 包装起来到 ByWeight 中
func (s ByWeight) Less(i, j int) bool { return s.PersonSlice[i].Weight < s.PersonSlice[j].Weight } // 将 Less 绑定到 ByWeight 上

func main() {
	s := []Person{
		{"apple", 12},
		{"pear", 20},
		{"banana", 50},
		{"orange", 87},
		{"hello", 34},
		{"world", 43},
	}

	sort.Sort(ByWeight{s})
	fmt.Println("People by weight:")
	printPeople(s)

	sort.Sort(ByName{s})
	fmt.Println("\nPeople by name:")
	printPeople(s)
}

func printPeople(s []Person) {
	for _, o := range s {
		fmt.Printf("%-8s (%v)\n", o.Name, o.Weight)
	}
}

输出

复制代码
People by weight:
apple    (12)
pear     (20)
hello    (34)
world    (43)
banana   (50)
orange   (87)

People by name:
apple    (12)
banana   (50)
hello    (34)
orange   (87)
pear     (20)
world    (43)

对结构体的排序,暂时就到这里。第一种排序适用于只根据一个字段排序比较合适,另外三种排序方法可根据多个字段排序。第四种方法每次都要多构造一个 ByXXX,很不方便,不如方法二和方法三,方法三只是简单封装了下方法二,对使用者来说,会更加方便一些,而且也会更少的出错。

posted @   牛奔  阅读(3237)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示