Goブログ記事
https://blog.golang.org/strings
の翻訳です。
原文はCreative Commons Attribution 3.0 ライセンスの下、Google社によりホストされています。
翻訳の誤りなどあればご指摘お待ちしております。
###はじめに
以前のブログの記事は、その実装のメカニズムを説明するためにいくつかの例を使用して、Go言語のスライスがどのように機能するかを説明しました。その背景を踏まえ、この記事は、Go言語の文字列について説明します。まず、文字列はブログの記事としてあまりにも単純なトピックに見えるかもしれませんが、それらを使いこなすことはそれらがどのように動作するかだけでなく、バイト、文字、及びルーンの違い、UnicodeとUTF-8との違い、文字列と文字列リテラルの違い、および他のより一層の微妙な違いについて、理解しておく必要があります。
このトピックにアプローチする一つの方法は、よく寄せられる質問への答えとしてそれを考えることです、「どのようなときにGo言語文字列に位置nのインデックスをつけられますか、なぜn番目の文字を取得できないのですか?」これから見ていくように、この質問は、現代の世界でテキストがどのように機能するかについての多くの詳細に私たちを導きます。
これらの問題の優れた紹介は、Go言語とは無関係に、ジョエル·スポルスキーの有名なブログの記事、絶対最低限すべてのソフトウェア開発者は絶対、積極的に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進数)形式の動詞を使用することです。それは単に文字列の連続バイトを、16進数としてバイトごとに2文字ダンプします。
fmt.Printf("%x\n", sample)
上記の出力と比較してみてください:
bdb23dbc20e28c98
素晴らしいトリックは書式に「スペース」フラグを使用することです、 % と x の間にスペースを入ます。ここで使用する書式文字列を上記のものと比較してみてください、
fmt.Printf("% x\n", sample)
バイトの間にスペースを出力し、少し負担の少ない結果を得る方法について理解してください:
bd b2 3d bc 20 e2 8c 98
さらにあります。 %q (quoted、引用符で囲まれた)動詞は、出力が明確になるように、文字列内のすべての印字不可能なバイトシーケンスをエスケープします。
fmt.Printf("%q\n", sample)
このテクニックは、文字列の大部分がテキストとしてわかるが、一部治すべき特殊文字がある場合には便利です。以下の結果が得られます:
"\xbd\xb2=\xbc ⌘"
目を細めてみると、ノイズに埋もれている一つの ASCIIの等号、通常のスペースと共に、最後によく知られているスウェーデン語の「Place of Interest」を示す記号を見ることができます。 :その記号はUnicode値U+2318を有し、UTF-8として、スペース(16進数の値20)の後のバイト e2 8c 98 でエンコードされています。
私たちは、文字列の変な値について、不慣れか、混乱している場合、 %q 動詞に「プラス」フラグを使用することができます。このフラグは、UTF-8を解釈する間、印字不可能な列だけでなく非ASCIIバイトについても、エスケープして出力します。結果、適切な形式のUTF-8文字列内の非ASCIIデータを表す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と文字列リテラル
見てきたように、文字列のインデクシングでは、文字ではなく、バイトが得られます。文字列は単なるバイトの束です。これは、文字列内に文字の値を格納するとき、バイト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表現が作られる方法について説明する価値はあります。純然たる事実は以下の通りです:それはソースコードが書かれたときに作られた。
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標準は、単一の値で表される項目を参照するために用語「コードポイント」を使用しています。コードポイントU+2318は、16進数値2318で、記号⌘を表します。 (そのコードポイントに関する多くの詳細については、Unicodeのページを参照してください。)
もっと平凡な例を選ぶと、UnicodeコードポイントU+0061はラテン文字'A'の小文字'a'です。
しかし、重いアクセント文字'A'の小文字'à'についてはどうでしょう?それは文字であり、また、コードポイント(U+00E0)ですが、それは他の表現を持っています。例えば、「組み合わせ」アクセントコードポイント、U+0300を使用することができ、àと同じ文字を作成するには、小文字a、U+0061に接続します。一般的に、一つの文字は、複数の異なるコードポイントの列、したがって異なるUTF-8バイト列により、表現される場合があります。
コンピューティングの文字の概念が曖昧なので、または少なくとも紛らわしいので、注意して使用します。物事が信頼できるようにするには、そこに指定された文字は常に同じコードポイントで表されることを保証する正規化技術ですが、その対象は、現在のトピックから離れます。後のブログの記事で、Go言語ライブラリの正規化対処方法を説明します。
「コードポイント」は長くてちょっと発音しにくいため、Go言語はこの概念のためにより短い用語を導入します、ルーンです。この用語は、ライブラリとソースコードに表示され、一つの興味深い点を加えて、「コードポイント」と全く同じことを意味します。
Go言語は、 int32 型の別名として単語 rune を定義します、整数値がコードポイントを表すことをプログラムが明確にすることができるように。また、文字定数とみなされるものは、Go言語ではルーン定数と呼ばれます。表現
'⌘'
の型と値は、 rune と整数値 0x2318 です。
要約すると、ここでの要点は次のとおりです。
- Go言語のソースコードは、常にUTF-8です。
- 文字列は任意のバイトを保持します。
- 文字列リテラルは、バイトレベルのエスケープが無ければ、常に有効なUTF-8シーケンスを保持しています。
- これらの配列は、ルーンと呼ばれるUnicodeコードポイント列を表します。
- 文字列内の文字が正規化されていることはGo言語では保証されません。
###Rangeループ
Go言語のソースコードがUTF-8であるという公理の詳細の他に、Go言語がUTF-8を特別に扱う唯一の方法がります、それは、文字列の for range ループを使用する場合です。
通常の for ループで何が起こるか見てきました。 for range ループは、対照的に、各反復で一つの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 ループがあなたの目的に十分でないとしても、必要な機能はライブラリ内のパッケージで提供されている可能性があります。
最も重要なパッケージは、UTF-8文字列を検証、分解、再構築するヘルパールーチンが含まれる、unicode/utf8です。ここに、上記の 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がその設計の中心であることについて、あなたがよりよい理解を得られたことを願っています。
ロブ·パイクより
##関連記事