はじめに
前回の記事では、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以降でないと使えないことやその他の条件によって適切な方法を選択する必要がありそうです。