Goブログ
Goにおける文字列、バイト、rune、文字
はじめに
前のブログ記事前のブログ記事では、いくつかの例を用いてGoのスライスの動作と実装メカニズムを説明しました。この記事では、その知識を基にGoの文字列について説明します。一見、文字列はブログ記事のテーマとしては簡単すぎるように思えるかもしれませんが、文字列を効果的に使用するには、その動作だけでなく、バイト、文字、runeの違い、UnicodeとUTF-8の違い、文字列と文字列リテラルの違い、その他さらに微妙な違いを理解する必要があります。
このトピックへのアプローチの1つは、よくある質問「Goの文字列を位置*n*でインデックス付けすると、なぜ*n*番目の文字が取得できないのか?」への回答として考えることです。ご覧のとおり、この質問は、現代のテキストの仕組みに関する多くの詳細へと導きます。
Goとは関係なく、これらの問題の優れた紹介として、Joel Spolskyの有名なブログ記事The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)があります。彼が提起する多くの点は、ここで繰り返されます。
文字列とは何か?
基本事項から始めましょう。
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進数)フォーマット動詞を使用することです。これは、文字列の連続するバイトを、バイトあたり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の等号記号と通常のスペースが埋め込まれており、最後に有名なスウェーデンの「名所」記号が表示されていることがわかります。その記号は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のみの引用符付き文字列として、1つは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です。
コードポイント、文字、rune
これまで、「バイト」と「文字」という単語の使用に非常に注意してきました。これは、文字列がバイトを保持するためであり、また「文字」という概念の定義が少し難しいという理由からです。Unicode標準では、「コードポイント」という用語を使用して、単一の値で表される項目を参照します。16進値2318のコードポイントU+2318は、記号⌘を表します。(そのコードポイントに関する詳細については、そのUnicodeページを参照してください。)
よりありふれた例として、UnicodeコードポイントU+0061は、小文字のラテン文字「A」です:a。
しかし、小文字のグレイブアクセント付きの文字「A」、àはどうでしょうか?これは文字であり、コードポイント(U+00E0)でもあります。しかし、他の表現もあります。たとえば、「結合」グレイブアクセントコードポイントU+0300を使用して、小文字のa(U+0061)に接続して、同じ文字àを作成できます。一般に、文字は多くの異なるコードポイントシーケンス、したがって異なるUTF-8バイトシーケンスによって表される可能性があります。
したがって、コンピューティングにおける文字の概念は曖昧であるか、少なくとも混乱を招くため、注意して使用します。信頼性を確保するために、特定の文字が常に同じコードポイントで表されることを保証する*正規化*テクニックがありますが、その主題は今のところ話題から逸れます。後のブログ投稿では、Goライブラリが正規化に対処する方法について説明します。
「コードポイント」は少し口語的なので、Goではこの概念を短くした用語*rune*を導入しています。この用語はライブラリとソースコードに表示され、「コードポイント」とまったく同じ意味を持ち、1つの興味深い追加機能があります。
Go言語では、`rune`という単語を`int32`型のエイリアスとして定義しているため、整数値がコードポイントを表す場合、プログラムを明確にすることができます。さらに、文字定数と考えるものを、Goでは*rune定数*と呼びます。式
'⌘'
の型と値は、整数値`0x2318`の`rune`です。
要約すると、重要な点は次のとおりです。
- Goのソースコードは常にUTF-8です。
- 文字列は任意のバイトを保持します。
- バイトレベルのエスケープがない文字列リテラルには、常に有効なUTF-8シーケンスが含まれます。
- これらのシーケンスは、runeと呼ばれるUnicodeコードポイントを表します。
- Goでは、文字列内の文字が正規化されているという保証はありません。
範囲ループ
GoのソースコードがUTF-8であるという公理的な詳細に加えて、GoがUTF-8を特別に扱う方法は実際には1つだけであり、それは文字列で`for` `range`ループを使用する場合です。
通常のfor
ループで何が起こるかを見てきました。対照的に、for range
ループは、各反復で1つのUTF-8エンコードされたruneをデコードします。ループのたびに、ループのインデックスは現在のruneの開始位置(バイト単位で測定)であり、コードポイントは値です。別の便利な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
の例と同等のプログラムですが、そのパッケージのDecodeRuneInString
関数を使用して作業を行います。関数の戻り値は、runeとその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の文字列の動作についてより良い理解を深めていただければ幸いです。Goの文字列は任意のバイトを含む可能性がありますが、UTF-8はその設計の中心的な部分です。
次の記事:Goの4年間
前の記事:配列、スライス(と文字列):'append'の仕組み
ブログインデックス