読者です 読者をやめる 読者になる 読者になる

golang で urf8 でない文字列を for range で回すとどうなるか

概要

golang の string は意識しなければたいていの場合 utf8 な文字列なわけですが, ファイルからテキスト読んできたりすると,非 utf8 な文字列が混じることとかあります. そんな 非 utf8 な文字列を for range で回した場合の挙動でハマったのでメモ.

端的に言うとRob Pike 氏のこれをよく読もう:

Strings, bytes, runes and characters in Go - The Go Blog

文字ごとに処理する

golang で string の長さをとると,それは byte 長です.たとえば len("日本語") の値は 9 になります. いわゆる文字()のことを golang では rune と定義していますが,文字ごとに処理したい場合には, for range で文字列を回してやればうまくいきます.(以下,文字とは rune の事と読み替えて下さい)

const s = "日本語"
for i, r := range s {
    fmt.Printf("%#U starts at byte position %d\n", r, i)
}

の結果は,

U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6

のようになります.文字の開始位置と文字が取れる仕組みです.

文字列が utf8 として正しくない場合はどうなるか?

golang のプログラムは utf8 で書くことになっているので,普段意識せずに string を使っていれば, string は utf8 のバイト列で表現されています.しかし,string は単に変更できない byte 列のようなものなので, utf8 として正しくないものも設定することが出来ます.また,ファイルから文字列を読み込んでくる場合など, 日本語環境では sjis などの文字列を読み込んで文字列に設定してしまうかも知れません.

そのようなとき,上の for range によるループでは返値はどうなるでしょうか?

const s = "\x93\xFA\x96\x7B\x8C\xEA" // "日本語" の sjis 表現
for i, r := range s {
    fmt.Printf("%#U starts at byte position %d\n", r, i)
}

答えはこうなります.

U+FFFD '�' starts at byte position 0
U+FFFD '�' starts at byte position 1
U+FFFD '�' starts at byte position 2
U+007B '{' starts at byte position 3               // ← ここだけたまたま文字として取れてる
U+FFFD '�' starts at byte position 4
U+FFFD '�' starts at byte position 5

for range の処理は,文字として取れなければ,1 byte ごと進める. そして,文字として取れないときは,RuneError という「文字」を返す,という動作になります. この RuneError は コードポイントとしては U+FFFD で表されて,utf8 の表現では, 0xEF 0xBF 0xBD という byte 列になり, 長さは 3 byte です.

なので,文字が返ってきたと思って,この文字の utf8 の長さ基準に処理を進めると,for は 1 byte しか進まないのに,文字の長さは 3 byte あって おかしな事になります.# そんな処理を書いていて,バグを生みました orz

これは,utf8.DecodeRune(String) などの文字列から文字をとる関数でも同様です.

実は,Rob Pike 氏の詳しい解説 Strings, bytes, runes and characters in Go にもこのことが触れられているのですが,

f:id:ikawaha:20170519144942p:plain

練習問題になっているのでした.答えを知りたかったです,先生.