[Go] slices.SortFuncで文字列を辞書順にソートする

# はじめに
[前回の記事](https://qiita.com/k21071/items/9ec0c45983177c0f4cc9)では、Goで文字列を辞書順にソートする方法の一つとして`sort.Slice`を使った方法を紹介しました。
公式ドキュメントの`sort.Slice`の項目を読んでいると[`slices.SortFunc`](https://pkg.go.dev/slices#SortFunc)という関数が紹介されており、これでも同様のことができそうでした。
そこでこの記事では、`slices.SortFunc`の使い方と`sort.Slice`と`slices.SortFunc`の違いや処理効率の良い方はどちらなのかについて調べてみました。

# slices.SortFuncって何？
これはGo 1.21から`slices`パッケージに入った新機能で、**任意のスライスを、指定した比較関数でソートする関数**です。
似たようなことは`sort.Slices`でもできますが、`slices.SortFunc`はそれをより**型安全＆多少高速**にしたものです。

# slices.SortFuncで文字列を辞書順にソートする
`slices.SortFunc`も使い方は`sort.Slice`と似ています。

```go
// 第一引数にソートしたいスライス、第二引数にどんなルールでソートするかの比較関数を渡す
slices.SortFunc(slice, lessFunc)
// 例
slices.SortFunc(names, func(a, b string) int {
		return strings.Compare(strings.ToLower(a), strings.ToLower(b))
})
```
`strings.Compare`は比較したい2つのstring型の値(a, b)を引数として渡すと、比較結果によって以下のint型の値が返ってきます。
`a < b` → -1
`a > b` → ＋1
`a = b` → 0
ここでは`strings.ToLower`を使って大文字を小文字に変換してから比較を行なっています。

```main.go
package main

import (
	"fmt"
	"slices"
	"strings"
)

func main() {
	names := []string{"Bob", "alice", "VERA"}
    
	slices.SortFunc(names, func(a, b string) int {
		return strings.Compare(strings.ToLower(a), strings.ToLower(b))
	})
	fmt.Println(names)
}
```
**出力結果**
```go
[alice Bob VERA]
```

## ログを出力して処理過程を見てみる
前回同様、ログを仕込んで処理過程も見てみます。

```go
package main

import (
	"fmt"
	"slices"
	"strings"
)

func main() {
	names := []string{"Bob", "alice", "VERA"}

	slices.SortFunc(names, func(a, b string) int {
		fmt.Println(names)
		fmt.Printf("Comparing: a = %v vs b = %v\n", a, b)
		fmt.Println(strings.Compare(strings.ToLower(a), strings.ToLower(b)))
		return strings.Compare(strings.ToLower(a), strings.ToLower(b))
	})
	
	fmt.Printf("result: %v", names)
}
```
**出力結果**
```go
[Bob alice VERA]
Comparing: a = alice vs b = Bob
-1
[alice Bob VERA]
Comparing: a = VERA vs b = Bob
1
result: [alice Bob VERA]
```
出力結果を見ると、初めはaに`alice`、bに`Bob`が入っています。`a < b`なので`-1`が返ってきて要素の入れ替えが発生します。次に、aには`VERA`、bには`Bob`が入っています。`a > b`なので`1`が返ってきて要素の入れ替えは発生しません。

# sort.Slicesとslices.SortFuncの速度を比較してみる
`slices.SortFunc`でも文字列を辞書順にソートすることができました。
では、`sort.Slice`と`slices.SortFunc`では処理効率が良いのはどちらでしょうか？
ベンチマークテストを行なって処理効率を比較してみようと思います。
今回の動作環境におけるGoのバージョンは`go1.24.2`を使用しています。

**ベンチマークテスト**
Goには標準でベンチマーク用のツール(`testing.B`)が組み込まれており、お手軽に性能比較を行うことができます。
まずはテストファイル(`sort_benchmark_test.go`)を作成します。

```sort_benchmark_test.go
package main

import (
	"math/rand"
	"slices"
	"sort"
	"testing"
	"time"
)

func generateTestData() []string {
	// rand.NewSourceでシードを与え、rand.Newで新しい乱数生成器を作る
	source := rand.NewSource(time.Now().UnixNano())
	rng := rand.New(source)

	names := []string{
		"James", "Mary", "John", "Patricia", "Robert", "Jennifer",
		"Michael", "Linda", "William", "Elizabeth", "David", "Barbara",
		"Richard", "Susan", "Joseph", "Jessica", "Thomas", "Sarah",
		"Charles", "Karen",
	}

	var data []string
	for i := 0; i < 1000000; i++ {
		data = append(data, names[rng.Intn(len(names))]) // rng.Intnを使って乱数を生成
	}
	return data
}

func BenchmarkSortSlice(b *testing.B) {
	for i := 0; i < b.N; i++ {
		data := generateTestData()
		sort.Slice(data, func(i, j int) bool {
			return data[i] < data[j]
		})
	}
}

func BenchmarkSlicesSortFunc(b *testing.B) {
	for i := 0; i < b.N; i++ {
		data := generateTestData()
		slices.SortFunc(data, func(a, b string) int {
			if a < b {
				return -1
			} else if a > b {
				return 1
			}
			return 0
		})
	}
}
```

**ディレクトリ構造**
```
slicestest
├─ main.go
├─ go.mod
└─ sort_benchmark_test.go
```

テストファイルが用意できたら以下のコマンドを実行してベンチマークを開始します。

```
$ go test -bench=. -benchmem
```
**実行結果**
```
$ go test -bench=. -benchmem

goos: darwin
goarch: arm64
pkg: slicestest
cpu: Apple M1
BenchmarkSortSlice-8        	      18	  65553181 ns/op	88022706 B/op	      41 allocs/op
BenchmarkSlicesSortFunc-8   	      18	  63693988 ns/op	88022645 B/op	      39 allocs/op
PASS
ok  	slicestest	5.008s
```

`ns/op`: 1回のベンチマークにかかる時間（ナノ秒）

`B/op`: 1回のベンチマークにおけるメモリ使用量（バイト）

`allocs/op`: 1回のベンチマークで発生したメモリアロケーション回数

`-benchmem`オプションをつけることでメモリ使用量やメモリアロケーション回数なども見ることができます。
ベンチマークの実行結果を見たところ、名前データが100万件あったとしても処理効率はほとんど変わらず、多少`slices.SortFunc`の処理効率が良いかなという程度でした。

# sort.Slicesとslice.SortFuncの違い
項目 | sort.Slice | slices.SortFunc
|-----------|------------|------------|
パッケージ | sort（Go 1.8〜） | slices（Go 1.21〜）
比較方法 | func(i, j int) bool | func(a, b T) int（汎用的）
型の安全性 | 型安全でない（インターフェース的） | 型安全（ジェネリクス使用）
内部処理 | sort.Sort() + インデックスアクセス | ジェネリクスベースのソートロジック
ユースケース | 任意の構造・配列に柔軟対応 | 同じ型のスライスに最適
推奨度（Go 1.21以降） | 古くても使われる | より推奨される（型安全）

# まとめ
本記事では、Goで文字列を辞書順にソートする方法として`slices.SortFunc`を使った方法を紹介しました。
`sort.Slice`と`slices.SortFunc`でベンチマークを取ってみましたが、処理効率にさほど差はないように見えました。
しかし、`slices.SortFunc`はGo1.21以降でないと使えないことやその他の条件によって適切な方法を選択する必要がありそうです。