Menu

Go言語のruneとbyte:UTF-8文字列の仕組みを解説

Goのruneとbyteの意味、文字列がUTF-8をどう保持するか、lenがバイト数を数える理由、rangeがルーンをデコードする仕組み、string・[]byte・[]runeの相互変換、そしてbytesパッケージの使い方を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

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は別名

名前同じ型リテラル意味
byteuint8byte('A')0x41データの1バイト
runeint32'A''é''世'Unicodeのコードポイント1つ

別名なので、byteuint8 であり、runeint32 です。別名と基底型の間に変換は要りません。シングルクォートはルーンを、ダブルクォートは文字列を作ります。ルーンリテラルはちょうど1文字を保持するので、'ab' はコンパイルエラーです。

ルーンは数値なので、算術演算ができます。

'7' - '0' は、数字の文字をその値に変える昔ながらの方法です。unicode パッケージはルーンを分類し(IsLetterIsDigitIsSpaceIsUpper)、ASCII以外の文字でも大文字小文字を正しく変換します。%c はルーンを文字として、%UU+4E16 の形で、%q はクォート付きのルーンリテラルとして表示します。

UTF-8が文字を保存する仕組み

コードポイントバイト数
U+0000からU+007F1ASCII:a7{
U+0080からU+07FF2éßжع
U+0800からU+FFFF3
U+10000以上4絵文字、まれなCJK文字

ASCIIのテキストはUTF-8でもまったく同じなので、バイト単位のコードはテストではうまく動き、アクセント付きの名前や日本語が来た途端に壊れがちです。unicode/utf8 パッケージはエンコーディングを直接扱います。

% x(空白付き)はバイトを空白区切りの16進数で表示します。utf8.ValidString は文字列が正しいUTF-8かどうかを報告します。Goの文字列は正しいUTF-8に限らずどんなバイトでも保持できます。range は不正なバイトに出会うと utf8.RuneErrorU+FFFD、置換文字)を返し、1バイト進みます。

string、[]byte、[]runeの相互変換

変換結果コスト
[]byte(s)UTF-8のバイトコピーする
[]rune(s)デコードしたコードポイントデコードしてコピーする(1ルーン4バイト)
string(b)バイトから作った文字列コピーする
r[]rune のときの string(r)UTF-8にエンコードするコピーする
rrune のときの 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、つまり Ã として扱われます。文字の境界を見つけるには、rangestrings.Index の仲間(常に境界上のバイトオフセットを返します)、または utf8.DecodeRuneInString を使います。

bytesパッケージ

bytes[]byte 向けに strings と同じ機能を持つパッケージです:bytes.Containsbytes.Splitbytes.Fieldsbytes.TrimSpacebytes.Equalbytes.ToUpper などがあります。データがバイトとして届く場合(ファイルの内容、HTTPのボディ、ネットワークのバッファ)に使えば、文字列に変換して戻す手間が省けます。

bytes.Buffer は伸長できるバイトバッファで、io.Readerio.Writer を実装しています。バイトの書き込みと読み取りの両方を行うときに適した道具です。文字列を組み立てるだけなら strings.Builder のほうが少し軽量です。スライスには == が使えないので、バイトスライスは bytes.Equal で比較します。

使い分け

  • 表示、比較、検索するテキスト:string のままにし、1文字ずつたどるときは range を使う。
  • 文字の位置、反転、n文字への切り詰め:[]rune に変換する。
  • I/O、ハッシュ、エンコード、バイナリプロトコル:[]bytebytes パッケージを使う。
  • 1文字なら rune、生の1バイトなら byte

よくある質問

Goのruneとは何ですか?

runeint32 の別名で、Unicodeのコードポイント1つを表します。ルーンリテラルはシングルクォートで書きます:'a' は97、'é' は233、'世' は19990です。for i, r := range s で文字列を走査すると、文字列のUTF-8のバイトからデコードされたルーンが得られます。

Goのruneとbyteの違いは何ですか?

byteuint8 の別名)は8ビットの生データです。runeint32 の別名)は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) を使います。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める