マルチバイトな文字列を扱うときに気をつけたい話

はじめに

現在開発中のプロジェクトではORMにgormを使っている。

1日1回依存しているパッケージのバージョンを更新してPRし、CIして変更差分に問題なければMergeして可能な限りコードを新しく保てるように開発してる。

そんな矢先、gormにMergeされたPRによって日本語の文字列が入ったテストケースがこけるようになってしまった。

チームのSlackで騒いでいると「あー multi byteな文字列が考慮されていないね」と教えていただいた。

後に修正 PRされるのだが、自分でも検証してみることにした。

該当のコード

該当のコードは以下の部分

	buff := gobytes.NewBuffer([]byte{})
	i := 0
	for pos := range str {
		if str[pos] == '?' {
			buff.WriteString(replacements[i])
			i++
		} else {
			buff.WriteByte(str[pos])
		}
	}

	str = buff.String()

一部変更して実行してみると

	str1 := "hello ? world"
	buff := bytes.NewBuffer([]byte{})
	i := 0
	for pos := range str1 {
		log.Println(pos)
		if str1[pos] == '?' {
			log.Println(i)
			i++
		} else {
			buff.WriteByte(str1[pos])
		}
	}
	log.Println(buff.String())

	log.Println("------------")

	str2 := "こんにちは ? 世界"
	buff2 := bytes.NewBuffer([]byte{})
	i = 0
	for pos := range str2 {
		log.Println(pos)
		if str2[pos] == '?' {
			log.Println(i)
			i++
		} else {
			buff2.WriteByte(str2[pos])
		}
	}

	log.Println(buff2.String())

以下のようにマルチバイトな文字列でない場合はstringのsliceが1バイトずつだが、 マルチバイト文字列は今回の場合だと3バイトずつになっているのが分かる

そのため buff2.WriteByte(str2[pos]) で文字列が正常に処理されず buff2.String() が正常に表示されない。

 $ go run main.go
2018/02/14 00:02:35 0
2018/02/14 00:02:35 1
2018/02/14 00:02:35 2
2018/02/14 00:02:35 3
2018/02/14 00:02:35 4
2018/02/14 00:02:35 5
2018/02/14 00:02:35 6
2018/02/14 00:02:35 0
2018/02/14 00:02:35 7
2018/02/14 00:02:35 8
2018/02/14 00:02:35 9
2018/02/14 00:02:35 10
2018/02/14 00:02:35 11
2018/02/14 00:02:35 12
2018/02/14 00:02:35 hello  world
2018/02/14 00:02:35 ------------
2018/02/14 00:02:35 0
2018/02/14 00:02:35 3
2018/02/14 00:02:35 6
2018/02/14 00:02:35 9
2018/02/14 00:02:35 12
2018/02/14 00:02:35 15
2018/02/14 00:02:35 16
2018/02/14 00:02:35 0
2018/02/14 00:02:35 17
2018/02/14 00:02:35 18
2018/02/14 00:02:35 21
2018/02/14 00:02:35

解決方法

修正 PRにもあるようにruneで処理するのが良い。

	str2 := "こんにちは ? 世界"
	buff2 := bytes.NewBuffer([]byte{})
	for i, s := range str2 {
		log.Println(i, s)
		if s == '?' {
			log.Println("? :", i)
			i++
		} else {
			buff2.WriteRune(s)
		}
	}

	log.Println(buff2.String())
}

正常に表示された

 $ go run main.go
2018/02/14 00:08:30 0 104
2018/02/14 00:08:30 1 101
2018/02/14 00:08:30 2 108
2018/02/14 00:08:30 3 108
2018/02/14 00:08:30 4 111
2018/02/14 00:08:30 5 32
2018/02/14 00:08:30 6 63
2018/02/14 00:08:30 ? : 6
2018/02/14 00:08:30 7 32
2018/02/14 00:08:30 8 119
2018/02/14 00:08:30 9 111
2018/02/14 00:08:30 10 114
2018/02/14 00:08:30 11 108
2018/02/14 00:08:30 12 100
2018/02/14 00:08:30 hello  world
2018/02/14 00:08:30 ------------
2018/02/14 00:08:30 0 12371
2018/02/14 00:08:30 3 12435
2018/02/14 00:08:30 6 12395
2018/02/14 00:08:30 9 12385
2018/02/14 00:08:30 12 12399
2018/02/14 00:08:30 15 32
2018/02/14 00:08:30 16 63
2018/02/14 00:08:30 ? : 16
2018/02/14 00:08:30 17 32
2018/02/14 00:08:30 18 19990
2018/02/14 00:08:30 21 30028
2018/02/14 00:08:30 こんにちは  世界

まとめ

マルチバイトな文字列を扱う可能性がある場合に、stringを1文字ずつ処理するような場合は配列のpositionではなくruneで扱うようにする。

refs: https://play.golang.org/p/YTQh2YrLjuO