はじめに
前回の記事では、Goで文字列を辞書順にソートする方法の一つとしてsort.Sliceを使った方法を紹介しました。
公式ドキュメントのsort.Sliceの項目を読んでいるとslices.SortFuncという関数が紹介されており、これでも同様のことができそうでした。
そこでこの記事では、slices.SortFuncの使い方とsort.Sliceとslices.SortFuncの違いや処理効率の良い方はどちらなのかについて調べてみました。
slices.SortFuncって何?
これはGo 1.21からslicesパッケージに入った新機能で、任意のスライスを、指定した比較関数でソートする関数です。
似たようなことはsort.Slicesでもできますが、slices.SortFuncはそれをより型安全&多少高速にしたものです。
slices.SortFuncで文字列を辞書順にソートする
slices.SortFuncも使い方はsort.Sliceと似ています。
// 第一引数にソートしたいスライス、第二引数にどんなルールでソートするかの比較関数を渡す
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を使って大文字を小文字に変換してから比較を行なっています。
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)
}
出力結果
[alice Bob VERA]
ログを出力して処理過程を見てみる
前回同様、ログを仕込んで処理過程も見てみます。
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)
}
出力結果
[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)を作成します。
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以降でないと使えないことやその他の条件によって適切な方法を選択する必要がありそうです。