HOWTO · Go

Go에서 구조체 슬라이스 정렬하는 방법

이 튜토리얼에서는 Go에서 구조체 슬라이스를 정렬하는 방법을 알아봅니다.

이 페이지의 내용

표준 라이브러리의 sort 패키지를 사용하면 구조체 슬라이스를 정렬할 수 있습니다. sort.Slice는 슬라이스와 두 인덱스를 받는 비교 함수를 인수로 받습니다. 비교 함수에서 slice[i]와 slice[j]의 필드를 비교하면 됩니다. 정렬은 슬라이스 자체에 적용되며, 비교 연산자를 <에서 >로 바꾸면 오름차순이 내림차순으로 바뀝니다.

각 정렬 예제에는 정렬 후의 슬라이스가 나옵니다. 정렬이 원본에 적용되므로 기존 순서도 유지해야 한다면 먼저 슬라이스를 복사해야 합니다. 슬라이스 헤더만 복사하면 독립적인 순서가 보존되지 않습니다.

구조체 슬라이스를 한 필드로 정렬하기: sort.Slice

다음 완전한 예제는 직원들을 급여가 낮은 순서부터 높은 순서로 정렬합니다. 비교 함수는 원래 슬라이스의 인덱스를 통해 각 직원을 읽습니다.

package main

import (
	"fmt"
	"sort"
)

type Employee struct {
	Name   string
	Salary int
}

func main() {
	employees := []Employee{
		{Name: "John", Salary: 1500},
		{Name: "Joe", Salary: 3000},
		{Name: "Jack", Salary: 3400},
	}

	sort.Slice(employees, func(i, j int) bool {
		return employees[i].Salary < employees[j].Salary
	})

	fmt.Println(employees)
}
[{John 1500} {Joe 3000} {Jack 3400}]

< 비교는 급여가 낮은 항목을 앞에 둡니다. 급여가 높은 순서부터 정렬하려면 비교에 >를 사용하세요. sort.Slice는 전달된 슬라이스를 변경하며, 급여가 같은 레코드의 순서가 유지된다고 보장하지 않습니다.

같은 키의 순서 보존하기: sort.SliceStable

sort.SliceStable도 인덱스를 사용하는 비교 함수로 구조체를 정렬합니다. 안정 정렬은 키가 같은 레코드들의 상대적인 입력 순서를 보존합니다. 순위나 가져온 순서처럼 기존 순서에 의미가 있을 때 유용합니다.

package main

import (
	"fmt"
	"sort"
)

type Employee struct {
	Name   string
	Salary int
}

func main() {
	employees := []Employee{
		{Name: "Zoe", Salary: 3000},
		{Name: "John", Salary: 1500},
		{Name: "Ava", Salary: 3000},
	}

	sort.SliceStable(employees, func(i, j int) bool {
		return employees[i].Salary < employees[j].Salary
	})

	fmt.Println(employees)
}
[{John 1500} {Zoe 3000} {Ava 3000}]

급여를 기준으로 정렬하면서 두 사람의 급여가 같고 Zoe가 먼저 나왔기 때문에 Zoe가 Ava보다 앞에 남습니다. 안정 정렬은 다른 슬라이스 요소 유형과 마찬가지로 구조체에도 사용할 수 있습니다.

보조 기준으로 여러 필드 정렬하기

주 필드와 보조 필드를 차례로 비교해 정렬하려면 먼저 주 필드의 값을 비교하고, 두 값이 같을 때 보조 필드의 값을 비교합니다. 이 예제는 직책을 알파벳순으로 정렬한 뒤, 직책이 같은 항목끼리는 이름을 알파벳순으로 정렬합니다.

package main

import (
	"fmt"
	"sort"
)

type Employee struct {
	Name     string
	Position string
}

func main() {
	employees := []Employee{
		{Name: "Michael", Position: "Developer"},
		{Name: "Jack", Position: "Manager"},
		{Name: "Joe", Position: "CEO"},
		{Name: "Leonard", Position: "Intern"},
		{Name: "Sheldon", Position: "Developer"},
	}

	sort.Slice(employees, func(i, j int) bool {
		if employees[i].Position != employees[j].Position {
			return employees[i].Position < employees[j].Position
		}
		return employees[i].Name < employees[j].Name
	})

	fmt.Println(employees)
}
[{Joe CEO} {Michael Developer} {Sheldon Developer} {Leonard Intern} {Jack Manager}]

비교 함수는 일관된 순서를 정의해야 합니다. 먼저 Position을 비교하고, 두 직책이 같을 때만 Name을 비교합니다. 두 번째 비교가 보조 기준이며, 두 필드 모두 오름차순으로 정렬됩니다. 비교 함수가 직책별 순서까지 정의하므로 여기서는 sort.SliceStable이 필요하지 않습니다.

재사용 가능한 정렬을 위해 sort.Interface 구현하기

정렬 규칙을 재사용하려면 이름이 있는 슬라이스 유형을 정의하고 sort.Interface를 구현하면 됩니다. Len은 항목 수를 반환하고, Less는 정렬 순서를 정의하며, Swap은 두 항목의 위치를 바꿉니다. 그런 다음 슬라이스를 sort.Sort에 전달합니다.

package main

import (
	"fmt"
	"sort"
)

type Employee struct {
	Name   string
	Salary int
}

type BySalary []Employee

func (employees BySalary) Len() int           { return len(employees) }
func (employees BySalary) Swap(i, j int)      { employees[i], employees[j] = employees[j], employees[i] }
func (employees BySalary) Less(i, j int) bool { return employees[i].Salary < employees[j].Salary }

func main() {
	employees := BySalary{
		{Name: "John", Salary: 1500},
		{Name: "Joe", Salary: 3000},
		{Name: "Jack", Salary: 3400},
	}

	sort.Sort(employees)
	fmt.Println(employees)
}
[{John 1500} {Joe 3000} {Jack 3400}]

여기서 BySalary는 필요한 세 메서드를 구현하므로 sort.Sort로 급여순 정렬을 수행할 수 있습니다. 같은 정렬 규칙에 이름을 붙여 여러 곳에서 재사용하거나, 사용자 정의 컬렉션 유형에 정렬 로직을 포함하려는 경우에 유용한 방식입니다. 한 번만 비교할 때는 보통 sort.Slice가 더 간단합니다.

정렬 기준이 한 작업에서만 쓰이거나 호출자가 선택한 필드에 따라 달라진다면 sort.Slice를 사용하세요. 이름이 있는 정렬 규칙을 여러 곳에서 재사용하거나 컬렉션 추상화에 포함하려면 sort.Interface를 사용하세요. 어느 방식을 쓰든 키가 같은 레코드끼리 순서가 바뀌어도 되는지 확인하세요. 원래 순서를 유지해야 한다면 sort.SliceStable을 선택하세요.