Avatar ·

Как проще всего отсортировать срез структур по произвольному полю в Go?

Всем привет! Я новичок в Go и никак не могу разобраться с сортировкой срезов структур. Допустим, у меня есть такой код: ```go type Person struct { Name string Age int } people := []Person{ {"Alice", 30}, {"Bob", 25}, {"Charlie", 35}, } ``` Я хочу отсортировать этот срез по полю `Name` (в алфавитном порядке). Потом, возможно, по полю `Age` (по возрастанию). В других языках (например, в Python) я бы просто передал ключ сортировки: `sorted(people, key=lambda p: p.name)`. Но в Go я вижу, что нужно писать отдельный тип, реализовывать интерфейс `sort.Interface` с тремя методами — `Len()`, `Less()`, `Swap()`. Это очень многословно! Есть ли более короткий и элегантный способ сделать это? Может быть, какая-то встроенная функция или пакет, который позволяет просто указать имя поля? Или я обречен писать по 10 строк кода для каждой сортировки? Заранее спасибо за помощь! <<>>

Краткий ответ: используйте sort.Slice и анонимные функции

Да, в Go есть элегантный способ — функция sort.Slice из стандартного пакета sort. Она появилась в Go 1.8 и позволяет сортировать срез без создания отдельного типа. Вам нужно только передать функцию-компаратор, которая сравнивает два элемента по нужному полю.

Вот как это выглядит для вашего примера:

package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
    }

    // Сортировка по имени (алфавитный порядок)
    sort.Slice(people, func(i, j int) bool {
        return people[i].Name < people[j].Name
    })

    fmt.Println("По имени:", people)

    // Сортировка по возрасту (по возрастанию)
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age < people[j].Age
    })

    fmt.Println("По возрасту:", people)
}

Вывод:

По имени: [{Alice 30} {Bob 25} {Charlie 35}]
По возрасту: [{Bob 25} {Alice 30} {Charlie 35}]

Как это работает

  • sort.Slice принимает два аргумента: сам срез (как interface{}) и функцию-компаратор.
  • Компаратор func(i, j int) bool должен вернуть true, если элемент с индексом i должен стоять раньше элемента с индексом j.
  • Внутри компаратора вы обращаетесь к нужному полю структуры и сравниваете его.
  • Для обратной сортировки (по убыванию) просто поменяйте знак: people[i].Age > people[j].Age.

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

Если нужно отсортировать сначала по одному полю, а при равенстве — по другому, просто добавьте проверку:

sort.Slice(people, func(i, j int) bool {
    if people[i].Age != people[j].Age {
        return people[i].Age < people[j].Age // сначала по возрасту
    }
    return people[i].Name < people[j].Name   // потом по имени
})

Почему это лучший подход

  • Минимум кода — не нужно писать отдельный тип и три метода.
  • Гибкость — можно сортировать по любому полю, даже по вычисляемому значению.
  • Производительность — sort.Slice работает так же эффективно, как и реализация через интерфейс.
  • Безопасность типов — компаратор работает с конкретным типом, ошибки на этапе компиляции.

Важное замечание

Начиная с Go 1.21 появилась функция slices.SortFunc из пакета slices, которая делает то же самое, но с более строгой типизацией и чуть лучшей производительностью. Пример:

import "slices"

slices.SortFunc(people, func(a, b Person) int {
    if a.Age < b.Age {
        return -1
    } else if a.Age > b.Age {
        return 1
    }
    return 0
})

Но для простоты и совместимости с более старыми версиями Go я рекомендую sort.Slice — он идеально подходит для 95% задач сортировки структур.

<<>>

Альтернатива: сортировка через sort.Interface с дженериками

Если вы работаете с Go 1.18+ и у вас много разных структур, которые нужно сортировать по разным полям, можно написать универсальную обёртку с использованием дженериков. Это более "инженерный" подход, который пригодится в больших проектах.

Пример:

package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

// Slice сортирует срез по переданной функции-компаратору
func Slice[T any](s []T, less func(a, b T) bool) {
    sort.Slice(s, func(i, j int) bool {
        return less(s[i], s[j])
    })
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
    }

    // Сортировка по имени
    Slice(people, func(a, b Person) bool {
        return a.Name < b.Name
    })

    fmt.Println(people)
}

Этот подход:

  • Даёт читаемый код с явным именем функции Slice.
  • Легко переиспользуется для любых типов.
  • Позволяет вынести компараторы в отдельные переменные или функции.

Однако для разовой сортировки проще и быстрее использовать sort.Slice напрямую — не нужно писать вспомогательные функции. Выбирайте этот вариант, если в вашем проекте сортировка структур встречается десятки раз.