Goで文字列中に4バイト文字があるか探したい


MySQLの文字コードがutf8だと4バイト文字が登録できない。
文字コードをutf8mb4に変更すれば良いという話は置いておいて、
文字列中に4バイトの文字があるかをGoで判定したい。

今回はとにかく4バイト文字があったら困るだけなので
3バイトの絵文字はOKなゆるゆるルール。

## コード

```main.go
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	check("ほげほげ")
	check("hogehoge")
	check("こんにちは😀") // ニコちゃんは4バイトの絵文字
	check("グーパンチ✊") // グーパンは3バイトの絵文字
}

func check(str string) {
	flg := false
	for _, r := range str {
		if utf8.RuneLen(r) > 3 {
			flg = true
			break
		}
	}
	if flg {
		fmt.Println("4バイト文字がいる")
	} else {
		fmt.Println("4バイト文字がいない")
	}
}
```

## 実行結果
```
$ go run main.go 
4バイト文字がいない
4バイト文字がいない
4バイト文字がいる
4バイト文字がいない
```

## 速度を確かめてみる

golangは標準でBenchmark取れるので使ってみます！！
https://golang.org/pkg/testing/

### テスト対象のコードを用意
絵文字（4byte文字）があるかをtrue/falseで返すだけの処理を作成。

テストのパターンは以下

- 一文字ずつループしてバイト数をチェック
- 正規表現で😀にだけマッチするかチェック（簡単な正規表現）
- 正規表現で😀から🙏にマッチするかチェック（ちょっと長い正規表現）

😀のUnicodeは`U+1F600`、🙏のUnicodeは`U+1F64F`だそうな。
http://0g0.org/category/1F600-1F64F/1/


```sample.go
package sample

import (
	"regexp"
	"unicode/utf8"
)

// Rune 一文字ずつループしてバイト数をチェック
func Rune(str string) bool {
	for _, r := range str {
		if utf8.RuneLen(r) > 3 {
			return true
		}
	}
	return false
}

// Regexp1 正規表現で😀にだけマッチするかチェック（簡単な正規表現）
func Regexp1(str string) bool {
	reg := regexp.MustCompile(`\x{1F600}`)
	return reg.MatchString(str)
}

// Regexp2 正規表現で😀から🙏にマッチするかチェック（ちょっと長い正規表現）
func Regexp2(str string) bool {
	reg := regexp.MustCompile(`[\x{1F600}-\x{1F64F}]`)
	return reg.MatchString(str)
}
```

### テストコードを用意

テスト用のテキストは[Wikipedia](https://ja.wikipedia.org/wiki/Go_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E))さんからGo言語の説明文持ってきて一番最後でニッコリする😀
文字数は420文字くらいです。

```sample_test.go
package sample

import (
	"testing"
)

var (
	str = `Goはプログラミング言語のひとつ。Googleによって開発されており[5]、設計にロブ・パイク、ケン・トンプソンらが関わっている。
主な特徴として、軽量スレッディングのための機能、Pythonのような動的型付け言語のようなプログラミングの容易性、などがある。Go処理系としてはコンパイラのみが開発されている。マスコット・キャラクターはGopher（ホリネズミ）。
発表当初はLinuxとMac OS Xのみしかサポートしていなかったが[6]、2012年3月にリリースされたバージョン1.0からはWindowsもサポートされている[7]。2014年12月にリリースされたバージョン1.4からAndroidをサポートし[8]、2015年8月19日にリリースされたバージョン1.5からiOSをサポートしている[9]。また、2011年5月10日に公開された Google App Engine 1.5.0 でも、Go言語がサポートされている[1]😀`
)

func BenchmarkRune(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Rune(str)
	}
}

func BenchmarkRegexp1(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Regexp1(str)
	}
}

func BenchmarkRegexp2(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Regexp2(str)
	}
}
```

### 実行結果
Goのバージョンは1.9.3です。

一文字ずつ回しているのが一番は実行速度が早いですねー

```
$ go version
go version go1.9.3 darwin/amd64

$ go test -bench . -benchmem 
goos: darwin
goarch: amd64
pkg: github.com/masakurapa/sample/sample
BenchmarkRune-4      	  500000	      2770 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp1-4   	  200000	      6509 ns/op	   40304 B/op	      22 allocs/op
BenchmarkRegexp2-4   	  100000	     22016 ns/op	   40176 B/op	      19 allocs/op
PASS
ok  	github.com/masakurapa/sample/sample	5.235s
```

## テスト その２
ただ先程作ったテストだと毎回`regexp.MustCompile()`が走っています。
そりゃ遅いだろって思うのでテストコードをちょっと直してみる。

予めCompileしておく版

```sample.go
package sample

import (
	"regexp"
	"unicode/utf8"
)

var (
	reg1 = regexp.MustCompile(`\x{1F600}`)
	reg2 = regexp.MustCompile(`[\x{1F600}-\x{1F64F}]`)
)

// Rune 一文字ずつ探す
func Rune(str string) bool {
	for _, r := range str {
		if utf8.RuneLen(r) > 3 {
			return true
		}
	}
	return false
}

// Regexp1 正規表現で探す（スマイル一文字だけ）
func Regexp1(str string) bool {
	return reg1.MatchString(str)
}

// Regexp2 正規表現で探す（複数の絵文字にマッチ）
func Regexp2(str string) bool {
	return reg2.MatchString(str)
}
```

### 実行結果
単純に😀にマッチする処理は早くなっていますが
😀〜🙏にマッチする方はやはり微妙。

実際に使うときは😀だけ探すなんてことはないし
もっとたくさんの絵文字を探すとなると、やはり正規表現はより複雑になってしまうので
一文字ずつ見るのがいいと思います。

```
$ go test -bench . -benchmem 
goos: darwin
goarch: amd64
pkg: github.com/masakurapa/sample/sample
BenchmarkRune-4      	  500000	      2853 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp1-4   	10000000	       152 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp2-4   	  100000	     16971 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/masakurapa/sample/sample	5.017s
```

## その他のパターン
テスト用の文字列を適当に直して数字見てみただけ
結果載せるだけ

### 一文字目に4バイト文字（絵文字）
```
$ go test -bench . -benchmem 
goos: darwin
goarch: amd64
pkg: github.com/masakurapa/sample/sample
BenchmarkRune-4      	100000000	        10.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp1-4   	10000000	       117 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp2-4   	10000000	       113 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/masakurapa/sample/sample	3.585s
```

### 真ん中らへんに4バイト文字（絵文字）
```
$ go test -bench . -benchmem 
goos: darwin
goarch: amd64
pkg: github.com/masakurapa/sample/sample
BenchmarkRune-4      	 1000000	      1277 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp1-4   	10000000	       139 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp2-4   	  200000	      7196 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/masakurapa/sample/sample	4.354s
```

### 4バイト文字（絵文字）がない
```
$ go test -bench . -benchmem 
goos: darwin
goarch: amd64
pkg: github.com/masakurapa/sample/sample
BenchmarkRune-4      	  500000	      3007 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp1-4   	20000000	       109 ns/op	       0 B/op	       0 allocs/op
BenchmarkRegexp2-4   	  100000	     16074 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/masakurapa/sample/sample	5.619s
```