70

I have an array/slice of members:

type Member struct {
    Id int
    LastName string
    FirstName string
}

var members []Member

My question is how to sort them by LastName and then by FirstName.

2
  • 1
    Did you have a look at the example given in the official documentation to package sort? golang.org/pkg/sort/#example__sortMultiKeys Commented Mar 21, 2016 at 6:53
  • Since Go 1.8, @abourget's answer is better (because shorter) than the currently accepted one. Commented Mar 18, 2017 at 8:39

13 Answers 13

109

In Go 1.22 and later, use slices.SortFunc, cmp.Or and cmp.Compare to write a concise and readable sort:

slices.SortFunc(members, func(a, b Member) int {
    return cmp.Or(
        cmp.Compare(a.LastName, b.LastName),
        cmp.Compare(a.FirstName, b.FirstName),
    )
})

Use the sort.Slice (available since Go 1.8) or the sort.Sort function to sort a slice of values.

With both functions, the application provides a function that tests if one slice element is less than another slice element. To sort by last name and then first name, compare last name and then first name:

// If last names are different, then use last
// name to determine whether element i is less than
// element j.
if members[i].LastName != members[j].LastName {
    return members[i].LastName < members[j].LastName
}
// Otherwise, use first name to determine whether
// element i is less than element j. 
return members[i].FirstName < members[j].FirstName

The less function is specified using an anonymous function with sort.Slice:

var members []Member
sort.Slice(members, func(i, j int) bool {
    if members[i].LastName != members[j].LastName {
        return members[i].LastName < members[j].LastName
    }
    return members[i].FirstName < members[j].FirstName
})

The less function is specified with through an interface with the sort.Sort function:

type byLastFirst []Member

func (members byLastFirst) Len() int           { return len(members) }
func (members byLastFirst) Swap(i, j int)      { members[i], members[j] = members[j], members[i] }
func (members byLastFirst) Less(i, j int) bool { 
    if members[i].LastName != members[j].LastName {
        return members[i].LastName < members[j].LastName
    }
    return members[i].FirstName < members[j].FirstName    }

sort.Sort(byLastFirst(members))

Unless performance analysis shows that sorting is a hot spot, use the approach that's most convenient for your application.

Sign up to request clarification or add additional context in comments.

2 Comments

Why do you compare LastName twice but FirstName only once? Should I follow the same procedure of comparing twice if I'm sorting 3, 4 or more fields?
The logic for all but the last field is: return true if less, return false if greater, otherwise compare the next field. The last field only requires the less comparison.
35

Use the newer sort.Slice function as such:

sort.Slice(members, func(i, j int) bool {
    switch strings.Compare(members[i].FirstName, members[j].FirstName) {
    case -1:
        return true
    case 1:
        return false
    }
    return members[i].LastName > members[j].LastName
})

or something like that.

5 Comments

you should mention that this is only available since Go 1.8
Don't use strings.Compare. The documentation at golang.org/pkg/strings/#Compare says "Compare is included only for symmetry with package bytes. It is usually clearer and always faster to use the built-in string comparison operators ==, <, >, and so on."
And if it's possible that both last name and first name can be the same for two members and you want to preserve the original order of the slice, you should use sort.Stable or sort.SliceStable.
strings.Compare makes remember in Java can't compare a simple string with "=="
Here's the source of strings.Compare. It is no different than doing == and <, which are both required in multi-valued comparisons.
23

Another pattern, which I find slightly cleaner:

if members[i].LastName != members[j].LastName {
    return members[i].LastName < members[j].LastName
}

return members[i].FirstName < members[j].FirstName

1 Comment

I like this pattern for readability too. It's even better as the number of fields that need to be compared increases. Still two comparisons for all but the last field though.
8

The shortest and still comprehensible code I managed to write for this is:

package main

import (
    "fmt"
    "sort"
)

type Member struct {
    Id        int
    LastName  string
    FirstName string
}

func sortByLastNameAndFirstName(members []Member) {
    sort.SliceStable(members, func(i, j int) bool {
        mi, mj := members[i], members[j]
        switch {
        case mi.LastName != mj.LastName:
            return mi.LastName < mj.LastName
        default:
            return mi.FirstName < mj.FirstName
        }
    })
}

The pattern using the switch statement easily extends to more than two sorting criteria and is still short enough to be read.

Here's the rest of the program:

func main() {
    members := []Member{
        {0, "The", "quick"},
        {1, "brown", "fox"},
        {2, "jumps", "over"},
        {3, "brown", "grass"},
        {4, "brown", "grass"},
        {5, "brown", "grass"},
        {6, "brown", "grass"},
        {7, "brown", "grass"},
        {8, "brown", "grass"},
        {9, "brown", "grass"},
        {10, "brown", "grass"},
        {11, "brown", "grass"},
    }

    sortByLastNameAndFirstNameFunctional(members)

    for _, member := range members {
        fmt.Println(member)
    }
}

