HOWTO · Go

Goで構造体のスライスを並べ替える方法

このチュートリアルでは、GoLang で構造体のスライスを並べ替える方法を示します。

このページの内容

標準ライブラリの sort パッケージを使うと、構造体のスライスを並べ替えられます。sort.Slice にはスライスと、2つのインデックスを受け取る比較関数を渡します。slice[i] と slice[j] のフィールドを比較してください。スライスはその場で並べ替えられ、比較演算子を < から > に変えると、昇順から降順になります。

各例では、並べ替え後のスライスを示します。並べ替えは元のスライスを変更するため、入力時の順序も必要な場合は、先にスライスをコピーしてください。スライスヘッダーだけをコピーしても、独立した順序は保たれません。

sort.Sliceで構造体スライスを1つのフィールドで並べ替える

この例では、従業員を給与の低い順に並べ替えます。比較関数は、元のスライス内のインデックスを使って各従業員を参照します。

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とAvaのうち、入力で先だったZoeが先に残ります。安定ソートは、ほかの要素型と同じように構造体にも使えます。

タイブレーカーを使って複数のフィールドで並べ替える

主キーと副キーで並べ替えるには、まず主キーの値を比較し、一致した場合に副キーの値を比較します。この例では、役職をアルファベット順に並べ、同じ役職の中では名前をアルファベット順に並べます。

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 を参照します。2つ目の比較がタイブレーカーとなり、どちらの比較も昇順で並びます。役職ごとの順序を比較関数自体が定めるため、ここでは sort.SliceStable は必要ありません。

再利用できる並べ替えのためにsort.Interfaceを実装する

並べ替えルールを再利用するには、名前付きスライス型を定義して sort.Interface を実装します。Len は要素数を返し、Less は要素の順序を定め、Swap は2つの要素を入れ替えます。その後、スライスを 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 が必要な3つのメソッドを実装しているため、sort.Sort で給与順に並べ替えられます。同じ並べ替えルールに名前を付けて再利用したい場合や、並べ替えの処理を独自のコレクション型にまとめたい場合に便利です。1回限りの比較なら、通常は sort.Slice のほうが簡潔です。

並べ替えの条件が1回の処理だけで使われる場合や、呼び出し側が選んだフィールドに応じて変わる場合は sort.Slice を選びます。名前付きの順序を複数箇所で再利用する場合や、コレクションの抽象化に含める場合は sort.Interface を使います。どちらの場合も、キーが同じレコードの相対順序が変わってよいかを確認してください。入力時の順序を保つ必要がある場合は sort.SliceStable を選びます。