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

BenchmarkEqualCaseBenchmarkEqualFold の比較では 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

メモリ・アロケーションが発生しなくなり,かなり速くなる。 まぁ,それでもいっちゃん遅いのだが(笑)

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)