A completely different idea, but same API

If you want to avoid mentioning the fields LastName and FirstName multiple times and if you want to avoid mixing i and j (which can happen all the way), I played around a bit and the basic idea is:

func sortByLastNameAndFirstNameFunctional(members []Member) {
    NewSorter().
        AddStr(member -> member.LastName).
        AddStr(member -> member.FirstName).
        AddInt(member -> member.Id).
        SortStable(members)
}

Since Go doesn't support the -> operator for creating anonymous functions and also doesn't have generics like Java, a bit of syntactical overhead is needed:

func sortByLastNameAndFirstNameFunctional(members []Member) {
    NewSorter().
        AddStr(func(i interface{}) string { return i.(Member).LastName }).
        AddStr(func(i interface{}) string { return i.(Member).FirstName }).
        AddInt(func(i interface{}) int { return i.(Member).Id}).
        SortStable(members)
}

The implementation as well as the API is a bit ugly using interface{} and reflection, but it only mentions each field once, and the application code does not have a single chance to accidentally mix the indexes i and j since it doesn't deal with them.

I designed this API in the spirit of Java's Comparator.comparing.

The infrastructure code for the above sorter is:

type Sorter struct{ keys []Key }

func NewSorter() *Sorter { return new(Sorter) }

func (l *Sorter) AddStr(key StringKey) *Sorter { l.keys = append(l.keys, key); return l }
func (l *Sorter) AddInt(key IntKey) *Sorter    { l.keys = append(l.keys, key); return l }

func (l *Sorter) SortStable(slice interface{}) {
    value := reflect.ValueOf(slice)
    sort.SliceStable(slice, func(i, j int) bool {
        si := value.Index(i).Interface()
        sj := value.Index(j).Interface()
        for _, key := range l.keys {
            if key.Less(si, sj) {
                return true
            }
            if key.Less(sj, si) {
                return false
            }
        }
        return false
    })
}

type Key interface {
    Less(a, b interface{}) bool
}

type StringKey func(interface{}) string

func (k StringKey) Less(a, b interface{}) bool  { return k(a) < k(b) }

type IntKey func(interface{}) int

func (k IntKey) Less(a, b interface{}) bool  { return k(a) < k(b) }

Comments

5

A slightly cleaner solution than the currently-accepted answer is to do something more like this:

sort.Slice(members, func(i, j int) bool {
    if members[i].FirstName != members[j].FirstName {
        return members[i].FirstName < members[j].FirstName
    }
    return members[i].LastName < members[j].LastName
})

The nice thing about doing it this way is that it's much clearer how you would extend it if you added a new field Age (or something):

sort.Slice(members, func(i, j int) bool {
    if members[i].FirstName != members[j].FirstName {
        return members[i].FirstName < members[j].FirstName
    }
    if members[i].Age != members[j].Age {
        return members[i].Age < members[j].Age
    }
    return members[i].LastName < members[j].LastName
})

So the pattern is that you add an if X[i] != X[j] statement for all properties until the last one, which is just compared normally.

Comments

3

This is how I usually do it:

// You can edit this code!
// Click here and start typing.
package main

import (
    "fmt"
    "sort"
    "strings"
)

type User struct {
    Name string
    Age  int
}

func main() {
    users := []User{
        {"A", 10},
        {"B", 10},
        {"C", 20},
        {"D", 5},
        {"A", 100},
    }
    sort.Slice(users, func(i, j int) bool {
        lhs, rhs := users[i], users[j]
        byAge := lhs.Age - rhs.Age
        byName := strings.Compare(lhs.Name, rhs.Name) // Returns 0 if equal, -1 if lhs is less than rhs, and 1 if lhs is greater than rhs
        
        // The + sign is not necessary, but adds clarity as it means increasing in value, aka ascending.
        // sortBy(+byAge, +byName) // Sort by age asc, by name asc
        // sortBy(-byAge, +byName) // Sort by age desc, by name asc
        // sortBy(+byName, +byAge) // Sort by name asc, by age asc
        return sortBy(-byAge, -byName) // Sort by age desc, by name desc
    })
    fmt.Println(users)
}

func sortBy(sc ...int) bool {
    for _, c := range sc {
        if c != 0 {
            return c < 0
        }
    }
    return sc[len(sc)-1] < 0
}

Comments

1

This has been very helpful. I needed to sort a slice of structs, and found my answer here. I actually extended it to triply-sorting. Although sorting this much is not optimal for runtime purposes, it's useful in some circumstances, especially when the alternative leads to code that is hard to maintain or modify and where faster runtimes are not crucial.

sort.Slice(servers, func(i, j int) bool {
        if servers[i].code < servers[j].code {
            return true
        } else if servers[i].code > servers[j].code {
            return false
        } else {
            if servers[i].group < servers[j].group {
                return true
            } else {
                if servers[i].group > servers[j].group {
                    return false
                }
            }
        }
        return servers[i].IDGroup < servers[j]. IDGroup
    })

