Goにおける文字列、バイト、ルーンと文字 #
Strings, bytes, runes and characters in Go By Rob Pike
はじめに #
1つ前の記事 では、その実装の背後にある機能を解説する例とともに、Goにおいてスライスがどのように動作するかを説明しました。 その知識を前提として、この記事ではGoにおける文字列について話します。 まず最初に、文字列はブログの記事にしては簡単すぎるように見えるかもしれませんが、上手に使うには文字列の動作を理解するだけでなく、 バイト、文字、ルーンの違いについても理解し、UnicodeとUTF-8の違いについても理解し、文字列と文字列リテラルの違いについても理解し、 その他多くの細かな違いについて理解する必要があります。
この話題を議論するときの1つのアプローチとして、FAQである「Goの文字列のn番目のインデックスにアクセスした時に、 なぜn番目の文字を取得できないのか」という質問の回答を考えてみましょう。この記事で説明していきますが、この質問には 現代社会でテキストがどのように動作しているかを多くの観点から考えるきっかけとなります。
Goにかぎらず、これらの問題について考える最高の導入は、Joel Spolskyの有名なブログポスト、The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) です。 彼がその記事の中で挙げた点を、この記事でも繰り返し言及します。
stringとは何か #
基礎からはじめましょう。
Goでは、文字列は実際には読み取り専用のバイトのスライスでした。バイトのスライスがなにかについて不確かな場合、あるいはそれがどう動作するか 不確かな場合は、前のブログポスト を読んで下さい。この記事ではすでに前のブログポストを読んでいることを前提とします。
stringは 任意の バイトを保持できることをはっきりと述べておくことは重要です。 stringはUnicode文字もUTF-8文字も、その他の事前定義の形式の文字を持つ必要はありません。 stringの中身を考える限りにおいては、それはバイトのスライスについて考えることと同義です。
(すぐあとで説明しますが)あるバイト値を定数で持つように \xNN
という記法を使って文字列リテラルを定義しました。
(もちろん、16進数でのバイト値の範囲は両端含んで 00
から FF
です)
const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"
文字列を表示する #
サンプルの文字列内のいくつかのバイトは正しいASCII文字やUTF-8の値ではないので、直接表示するとおかしな出力になります。 単順に表示すると
fmt.Println(sample)
おかしな結果になります。(見た目は環境によって変わります。)
��=� ⌘
文字列が本当はどのような値を保持しているかを見たければ、文字列を分解して、個々に調べる必要があります。 それにはいくつかの方法があります。最も明示的な方法は、中身をループで回して、バイトを1つずつ取り出す方法です。
for i := 0; i < len(sample); i++ {
fmt.Printf("%x ", sample[i])
}
先に言ったように、文字列にインデックスでアクセスすると個々の文字ではなく個々のバイトにアクセスします。 その話についてはあとで触れるとして、いまはバイトについてだけ考えましょう。バイトごとのループの出力はこのようになります。
bd b2 3d bc 20 e2 8c 98
個々のバイトが、文字列を定義したエスケープ済み16進数と一致することに注目してください。
汚い文字列を表示できる形にするのにより短い書き方は fmt.Printf
の %x
(16進数)フォーマット書式です。
この書式では文字列の一連のバイトを16進数の1バイトあたり2つ数字としてダンプします。
fmt.Printf("%x\n", sample)
これの出力を先の表示と比較してみましょう。
bdb23dbc20e28c98
コツとしては、書式内で %
と x
の間に空白を置く「空白」フラグを使うことです。上の書式文字列と比較してみましょう。
fmt.Printf("% x\n", sample)
結果の出力にはバイトごとに間に空白が入り、より自然な形になったことに気がつくでしょう。
bd b2 3d bc 20 e2 8c 98
他にも方法があります。 %q
(引用)書式を使うと、文字列中でうまく表示ができないバイト列がある場合は、
出力がおかしくならないようにエスケープしてくれます。
fmt.Printf("%q\n", sample)
この方法は、文字列の大部分は読めるけれど、おかしな所を無くしたい時に便利です。先ほどの文字列では次のような出力になります。
"\xbd\xb2=\xbc ⌘"
この出力をよく見てみると、おかしな文字列の中にASCII文字の等号記号と半角スペースとよく知られたスウェーデン語の「Place of Interest(名所)」 記号があることがわかります。この記号はUnicode値ではU+2318で表され、UTF-8では16進数で28で表される半角スペースに続いて、 e2 8c 98で表されます。
文字列の中におかしな値があることでよくわからなくなってしまったり混乱してしまうようであれば、%q
書式の中で「プラス」記号を使うと良いでしょう。
このフラグはうまく表示できないバイト列をエスケープするだけでなく、UTF-8として解釈できる非ASCII文字のバイト列もエスケープします。
このフラグを使うと、非ASCII文字のUTF-8として解釈できるUnicode値を文字列内に表示します。
fmt.Printf("%+q\n", sample)
この書式では、先ほどのスウェーデン語の記号のUnicode値は \u
でエスケープされて表示されます。
"\xbd\xb2=\xbc \u2318"
これらのテクニックは文字列の中身をデバッグしたい時には知っておくと便利なものですし、これから先の議論においても便利なものになります。 これらの方法は文字列の場合と同様にバイト列に対してもまったく同様に使えることも知っておくと良いでしょう。
つぎに、これまでに挙げた表示のオプションを、実行できるプログラムの形ですべて挙げてみます。
package main
import "fmt"
func main() {
const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"
fmt.Println("Println:")
fmt.Println(sample)
fmt.Println("Byte loop:")
for i := 0; i < len(sample); i++ {
fmt.Printf("%x ", sample[i])
}
fmt.Printf("\n")
fmt.Println("Printf with %x:")
fmt.Printf("%x\n", sample)
fmt.Println("Printf with % x:")
fmt.Printf("% x\n", sample)
fmt.Println("Printf with %q:")
fmt.Printf("%q\n", sample)
fmt.Println("Printf with %+q:")
fmt.Printf("%+q\n", sample)
}
(演習:上の例を変更して、文字列の代わりにバイトスライスを使ってみましょう。 ヒント:スライスを作るにはキャストを使います。)
(演習:文字列内の個々のバイトに対して %q
書式を使ってみましょう。出力から何が分かるでしょうか。)
UTF-8と文字列リテラル #
これまで見てきたように、文字列をインデックスでアクセスするとその場所にある文字ではなくバイトが返されます。stringはバイト列にすぎないのです。 すなわち、string内に文字値を保存すると、そのバイト表現を保存します。何が起きているか、確認しながら見てみましょう。
次のサンプルは、1文字の文字列定数を3通りの方法で出力する簡単なプログラムです。それぞれ、単純な文字列、ASCII文字限定の引用文字列、 16進数での個々のバイト列です。混乱を避けるため、文字列定数がリテラル文字列のみを含むようにバッククォートで囲った「生文字列」を生成します。 (先に見たようにダブルクォートで囲った通常の文字列ではエスケープシーケンスを含む可能性があります。)
func main() {
const placeOfInterest = `⌘`
fmt.Printf("plain string: ")
fmt.Printf("%s", placeOfInterest)
fmt.Printf("\n")
fmt.Printf("quoted string: ")
fmt.Printf("%+q", placeOfInterest)
fmt.Printf("\n")
fmt.Printf("hex bytes: ")
for i := 0; i < len(placeOfInterest); i++ {
fmt.Printf("%x ", placeOfInterest[i])
}
fmt.Printf("\n")
}
出力は次のとおりです。
plain string: ⌘
quoted string: "\u2318"
hex bytes: e2 8c 98
これでわかることは、Unicode文字値 U+2318 (「Place of Interest(名所)」記号 ⌘)は、バイト列 e2 8c 98
で表現され、
これらのバイト列は16進数値 2318
のUTF-8エンコードであるとわかります。
これは、UTF-8に詳しい人にとっては、明らかで瑣末なことかもしれませんが、文字列のUTF-8表現がどのように生成されるのかを説明するか を考えるみることは有意義なことです。単純な事実としては、UTF-8の文字列がソースコードが書かれた時に生成されている、ということです。
GoのソースコードはUTF-8で書かれると定義されています。他のエンコードは許されていません。これは、ソースコード中に次の文字列を書いたときに、
`⌘`
そのプログラムを書くために使っているテキストエディターは記号 ⌘ のUTF-8エンコードをソーステキスト中に埋め込んでいる、 ということを示唆しています。16進数バイトを表示するとき、それはエディターがファイルに埋め込んだデータをただダンプしているにすぎません。
短く言えば、GoのソースコードはUTF-8で、 したがって、文字列リテラルのソースコードはUTF-8文字列なのです 。 生文字列ではありえませんが、その文字列リテラルにエスケープシーケンスがなければ、生成された文字列は まさに引用符の間にあるソースの文字列だけを保持します。 したがって、その定義とその生成過程から、生文字列は常にその正しいUTF-8表現を保持しています。 同様に、前の節で出てきたようなUTF-8の規則から外れたエスケープを持っていなければ、通常の文字列リテラルでも常に正しいUTF-8を保持しています。
Goの文字列は常にUTF-8だと思っている人もいますが、そうではありません。文字列リテラルだけがUTF-8なのです。 前の節でお見せしたように、文字列値には任意のバイトを含むことが出来ます。この節で説明したように、文字列リテラルはバイトレベルでのエスケープが ない限り、常にUTF-8の文字列を保持しています。
まとめると、文字列は任意のバイトを含むことが出来ますが、文字列リテラルから生成された場合は、そのバイト列は(ほぼ常に)UTF-8です。
コードポイント、文字、ルーン #
ここまで、「バイト」と「文字」というそれぞれの言葉の使い分けに繊細な注意を払ってきました。 その理由は、文字列がバイト列を保持できること、そして「文字」という概念はいささか定義が難しいことから来ています。 Unicode 標準は、1つの値で表現される項目を指す場合「コードポイント」という用語を使います。 コードポイント U+2318 、16進数値 2318 は記号 ⌘ を表します。 (このコードポイントについてもっと知りたい場合は、そのUnicodeページを見てみましょう。)
もっと平凡な例を出すと、Unicodeコードポイント U+0061 は、小文字のラテン文字 ‘A’、すなわち a です。
しかし、小文字のグレーブアクセント付き文字の ‘A’、つまり à はどう表現されるのでしょうか。これは文字で、コードポイント(U+00E0)もあります。 しかし、ほかの表現もあります。たとえば、グレーブアクセントのコードポイント U+0300 を小文字 a のコードポイント U+0061 の「連結」を使って、 同じ文字 à を生成できます。一般的に、文字はいくつもの異なるコードポイントシーケンスで表現しうるため、異なるUTF-8のバイト列で表現できます。
したがって、コンピュータにおける文字の概念はあいまい、あるいは少なくともややこしく、そのため注意して扱うのです。 安心して使えるように、ある文字が常に同じコードポイントで表現されるようにする正規化のテクニックがありますが、 この記事の本題からは大きく外れてしまいます。後のブログエントリでGoのライブラリが正規化に対処しているかを説明しましょう。
「コードポイント」はいささか呼びにくいので、Goではその概念を表すより短い用語である「ルーン (rune)」を導入しました。 この用語はライブラリやソースコードに出てきますが、「コードポイント」とまったく同義です。さらにGoにおいてはもう1つの意味があります。
Go言語では rune
は int32
のエイリアスとして定義しています。したがって、プログラムではある整数値がコードポイントを表しているかどうかを
明確に区別することができます。さらに、Goでは、文字定数と思われているものは、ルーン定数になっています。
次の表現の型と値は、
'⌘'
整数値 0x2318
のルーンです。
まとめると、次のような目立った特徴があります。
- Goのソースコードは常にUTF-8
- 文字列は任意のバイトを保持できる
- 文字列リテラルは、バイトレベルのエスケープがない場合、常に正しいUTF-8シーケンスを保持する
- これらのシーケンスは、ルーンと呼ばれるUnicodeコードポイントを表している
- Goでは、文字列内の文字が正規化されている保証はない
rangeループ #
GoのソースコードはUTF-8であるという、自明のような詳細に加えて、GoがUTF-8を本当に特別に扱っている唯一の点は、文字列を for range
ループ
するときです。
通常の for
ループで何が起きるかはすでに見ています。対照的に for range
ループでは、イテレーションごとに
1つのUTF-8にエンコードされたルーンをデコードします。ループが回るごとに、ループのインデックスはバイト換算したときの現在のルーンの開始位置となり、値はそのルーン値のコードポイントとなります。これまで紹介したものとはまた別の便利な Printf
書式 %#U
を使った例をお見せします。
この書式では、Unicode値のコードポイントとその表現を表示します。
const nihongo = "日本語"
for index, runeValue := range nihongo {
fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
}
これを出力すると、それぞれのコードポイントが複数のバイトから成っていることがわかります。
U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6
(演習:文字列内に不正なUTF-8バイト列を入れてみましょう。(方法はだって?)ループの繰り返しで何が起きるでしょうか。)
ライブラリ #
Goの標準ライブラリではUTF-8文字列を解釈するための強力なサポートを提供しています。
目的に対して for range
ループでは不十分な場合には、おそらく必要なものはライブラリ内のパッケージで提供されているでしょう。
そのようなパッケージの中で最重要なものが unicode/utf8 です。
この中にUTF-8文字列を検証、分解、再構築するためのヘルパー関数があります。次の例は、先の for range
ループの例と同じ処理をしますが、
unicode/utf-8
内の DecodeRuneInString
関数を使っています。この関数の戻り値はルーンとUTF-8エンコードされたバイト幅です。
const nihongo = "日本語"
for i, w := 0, 0; i < len(nihongo); i += w {
runeValue, width := utf8.DecodeRuneInString(nihongo[i:])
fmt.Printf("%#U starts at byte position %d\n", runeValue, i)
w = width
}
実行して先の例と同じ結果になることを確認してみましょう。 for range
ループと DecodeRuneInString
はまったく同じ繰り返し処理を
するように定義されています。
unicode/utf-8
パッケージのドキュメントを見て、他の関数も見てみましょう。
結論 #
はじめに投げかけた質問に答えましょう。文字列はバイトからなり、それゆえインデックスでアクセスするとその場所の文字ではなくバイトを返します。 文字列は文字以外のものも保持します。事実、「文字」の定義は曖昧で、文字列は文字からできていると定義することで、その曖昧さを解決しようとする ことは間違いでしょう。
Unicode、UTF-8、多言語文字列処理の世界に関してはまだまだ語ることがたくさんありますが、それは他の記事に任せましょう。 いまは、あなたがGoの文字列がどのように振る舞うかを理解し、任意のバイトを含むかもしれないとしても、UTF-8はGoの文字列の設計における中心 であることの理解がより深まったことを願っています。
By Rob Pike