strings.EqualFold 関数を使え
GolangCI が吐くレビュー結果を基にチマチマとコードを直していたのだが,その中で
SA6005: should use strings.EqualFold(a, b) instead of strings.ToLower(a) == strings.ToLower(b)
if strings.ToLower(left) == strings.ToLower(right) {
という指摘があった。 いや,もの知らずでゴメンペコン。
strings
.EqualFold()
関数ってなんじゃら? と思ってソースコードを見たら
// EqualFold reports whether s and t, interpreted as UTF-8 strings,
// are equal under Unicode case-folding.
func EqualFold(s, t string) bool {
...
}
と書かれている。
ふむふむ。 では試してみよう。 こんな感じのコードを書いて
package main
import (
"fmt"
"strings"
)
func main() {
lefts := []string{"go", "go"}
rights := []string{"Go", "GO", "go", "Go", "GO", "go"}
for _, left := range lefts {
for _, right := range rights {
fmt.Printf("%s == %s : %v\n", left, right, strings.EqualFold(left, right))
}
}
}
実行してみると
$ go run sample1.go
go == Go : true
go == GO : true
go == go : true
go == Go : false
go == GO : false
go == go : false
go == Go : false
go == GO : false
go == go : false
go == Go : true
go == GO : true
go == go : true
ってな感じになった。 全角と半角は区別してくれるらしい。 Unicode の文字種をきちんと判別しているということだ。
ちなみに strings
.ToLower()
関数を使って
package main
import (
"fmt"
"strings"
)
func main() {
lefts := []string{"go", "go"}
rights := []string{"Go", "GO", "go", "Go", "GO", "go"}
for _, left := range lefts {
for _, right := range rights {
fmt.Printf("%s == %s : %v\n", left, right, (left == strings.ToLower(right)))
}
}
}
とやっても同じ結果になる。
strings
.EqualFold()
関数と strings
.ToLower()
関数でどっちが速いかなんてのは考えるまでもないのだが,いちおう試しておこう。
こんな感じのコードでいいかな。
package equalfold
import (
"strings"
"testing"
)
var (
lefts = []string{"go", "go"}
rights = []string{"Go", "GO", "go", "Go", "GO", "go"}
rights2 = []string{"go", "go", "go", "go", "go", "go"}
)
func BenchmarkEqualCase(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, left := range lefts {
for _, right := range rights2 {
if left == right {
_ = left
} else {
_ = right
}
}
}
}
}
func BenchmarkEqualLower(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, left := range lefts {
for _, right := range rights {
if left == strings.ToLower(right) {
_ = left
} else {
_ = right
}
}
}
}
}
func BenchmarkEqualFold(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, left := range lefts {
for _, right := range rights {
if strings.EqualFold(left, right) {
_ = left
} else {
_ = right
}
}
}
}
}
BenchmarkEqualCase
は他の2つのコードとの比較用に書いてみた。
実行結果はこんな感じ。
$ go test -bench Equal -benchmem
goos: linux
goarch: amd64
pkg: sample
BenchmarkEqualCase-4 32061360 36.2 ns/op 0 B/op 0 allocs/op
BenchmarkEqualLower-4 1367802 869 ns/op 64 B/op 8 allocs/op
BenchmarkEqualFold-4 3149362 378 ns/op 0 B/op 0 allocs/op
PASS
ok sample 4.748s
表にまとめておこう。
関数名 | 実行時間 | Alloc サイズ | Alloc 回数 |
---|---|---|---|
BenchmarkEqualCase |
36.2 ns | 0 bytes | 0 |
BenchmarkEqualLower |
869 ns | 64 bytes | 8 |
BenchmarkEqualFold |
378 ns | 0 bytes | 0 |
BenchmarkEqualCase
と BenchmarkEqualFold
の比較では BenchmarkEqualFold
のほうが10倍の時間がかかっているが,それよりも BenchmarkEqualLower
の処理のほうが圧倒的に遅いことが分かる。
まぁメモリ・アロケーションが絡むとねぇ。
というわけで,大文字小文字を無視した文字列比較では素直に strings
.EqualFold()
関数を使いましょう,という話でした。
【付録】 “NUL” 文字の比較
まるきし余談ではあるが
この記事にある isDevNull3
関数について
func isDevNull3(name string) bool {
return strings.ToLower(name) == "nul"
}
strings
.EqualFold()
関数を使うよう書き換えてみる。
func isDevNull3(name string) bool {
return strings.EqualFold(name, "nul")
}
これでベンチマークテストを実行すると
goos: linux
goarch: amd64
pkg: sample/lowercase
BenchmarkS1-4 41640913 27.2 ns/op 0 B/op 0 allocs/op
BenchmarkS2-4 35464141 30.7 ns/op 0 B/op 0 allocs/op
BenchmarkS3-4 12628962 94.4 ns/op 0 B/op 0 allocs/op
PASS
ok sample/lowercase 3.578s
メモリ・アロケーションが発生しなくなり,かなり速くなる。 まぁ,それでもいっちゃん遅いのだが(笑)
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。