This code sorts first by code, then by group, then by IDGroup.

Comments

1

Just created go project to implement it: https://github.com/itroot/keysort

You can just return a list of fields to sort:

package main

import (
    "fmt"

    "github.com/itroot/keysort"
)

type Data struct {
    A int
    B string
    C bool
}

func main() {
    slice := []Data{{1, "2", false}, {2, "2", false}, {2, "1", true}, {2, "1", false}}
    keysort.Sort(slice, func(i int) keysort.Sortable {
        e := slice[i]
        return keysort.Sequence{e.A, e.B, e.C}
    })
    fmt.Println(slice)
}

Playground link: https://play.golang.org/p/reEDcoXNiwh

2 Comments

This produces [{1 2 false} {2 1 false} {2 1 true} {2 2 false}] where B is sorted 2,1,1,2 which is clearly not correct ordering.
This is an expected behaviour, as it works akin sorting tuples in Python, or SQL ORDER BY A, B, C. If you want to sort by B first, you need to do return keysort.Sequence{e.B, e.A, e.C} - go.dev/play/p/FEdr1ND4Xme, and it will return [{2 1 false} {2 1 true} {1 2 false} {2 2 false}]
1

You can create a slice of functions:

package main

type (
   mFunc func(a, b member) bool
   member struct { lastName, firstName string }
)

var members = []member{
   {"Mullen", "Larry"},
   {"Howard", "James"},
   {"Clayton", "Adam"},
   {"Howard", "Ben"},
}

var mFuncs = []mFunc{
   func(a, b member) bool { return a.lastName < b.lastName },
   func(a, b member) bool { return a.firstName < b.firstName },
}

and then iterate through the functions, until one of the functions finds a difference:

package main

import (
   "fmt"
   "sort"
)

func main() {
   sort.Slice(members, func(a, b int) bool {
      ma, mb := members[a], members[b]
      for _, mf := range mFuncs {
         if mf(ma, mb) {
            return true
         }
         if mf(mb, ma) {
            break
         }
      }
      return false
   })
   fmt.Println(members) // [{Clayton Adam} {Howard Ben} {Howard James} {Mullen Larry}]
}

https://golang.org/pkg/sort#example__sortMultiKeys

Comments

1

Introduced in Go 1.21

import (
"cmp"
"fmt"
"slices"
)

func main() {
type Person struct {
    Name string
    Age  int
}
people := []Person{
    {"Gopher", 13},
    {"Alice", 55},
    {"Bob", 24},
    {"Alice", 20},
}
slices.SortFunc(people, func(a, b Person) int {
    if n := cmp.Compare(a.Name, b.Name); n != 0 {
        return n
    }
    // If names are equal, order by age
    return cmp.Compare(a.Age, b.Age)
})
fmt.Println(people)
}

Comments

0

Just read the code of the SortMultiKeys example in the official Go documentation at https://pkg.go.dev/sort and adapt it to your Member structure.

To keep this answer up to date I won't copy and paste the whole example here, but eventually you'll be able to write it as follows:

OrderedBy(lastName, firstName).Sort(members)

If the requirement changed to sort also by age, you'd simply add the age closure:

OrderBy(lastName, firstName, age).Sort(members)

Comments

-1

All good answers, If you don't want to write any configuration. You can use the external package to perform sorting.

go get -d github.com/raunakjodhawat/multisort

And call the function like so:

sortedSlice, err := multisort.MultiSorted(inputSlice, inputKeys, SortOrders)

To look at a concrete example, go to: https://github.com/raunakjodhawat/multisort

Comments

-4

Here is a slightly more concise implementation of the accepted answer:

package main

import (
    "fmt"
    "sort"
)

type Member struct {
    FirstName string
    LastName  string
}

func main() {
    members := []Member{
        {"John", "Doe"},
        {"Jane", "Doe"},
        {"Mary", "Contrary"},
    }

    fmt.Println(members)

    sort.Slice(members, func(i, j int) bool {
        return members[i].LastName < members[j].LastName || members[i].FirstName < members[j].FirstName
    })

    fmt.Println(members)
}

Running will print the following output:

[{John Doe} {Jane Doe} {Mary Contrary}]
[{Mary Contrary} {Jane Doe} {John Doe}]

This is as expected: the members are printed in order of ascending last name, where in the case of a tie, they are printed in order of first name.

2 Comments

This appears to missort [{John Doe} {Zach Darren} {Jane Doe} {Mary Contrary}] as [{Mary Contrary} {Jane Doe} {Zach Darren} {John Doe}].
Just missed one clause in there. members[i].LastName < members[j].LastName || (members[i].LastName == members[j].LastName && members[i].FirstName < members[j].FirstName) => [{Mary Contrary} {Zach Darren} {Jane Doe} {John Doe}]

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.