什么是最简单的方法来按(任意)字段名对结构体数组进行排序?

252

我刚遇到一个问题,我有一个结构体数组,例如:

package main

import "log"

type Planet struct {
    Name       string  `json:"name"`
    Aphelion   float64 `json:"aphelion"`   // in million km
    Perihelion float64 `json:"perihelion"` // in million km
    Axis       int64   `json:"Axis"`       // in km
    Radius     float64 `json:"radius"`
}

func main() {
    var mars = new(Planet)
    mars.Name = "Mars"
    mars.Aphelion = 249.2
    mars.Perihelion = 206.7
    mars.Axis = 227939100
    mars.Radius = 3389.5

    var earth = new(Planet)
    earth.Name = "Earth"
    earth.Aphelion = 151.930
    earth.Perihelion = 147.095
    earth.Axis = 149598261
    earth.Radius = 6371.0

    var venus = new(Planet)
    venus.Name = "Venus"
    venus.Aphelion = 108.939
    venus.Perihelion = 107.477
    venus.Axis = 108208000
    venus.Radius = 6051.8

    planets := [...]Planet{*mars, *venus, *earth}
    log.Println(planets)
}

假设你想按Axis排序,你该怎么做呢?

(注:我看过http://golang.org/pkg/sort/,它似乎可以工作,但仅仅是为了通过一个非常简单的键排序而添加了约20行代码。 我有Python背景,它的操作就像这样简单:sorted(planets, key=lambda n: n.Axis) - 在Go中是否有类似简单的方法?)


这里有另一个第三方包 https://github.com/patrickmn/sortutil。它可以进行普通排序和嵌套排序。我从文档中引用关于性能的内容:“虽然sortutil很方便,但它不会在性能上击败专用排序。当需要高性能时,应该考虑实现一个类型ByName的接口,该接口嵌入例如[]MyStruct并执行sort.Sort(ByName{MySlice})。” - Tutompita
7个回答

596

从Go1.8开始,您现在可以使用sort.Slice来对一个切片进行排序:

sort.Slice(planets, func(i, j int) bool {
  return planets[i].Axis < planets[j].Axis
})
通常情况下,使用切片而不是数组没有什么理由,但在您的示例中,您确实使用了一个数组,因此必须将其与一个切片叠加(添加[:]),以使其可以与sort.Slice一起使用。
sort.Slice(planets[:], func(i, j int) bool {
  return planets[i].Axis < planets[j].Axis
})

排序会改变数组,所以如果你真的想要,你可以在排序后继续使用该数组而不是切片。


sort.Slice 有点令人惊讶。less 函数只接受索引,因此它必须(在此答案中)使用一个单独捕获的 planets 数组。似乎没有任何强制要求排序的切片和 less 函数操作相同的数据。为了使其工作,您必须三次键入 planets(DRY)。 - Brent Bradburn
planets[:] 是至关重要的。但我不明白为什么。不过它确实有效。 - STEEL
通常情况下,你应该使用切片而不是数组。这样你就不需要使用 [:] - AndreKR
非常好用!!谢谢。令人困惑的是i和j设置了什么,以及输出是什么。sqlrun -e "SELECT FOO,BAR FROM FOOBAR" sqlrun将使用以下默认值选择服务器、端口、用户名、密码和输出类型。 --add-server 添加/配置服务器。 --add-user 添加/配置用户。 -A, --autocomplete 打开MySQL客户端自动完成。 这与mysql客户端的工作方式相反,其中-A将其关闭, - Rich
1
@mrpandey 啊,我明白你的意思了。由于比较器无法获取到传递给它的切片,你能做的最好的办法要么是创建一个返回实际比较器函数的函数(你仍然需要重复切片的名称一次),要么就将整个排序过程放入一个函数中。记住,由于内联,函数调用是廉价的。 - undefined
显示剩余3条评论

99

更新:此答案涉及旧版本的go。对于Go 1.8及更高版本,请参见上面的AndreKR的答案


如果您想要比标准库sort包更简洁的东西,可以使用第三方github.com/bradfitz/slice包。它使用一些技巧生成了需要对切片进行排序的LenSwap方法,因此您只需要提供一个Less方法即可。

使用此包,您可以执行以下排序:

slice.Sort(planets[:], func(i, j int) bool {
    return planets[i].Axis < planets[j].Axis
})

planets[:] 部分是必要的,以产生覆盖数组的切片。如果你将 planets 改为切片而不是数组,你可以跳过那部分。


54
我必须使用第三方包来对数组排序(除非我想写大量冗长的代码)?这种语言有什么问题吗?我的意思是...它只是排序!没有黑魔法。 - Jendas
11
Go的设计理念是简单而不是容易。Ruby则更容易上手,即使你不知道某些功能的具体实现方式,你也可以直接尝试使用,而且通常都能按照期望正常运行。但如果你想要理解语言规范并构建一个解释器,或者在学习Ruby的同时阅读Rails的代码,那你可千万别尝试。相比之下,Go更加简单易懂。即使是初学者,在完成了Go官方教程后,就可以阅读语言规范,甚至阅读最复杂的代码也没问题。因为Go的设计就是如此简单。 - kik
23
@kik 这没有任何意义。简单并不意味着没有特性。排序是库中最重要和基础的特性之一,但却是简单的。Golang拥有标准库来处理html模板、crc32哈希、打印机、扫描仪等,这并不会使它变得不简单。在标准库中没有排序不是因为简单性的问题,而是缺少所有语言都认为是标准的基本特性。即使是C也有排序函数。不要在Golang方面太过精英主义,开始考虑Golang可能确实在这个问题上出了错(如果它确实没有排序函数)。 - Eksapsy
1
在go1.8中添加了func Slice函数。 - Jerry An

50

截至Go 1.8版本,@AndreKR的回答是更好的解决方案。


您可以实现一个实现排序接口的集合类型。

这里有两个示例,它们允许您按Axis或Name进行排序:

package main

import "log"
import "sort"

// AxisSorter sorts planets by axis.
type AxisSorter []Planet

func (a AxisSorter) Len() int           { return len(a) }
func (a AxisSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a AxisSorter) Less(i, j int) bool { return a[i].Axis < a[j].Axis }

// NameSorter sorts planets by name.
type NameSorter []Planet

func (a NameSorter) Len() int           { return len(a) }
func (a NameSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a NameSorter) Less(i, j int) bool { return a[i].Name < a[j].Name }

type Planet struct {
    Name       string  `json:"name"`
    Aphelion   float64 `json:"aphelion"`   // in million km
    Perihelion float64 `json:"perihelion"` // in million km
    Axis       int64   `json:"Axis"`       // in km
    Radius     float64 `json:"radius"`
}

func main() {
    var mars Planet
    mars.Name = "Mars"
    mars.Aphelion = 249.2
    mars.Perihelion = 206.7
    mars.Axis = 227939100
    mars.Radius = 3389.5

    var earth Planet
    earth.Name = "Earth"
    earth.Aphelion = 151.930
    earth.Perihelion = 147.095
    earth.Axis = 149598261
    earth.Radius = 6371.0

    var venus Planet
    venus.Name = "Venus"
    venus.Aphelion = 108.939
    venus.Perihelion = 107.477
    venus.Axis = 108208000
    venus.Radius = 6051.8

    planets := []Planet{mars, venus, earth}
    log.Println("unsorted:", planets)

    sort.Sort(AxisSorter(planets))
    log.Println("by axis:", planets)

    sort.Sort(NameSorter(planets))
    log.Println("by name:", planets)
}

这就是我链接的冗长解决方案,不是吗? - Martin Thoma
2
你在我写这篇文章的时候已经链接了它。抱歉。但是只使用标准工具,没有更短的方法来完成这个任务。 - jimt

7
您可以将 []Planet 中的 Sort interface 实现到包含集合和执行比较的闭包类型上。您需要为每个属性提供比较闭包的实现。我认为这种方法比为结构体的每个属性实现一个 Sort 类型更好。此答案几乎是从 sort 文档 直接摘抄,所以我不能为此过多地负责。
package main

import (
    "log"
    "sort"
)

type Planet struct {
    Name       string  `json:"name"`
    Aphelion   float64 `json:"aphelion"`   // in million km
    Perihelion float64 `json:"perihelion"` // in million km
    Axis       int64   `json:"Axis"`       // in km
    Radius     float64 `json:"radius"`
}

type By func(p1, p2 *Planet) bool

func (by By) Sort(planets []Planet) {
    ps := &planetSorter{
        planets: planets,
        by:      by, 
    }
    sort.Sort(ps)
}

type planetSorter struct {
    planets []Planet
    by      func(p1, p2 *Planet) bool 
}

func (s *planetSorter) Len() int {
    return len(s.planets)
}

func (s *planetSorter) Swap(i, j int) {
    s.planets[i], s.planets[j] = s.planets[j], s.planets[i]
}

func (s *planetSorter) Less(i, j int) bool {
    return s.by(&s.planets[i], &s.planets[j])
}

如何调用它。
func main() {
    /* Same code as in the question */

    planets := []Planet{*mars, *venus, *earth}

    By(func(p1, p2 *Planet) bool {
        return p1.Name < p2.Name
    }).Sort(planets)

    log.Println(planets)

    By(func(p1, p2 *Planet) bool {
        return p1.Axis < p2.Axis
    }).Sort(planets)

    log.Println(planets)
}

Here is a Demo


3

这里有另一种减少样板代码的方法。免责声明,它使用反射并且会失去类型安全。

这是一个演示

所有的魔法都发生在Prop函数中。它接收要排序的结构体属性和您想要排序的顺序(升序、降序),并返回执行比较的函数。

package main

import (
    "log"
    "reflect"
    "sort"
)

func test(planets []Planet) {
    log.Println("Sort Name")
    By(Prop("Name", true)).Sort(planets)
    log.Println(planets)

    log.Println("Sort Aphelion")
    By(Prop("Aphelion", true)).Sort(planets)
    log.Println(planets)

    log.Println("Sort Perihelion")
    By(Prop("Perihelion", true)).Sort(planets)
    log.Println(planets)

    log.Println("Sort Axis")
    By(Prop("Axis", true)).Sort(planets)
    log.Println(planets)

    log.Println("Sort Radius")
    By(Prop("Radius", true)).Sort(planets)
    log.Println(planets)
}

func Prop(field string, asc bool) func(p1, p2 *Planet) bool {
    return func(p1, p2 *Planet) bool {

        v1 := reflect.Indirect(reflect.ValueOf(p1)).FieldByName(field)
        v2 := reflect.Indirect(reflect.ValueOf(p2)).FieldByName(field)

        ret := false

        switch v1.Kind() {
        case reflect.Int64:
            ret = int64(v1.Int()) < int64(v2.Int())
        case reflect.Float64:
            ret = float64(v1.Float()) < float64(v2.Float())
        case reflect.String:
            ret = string(v1.String()) < string(v2.String())
        }

        if asc {
            return ret
        }
        return !ret
    }
}

type Planet struct {
    Name       string  `json:"name"`
    Aphelion   float64 `json:"aphelion"`   // in million km
    Perihelion float64 `json:"perihelion"` // in million km
    Axis       int64   `json:"Axis"`       // in km
    Radius     float64 `json:"radius"`
}

type By func(p1, p2 *Planet) bool

func (by By) Sort(planets []Planet) {
    ps := &planetSorter{
        planets: planets,
        by:      by, // The Sort method's receiver is the function (closure) that defines the sort order.
    }
    sort.Sort(ps)
}

type planetSorter struct {
    planets []Planet
    by      func(p1, p2 *Planet) bool // Closure used in the Less method.
}

// Len is part of sort.Interface.
func (s *planetSorter) Len() int { return len(s.planets) }

// Swap is part of sort.Interface.
func (s *planetSorter) Swap(i, j int) {
    s.planets[i], s.planets[j] = s.planets[j], s.planets[i]
}

// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter.
func (s *planetSorter) Less(i, j int) bool {
    return s.by(&s.planets[i], &s.planets[j])
}

func main() {
    test(dataSet())
}

func dataSet() []Planet {

    var mars = new(Planet)
    mars.Name = "Mars"
    mars.Aphelion = 249.2
    mars.Perihelion = 206.7
    mars.Axis = 227939100
    mars.Radius = 3389.5

    var earth = new(Planet)
    earth.Name = "Earth"
    earth.Aphelion = 151.930
    earth.Perihelion = 147.095
    earth.Axis = 149598261
    earth.Radius = 6371.0

    var venus = new(Planet)
    venus.Name = "Venus"
    venus.Aphelion = 108.939
    venus.Perihelion = 107.477
    venus.Axis = 108208000
    venus.Radius = 6051.8

    return []Planet{*mars, *venus, *earth}
}

1

其他答案已经很好了。

我想为按结构体值排序的字母顺序添加一个示例,该值是一个字符串。在Go中基本相同,可能对具有其他编程语言背景的人有所帮助 :)

