The Go Blog
Goにおける文字列、バイト、ルーン、および文字
はじめに
前回のブログ記事では、Goにおけるスライスの仕組みについて、その実装の背後にあるメカニズムを説明するために多くの例を用いて解説しました。この背景に基づいて、この記事ではGoにおける文字列について議論します。一見すると、文字列はブログ記事にするにはあまりにも単純な話題に見えるかもしれませんが、それらをうまく使うためには、どのように機能するかだけでなく、バイト、文字、ルーンの違い、UnicodeとUTF-8の違い、文字列と文字列リテラルの違い、さらにはより微妙な区別を理解する必要があります。
このトピックにアプローチする一つの方法は、「Goの文字列を位置 *n* でインデックス付けするとき、なぜ *n* 番目の文字が得られないのですか?」というよくある質問への答えとして考えることです。ご覧の通り、この質問は現代世界でテキストがどのように機能するかについての多くの詳細へと私たちを導きます。
これらの問題のいくつかについて、Goとは独立した優れた入門書として、Joel Spolskyの有名なブログ記事「すべてのソフトウェア開発者がUnicodeと文字セットについて絶対、確実に知っておくべき最低限のこと(言い訳無用!)」があります。彼が提起する多くの点は、ここで繰り返されるでしょう。
文字列とは何か?
まず基本から始めましょう。
Goでは、文字列は事実上、バイトの読み取り専用スライスです。バイトスライスが何であるか、またそれがどのように機能するかについて少しでも不明な点がある場合は、前回のブログ記事をお読みください。ここでは、それを読んでいることを前提とします。
文字列は *任意の* バイトを保持するということを、最初にはっきりと述べておくことが重要です。Unicodeテキスト、UTF-8テキスト、またはその他の定義済みの形式を保持する必要はありません。文字列の内容に関する限り、それはバイトのスライスとまったく同じです。
以下は、\xNN表記法を使用して、いくつかの特殊なバイト値を持つ文字列定数を定義する文字列リテラルです(これについては後ほど詳しく説明します)。 (もちろん、バイトは16進値00からFFまで(両端を含む)の範囲です。)
const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"
文字列の表示
サンプル文字列の一部のバイトは有効なASCIIでも、有効なUTF-8でもないため、文字列を直接印刷すると醜い出力が生成されます。単純なprint文は
fmt.Println(sample)
このような混乱を生み出します(正確な表示は環境によって異なります)。
��=� ⌘
その文字列が実際に何を持っているかを知るには、それを分解して部品を調べる必要があります。これを行うにはいくつかの方法があります。最も明白なのは、次のforループのように、その内容をループしてバイトを個別に抽出することです。
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進数) フォーマット動詞を使用することです。これは、文字列のシーケンシャルバイトを1バイトあたり2桁の16進数でダンプするだけです。
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の等号が1つ、通常のスペースとともに入っており、最後にはよく知られたスウェーデンの「名所」記号が現れます。この記号はUnicode値U+2318を持ち、スペース(16進値`20`)の後ろのバイト`e2` `8c` `98`によってUTF-8でエンコードされています。
文字列内の奇妙な値に馴染みがなかったり、混乱したりする場合は、%q 動詞に「プラス」フラグを使用できます。このフラグにより、出力は非表示シーケンスだけでなく、非ASCIIバイトもすべてエスケープされ、同時にUTF-8として解釈されます。その結果、文字列内の非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と文字列リテラル
これまでに見てきたように、文字列をインデックス付けすると、文字ではなくバイトが生成されます。文字列は単なるバイトの集まりなのです。つまり、文字列に文字値を格納するときは、そのバイトごとの表現を格納するということです。それがどのように起こるかを見るために、より管理された例を見てみましょう。
以下は、単一の文字を含む文字列定数を3つの異なる方法で表示する簡単なプログラムです。1つはプレーンな文字列として、もう1つは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、すなわち「名所」記号⌘が、バイトe2 8c 98によって表現され、これらのバイトが16進値2318の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標準では、「コードポイント」という用語を、単一の値で表される項目を指すために使用します。16進値2318のコードポイントU+2318は、記号⌘を表します(このコードポイントに関するさらに詳しい情報については、そのUnicodeページを参照してください)。
より実用的な例を挙げると、UnicodeコードポイントU+0061は小文字のラテン文字「A」です。
しかし、小文字のグレイブアクセント付き文字「A」、つまり à はどうでしょうか?これは文字であり、コードポイント(U+00E0)でもありますが、他の表現もあります。例えば、「結合文字」グレイブアクセントコードポイントU+0300を小文字のa(U+0061)に付加して、同じ文字 à を作成することができます。一般的に、1つの文字は複数の異なるコードポイントのシーケンス、したがって複数の異なるUTF-8バイトのシーケンスで表現されることがあります。
したがって、コンピュータにおける文字の概念は曖昧であり、少なくとも混乱を招くため、慎重に扱う必要があります。信頼性を高めるために、特定の文字が常に同じコードポイントで表されることを保証する*正規化*手法がありますが、その主題は今のところ本題から外れすぎます。Goライブラリが正規化にどのように対応しているかについては、後のブログ記事で説明します。
「コードポイント」は少し言い回しが長いので、Goではこの概念に短い用語を導入しています。それは *rune* です。この用語はライブラリやソースコードに登場し、「コードポイント」とまったく同じ意味を持ちますが、興味深い追加点があります。
Go言語では、runeという単語を型int32のエイリアスとして定義しています。これにより、整数値がコードポイントを表す場合、プログラムは明確になります。さらに、皆さんが文字定数と考えるかもしれないものは、Goではrune定数と呼ばれます。この式の型と値は
'⌘'
整数値0x2318を持つruneです。
まとめると、主なポイントは次のとおりです。
- Goのソースコードは常にUTF-8です。
- 文字列は任意のバイトを保持します。
- 文字列リテラルは、バイトレベルのエスケープがない限り、常に有効なUTF-8シーケンスを保持します。
- これらのシーケンスは、ルーンと呼ばれるUnicodeコードポイントを表します。
- Goでは、文字列内の文字が正規化されているという保証はありません。
レンジループ
GoのソースコードがUTF-8であるという公理的な詳細を除けば、GoがUTF-8を特別に扱う方法はたった一つしかありません。それは文字列でfor rangeループを使用する場合です。
通常のforループで何が起こるかはすでに見てきました。対照的に、for rangeループは、各イテレーションでUTF-8でエンコードされたルーンを1つデコードします。ループの各周回で、ループのインデックスは現在のルーンの開始位置(バイト単位で測定)であり、コードポイントはその値です。以下は、コードポイントのUnicode値とその表示形式を示す、もう一つの便利なPrintfフォーマット%#Uを使用した例です。
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の例と同等のプログラムですが、そのパッケージの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/utf8パッケージのドキュメントを見て、他にどのような機能を提供しているか確認してください。
まとめ
冒頭で提起された質問に答えるために:文字列はバイトから構築されているため、インデックス付けすると文字ではなくバイトが得られます。文字列は文字さえ保持しないかもしれません。実際、「文字」の定義は曖昧であり、文字列が文字で構成されていると定義することでその曖昧さを解消しようとするのは誤りでしょう。
Unicode、UTF-8、および多言語テキスト処理の世界については、さらに多くのことを語るべきですが、それは別の投稿に譲りましょう。今のところ、Goの文字列がどのように動作するか、そしてそれらが任意のバイトを含む可能性があるにもかかわらず、UTF-8がその設計の中心的な部分であることについて、より深く理解していただけたことを願っています。
次の記事:Goの4年間
前の記事:配列、スライス(と文字列):'append'の仕組み
ブログインデックス