Menu

Сортировка слайса в Golang: slices.Sort, SortFunc и несколько полей

Сортировка слайсов в Go через slices.Sort и slices.SortFunc, сортировка структур по одному или нескольким полям через cmp.Compare, стабильная сортировка, сохраняющая порядок равных элементов, и чтение старого кода с sort.Slice.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Сортировка встроенных типов

slices.Sort сортирует любой слайс с упорядоченным типом элементов: целые, числа с плавающей точкой и строки. Она сортирует на месте и ничего не возвращает.

Вывод:

[3 7 19 42 88]
[Bob Carl alice lisa]
true
2 true

Строки сортируются по байтам, поэтому каждая заглавная буква ASCII идёт раньше любой строчной. Для имён пользователи ждут совсем не этого. Следующий раздел это исправляет.

Пакет slices появился в Go 1.21. Его Sort это pattern-defeating quicksort: O(n log n), на месте и нестабильная.

Свой порядок через SortFunc

slices.SortFunc принимает функцию сравнения func(a, b T) int. Возвращайте отрицательное число, когда первым должен идти a, положительное, когда b, и ноль, когда они равны. Помощник cmp.Compare возвращает ровно это для упорядоченных типов.

strings.ToLower внутри сравнения создаёт новую строку всякий раз, когда во входе есть заглавная буква, а сравнение выполняется примерно n log n раз. Для больших слайсов вычислите ключи в нижнем регистре один раз. Для имён на других языках (диакритика, правила локали) используйте golang.org/x/text/collate, который не входит в стандартную библиотеку.

Функция сравнения должна быть согласованной: если она говорит, что a идёт перед b, то должна говорить, что b идёт после a. Функция, которая это нарушает (например, возвращает -1 всякий раз, когда значения различаются), даёт неправильно упорядоченный слайс без всякой ошибки. Вычитание целых (return a - b) выглядит изящно, но переполняется на больших значениях; используйте cmp.Compare.

Сортировка структур

Функция сравнения получает элементы, так что сортировка структур по полю это тот же вызов:

Сотрудники с равной зарплатой здесь могут выйти в любом порядке, потому что SortFunc нестабильна. Если нужен гарантированный порядок, разрешайте равенство дополнительными полями или используйте стабильную сортировку.

Сортировка по нескольким полям

Сначала сравните самое важное поле и переходите к следующему только при равенстве. cmp.Or (Go 1.22) возвращает свой первый ненулевой аргумент, и это превращает всё в одну строку:

Вывод:

eng     150 Cy
eng     120 Ana
eng     120 Eve
sales    90 Bob
sales    90 Dee

Вычисляются все сравнения, даже если решило первое, ведь это обычные аргументы. Для сравнения полей это почти ничего не стоит. Когда дополнительное сравнение дорогое, напишите цепочку if c := ...; c != 0 { return c } вручную.

Стабильная сортировка

Стабильная сортировка сохраняет исходный порядок элементов, которые при сравнении равны. Это важно, когда во входных данных уже есть осмысленный порядок, например записи отсортированы по времени, а теперь вы группируете их по пользователю.

Программа печатает [{ana 2} {ana 4} {bob 1} {bob 3} {bob 5}]: внутри каждого пользователя исходная последовательность сохраняется. Стабильная сортировка делает больше работы, поэтому используйте её, только когда порядок равных элементов важен.

Сортировка мапы

У мап нет порядка. Чтобы показать мапу, отсортированную по ключу, отсортируйте её ключи: slices.Sorted(maps.Keys(m)) (Go 1.23). Чтобы отсортировать по значению, сортируйте ключи сравнением, которое заглядывает в значения:

Дополнительное сравнение по слову важно: без него chan и slice (у обоих 7) печатались бы в разном порядке от запуска к запуску, потому что ключи выходят из мапы в случайном порядке. Подробнее на странице о мапах.

Пакет sort: sort.Slice и компания

До Go 1.21 сортировка шла через пакет sort. Вы увидите его во множестве существующего кода:

sort.Ints(nums)
sort.Strings(names)
sort.Slice(staff, func(i, j int) bool {
	return staff[i].Salary < staff[j].Salary
})
sort.SliceStable(staff, func(i, j int) bool { ... })

Различия, которые стоит знать:

sort.Sliceslices.SortFunc
Функция получаетиндексы i, jэлементы a, b
Возвращаетbool (меньше ли i, чем j)int (отрицательное, ноль, положительное)
Типобезопасностьпринимает any, использует рефлексиюобобщённая, проверяется на этапе компиляции
Скоростьмедленнеебыстрее

Частый баг с sort.Slice: функция «меньше» замыкается на другой слайс, а не на тот, что сортируется, ведь она обращается по позиции. У SortFunc такого бага быть не может, потому что она отдаёт вам сами элементы.

Тип sort.Interface (Len, Less, Swap) это самая старая форма. Через него по-прежнему сортируют данные, которые не являются одним слайсом, например два параллельных слайса, которые должны переставляться вместе. Начиная с Go 1.22 sort.Ints, sort.Strings и sort.Float64s просто вызывают slices.Sort.

Частые ошибки

  • Ожидание, что Sort вернёт отсортированный слайс. Она сортирует на месте и ничего не возвращает. Если нужен новый отсортированный слайс, используйте slices.Sorted(slices.Values(s)) или сначала slices.Clone.
  • Расчёт на то, что равные элементы сохранят порядок. Это обещают только варианты Stable.
  • Сравнение вычитанием. a - b переполняется. Используйте cmp.Compare.
  • Сортировка float с NaN. cmp.Compare ставит NaN перед всеми остальными значениями, и сортировка остаётся согласованной. Самописное сравнение a < b так не умеет.

Часто задаваемые вопросы

Как отсортировать слайс в Go?

Для чисел и строк вызовите slices.Sort(s) (Go 1.21). Она сортирует на месте по возрастанию. Для всего остального или другого порядка используйте slices.SortFunc(s, func(a, b T) int { ... }), где функция возвращает отрицательное число, если a идёт первым, положительное, если первым идёт b, и ноль, если они равны.

Как отсортировать слайс по убыванию в Go?

Поменяйте аргументы местами в сравнении: slices.SortFunc(s, func(a, b int) int { return cmp.Compare(b, a) }). Или отсортируйте по возрастанию, а затем вызовите slices.Reverse(s).

Как отсортировать слайс структур по нескольким полям в Go?

Сравните первое поле и переходите к следующему, только если оно равно. Ровно это делает cmp.Or (Go 1.22): return cmp.Or(cmp.Compare(a.Dept, b.Dept), cmp.Compare(b.Salary, a.Salary), strings.Compare(a.Name, b.Name)) возвращает первый ненулевой результат.

Чем sort.Slice отличается от slices.SortFunc?

sort.Slice(s, func(i, j int) bool) это старый API: он принимает функцию «меньше» по индексам и меняет элементы местами через рефлексию. slices.SortFunc(s, func(a, b T) int) обобщённая, проверяется на этапе компиляции, получает сами элементы и работает быстрее. Новый код должен использовать пакет slices; sort.Slice по-прежнему часто встречается в коде, написанном до Go 1.21.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