type My struct {
    Val string
}

func main() {
    m1 := My{Val: "B"}
    m2 := My{Val: "C"}
    m3 := My{Val: "A"}
    m4 := My{Val: "D"}
    mList := []My{m1, m2, m3, m4}
    sort.Slice(mList, func(i, j int) bool {
        return mList[i].Val < mList[j].Val
    })
    fmt.Println("Sorted:", mList[0], mList[1], mList[2], mList[3])
}

0
你也可以使用快速排序实现,并在分区函数内选择要排序的字段,例如我选择 Name。
package main

import (
    "fmt"
)

type Planet struct {
    Name       string  `json:"name"`
    Aphelion   float64 `json:"aphelion"`   // in million km
    Perihelion float64 `json:"perihelion"` // in million km
    Axis       int64   `json:"Axis"`       // in km
    Radius     float64 `json:"radius"`
}

func main() {
    var mars Planet
    mars.Name = "Mars"
    mars.Aphelion = 249.2
    mars.Perihelion = 206.7
    mars.Axis = 227939100
    mars.Radius = 3389.5

    var earth Planet
    earth.Name = "Earth"
    earth.Aphelion = 151.930
    earth.Perihelion = 147.095
    earth.Axis = 149598261
    earth.Radius = 6371.0

    var venus Planet
    venus.Name = "Venus"
    venus.Aphelion = 108.939
    venus.Perihelion = 107.477
    venus.Axis = 108208000
    venus.Radius = 6051.8

    planets := []Planet{mars, venus, earth}
    fmt.Println(quickSort(&planets,0,len(planets)-1))

}

func quickSort(arr *[]Planet, start, end int)[]Planet{
    if start < end{
        partitionIndex := partition(*arr,start,end)
        quickSort(arr,start,partitionIndex-1)
        quickSort(arr,partitionIndex+1, end)
    }
    return *arr
}

func partition(arr []Planet, start, end int) int{
    pivot := arr[end].Name
    pIndex := start
    for i:= start; i<end; i++{
        if arr[i].Name <= pivot{
            //  swap
            arr[i],arr[pIndex] = arr[pIndex],arr[i]
            pIndex++
        }
    }
    arr[pIndex],arr[end] = arr[end],arr[pIndex]
    return pIndex
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接