Goの文字列はバイトの並びで、Goのソースと文字列リテラルはUTF-8です。byte はそのバイトの1つです。rune はUnicodeの1文字(コードポイント)で、UTF-8はそれを1から4バイトで保存します。Goの文字列に関する混乱のほとんどは、この2つを混同することから生まれます。
語 はUTF-8で3バイトなので、"語Go" は5バイトですが3ルーンです。インデックスのループは5つのバイトを見ますが、バイト0、1、2は単独では意味を持ちません。range ループはUTF-8をデコードし、インデックス0、3、4で3つのルーンを返します。i は常に文字数ではなく バイト オフセットです。
byteとruneは別名
| 名前 | 同じ型 | リテラル | 意味 |
|---|---|---|---|
byte | uint8 | byte('A')、0x41 | データの1バイト |
rune | int32 | 'A'、'é'、'世' | Unicodeのコードポイント1つ |
別名なので、byte は uint8 であり、rune は int32 です。別名と基底型の間に変換は要りません。シングルクォートはルーンを、ダブルクォートは文字列を作ります。ルーンリテラルはちょうど1文字を保持するので、'ab' はコンパイルエラーです。
ルーンは数値なので、算術演算ができます。
'7' - '0' は、数字の文字をその値に変える昔ながらの方法です。unicode パッケージはルーンを分類し(IsLetter、IsDigit、IsSpace、IsUpper)、ASCII以外の文字でも大文字小文字を正しく変換します。%c はルーンを文字として、%U は U+4E16 の形で、%q はクォート付きのルーンリテラルとして表示します。
UTF-8が文字を保存する仕組み
| コードポイント | バイト数 | 例 |
|---|---|---|
| U+0000からU+007F | 1 | ASCII:a、7、{ |
| U+0080からU+07FF | 2 | é、ß、ж、ع |
| U+0800からU+FFFF | 3 | 語、€、한 |
| U+10000以上 | 4 | 絵文字、まれなCJK文字 |
ASCIIのテキストはUTF-8でもまったく同じなので、バイト単位のコードはテストではうまく動き、アクセント付きの名前や日本語が来た途端に壊れがちです。unicode/utf8 パッケージはエンコーディングを直接扱います。
% x(空白付き)はバイトを空白区切りの16進数で表示します。utf8.ValidString は文字列が正しいUTF-8かどうかを報告します。Goの文字列は正しいUTF-8に限らずどんなバイトでも保持できます。range は不正なバイトに出会うと utf8.RuneError(U+FFFD、置換文字)を返し、1バイト進みます。
string、[]byte、[]runeの相互変換
| 変換 | 結果 | コスト |
|---|---|---|
[]byte(s) | UTF-8のバイト | コピーする |
[]rune(s) | デコードしたコードポイント | デコードしてコピーする(1ルーン4バイト) |
string(b) | バイトから作った文字列 | コピーする |
r が []rune のときの string(r) | UTF-8にエンコードする | コピーする |
r が rune のときの string(r) | 1文字の文字列 | 小さい |
バイト単位で反転すると 世界 が不正なUTF-8にばらばらになるので、reverse はルーンに対して処理しています。ただしルーンでもまだ完全ではありません。é のような文字は e と結合用のアクセント記号の2ルーンで書くこともできます。ユーザーから見た文字(書記素クラスタ)を扱うには、github.com/rivo/uniseg などのライブラリを使います。
文字列は変更できずスライスは変更できるので、どの変換もコピーします。ホットなコードでは行ったり来たりの変換を避けてください。コンパイラは一部のケース(string(b) をマップのキーや比較に使う場合など)でコピーを省くよう最適化します。
インデックスはバイトを返す
s[i] は byte を返し、s[i:j] はバイトオフセットでスライスします。
s := "café"
fmt.Println(s[3]) // 195: the first byte of é
fmt.Println(string(s[3])) // "Ã": that byte read as a code point
fmt.Println(s[:3]) // "caf"
fmt.Println(s[:4]) // "caf\xc3": half of é, invalid UTF-8
string(s[3]) は罠です。1バイトを string に変換すると、そのバイトそのものではなくコードポイント195、つまり Ã として扱われます。文字の境界を見つけるには、range、strings.Index の仲間(常に境界上のバイトオフセットを返します)、または utf8.DecodeRuneInString を使います。
bytesパッケージ
bytes は []byte 向けに strings と同じ機能を持つパッケージです:bytes.Contains、bytes.Split、bytes.Fields、bytes.TrimSpace、bytes.Equal、bytes.ToUpper などがあります。データがバイトとして届く場合(ファイルの内容、HTTPのボディ、ネットワークのバッファ)に使えば、文字列に変換して戻す手間が省けます。
bytes.Buffer は伸長できるバイトバッファで、io.Reader と io.Writer を実装しています。バイトの書き込みと読み取りの両方を行うときに適した道具です。文字列を組み立てるだけなら strings.Builder のほうが少し軽量です。スライスには == が使えないので、バイトスライスは bytes.Equal で比較します。
使い分け
- 表示、比較、検索するテキスト:
stringのままにし、1文字ずつたどるときはrangeを使う。 - 文字の位置、反転、n文字への切り詰め:
[]runeに変換する。 - I/O、ハッシュ、エンコード、バイナリプロトコル:
[]byteとbytesパッケージを使う。 - 1文字なら
rune、生の1バイトならbyte。
よくある質問
Goのruneとは何ですか?
rune は int32 の別名で、Unicodeのコードポイント1つを表します。ルーンリテラルはシングルクォートで書きます:'a' は97、'é' は233、'世' は19990です。for i, r := range s で文字列を走査すると、文字列のUTF-8のバイトからデコードされたルーンが得られます。
Goのruneとbyteの違いは何ですか?
byte(uint8 の別名)は8ビットの生データです。rune(int32 の別名)はUnicodeの1文字全体です。UTF-8の文字列では、ASCIIの文字は1文字1バイトですが、それ以外の文字は2から4バイトを使い、それでも1ルーンです。len(s) はバイト数を、utf8.RuneCountInString(s) はルーン数を数えます。
Goで文字列を1文字ずつループするには?
for i, r := range s を使います。各反復でバイトオフセット i とルーン r が得られるので、マルチバイト文字も正しく扱えます。単純なインデックスのループ for i := 0; i < len(s); i++ はバイトを走査するので、ASCII以外の文字がばらばらに分かれてしまいます。
Goで文字列をバイトスライスに変換するには?
b := []byte(s) は文字列のバイトを新しいスライスにコピーします。string(b) で元に戻せますが、これもコピーです。文字列を反転したり最初のn文字を取り出したりと、文字単位で扱う必要があるときは、代わりに []rune(s) を使います。