[Go] rune の仕様と良くあるエラーケース

2024-10-31

概要

Tour of Go で紹介されているように、rune は Go の基本文法の一つとされています。

しかし、コーディングする上で string や byte と比較すると、rune を使う機会ははるかに少なくなり、その必要性や意識すべきポイントを忘れがちになります。

今回はこの rune について、本記事では、必要性と良くあるエラーケースを紹介し、rune に対する学び直しを目的として執筆しました。

rune とは

rune は主に Unicode の文字を扱うためのデータ型です。これは int32 のエイリアス型であるため、Unicode のコードポイントを表現できます。 Unicode の概念については以下の記事の内容が参考になるかと思います。
JavaScript における文字コードと「文字数」の数え方

rune が必要となるのは、UTF-8 エンコーディングされたバイト配列から成る string 型を、Unicode の文字として正しく扱うためです。

これをコードベースで確認します。

以下のコードは string のデータ長を確認しています。

package main

import (
	"fmt"
)

func main() {
	a := "こんにちは"
	fmt.Println(len(a)) // output:15(内訳 [こ:3 ん:3 に:3 ち:3 は:3])
}

この時 len() はバイト数を表しています。

次にこの文字列を rune として扱った場合を見てみます。

以下のコードは string のデータを rune のスライスにキャストし、そのスライスの長さを確認しています。

package main

import (
	"fmt"
)

func main() {
	a := "こんにちは"
	fmt.Println(len([]rune(a))) // output:5
}

各 rune の値が文字リテラルに対応するため、文字列リテラルの長さである 5 が出力されています。

rune のエラーケース

インデックスによる文字列の指定

for で文字列をループに回す際、ループの単位はコードポイントの単位になります。

これをコードで確認すると以下のようになります。

package main

import (
	"fmt"
)

func main() {
	a := "こんにちは"
	for i, r := range a {
		fmt.Printf("%d: %q\n", i, r)
	}
}

// output:
// 0: 'こ'
// 3: 'ん'
// 6: 'に'
// 9: 'ち'
// 12: 'は'

ループの回数は文字列リテラルの長さ(=5)になっていることが分かります。

注目して欲しいのは、対応する index はそのバイト数だけジャンプするという点です。

この index がジャンプすることを知らず、i が 0,1,2,3,4 と増加すると、予期せぬ結果になる可能性があります。

例としては以下のようなコードが考えられます。

このコードは slice の要素を index によって指定し、「こんにちは」の文字列が出力されることを期待したものとします。

package main

import "fmt"

func main() {
	a := "こんにちは"
	for i := range a {
		fmt.Printf("%d: %q\n", i, a[i])
	}
}

// output:
// 0: 'ã'
// 3: 'ã'
// 6: 'ã'
// 9: 'ã'
// 12: 'ã'

上記のように、UTF-8 によってデコードされた結果、予期せぬバイト列が出力されます。

Go で競技プログラミングをコーディングする際などにこのエラーケースに引っかかることがありそうです。

終わりに

今回は rune について紹介させていただきました。

文字セットや Go の言語仕様について学ぶ手助けになれば幸いです。

弊社 Belong では一緒にサービスを育てる仲間を募集しています。

もし弊社に興味を持っていただけたら 弊社の紹介ページ をご覧いただけたら幸いです。

参考資料