[Go] rune の仕様と良くあるエラーケース
概要
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 では一緒にサービスを育てる仲間を募集しています。
もし弊社に興味を持っていただけたら 弊社の紹介ページ をご覧いただけたら幸いです。