Go で文字列からN番目の文字を抜き出したい際に、以下のようなコードを想像する方は多いのではないでしょうか?
s := "Hello World"
fmt.Println(s[0]) // 72
私もそのうちの一人でした。fmt.Println(s[0])
の値として期待するのは ”72” ではなく “H” です。この挙動は私が業務で使用している node.js とは異なる挙動で気になったので調べてみました。
String とは
In Go, a string is in effect a read-only slice of bytes.
Go において string は read-only な byte のスライスです。
つまり、fmt.Println(s[0])
の値として表示された “72” は byte を表しています。
s := "Hello World"
fmt.Println(s[0]) // 72 <- byte
Goにおいて string は、UnicodeやUTF-8などで定義されたテキスト、その他の定義済みのフォーマット等を保持しているわけではないのです。
Unicode と Code Point
唐突ですが、Unicode と Code Point について少し解説します。
Unicodeは世界で使われる全ての文字を共通の文字集合にて利用可能に使用という考えで作られた標準規格です。
コンピュータ上では、文字は内部的に数値として扱われます。そのため数値が文字をどのように表すのかという規則が必要になります。そこで登場するのが Code Point です。
Unicode では、Code Point とは文字をエンコードした16ビットの数値になります。Code Space と呼ばれる 0 to 10FFFF(16進数)の間の整数で表現されます。
以下の図では、文字(Abstract)と Code Point(Encoded)の対応関係を示しています。
引用:https://www.unicode.org/versions/Unicode14.0.0/ch02.pdf#G25564
Rune と Code Point
The Go language defines the word
rune
as an alias for the typeint32
Go では rune は int32 のエイリアスです。
“Code point” is a bit of a mouthful, so Go introduces a shorter term for the concept: rune.
つまり、rune は 先ほどの章で説明した Unicode の Code Point です。
試しに Unicode でエスケープした文字と、 rune にキャストした値を比較しても一致しています。
s := `⌘`
fmt.Printf("%+q\n", s) // "\u2318"
fmt.Printf("% x", []rune(s)[0]) // 2318
Single quote, Double quote, Back quote
文字列をリテラルで定義するときに、single quote(’)、double quote(”)、back quote(`) で定義することができるが、以下のような違いがある。
'a' // rune型(code point なので複数の文字の表現はできない)
"Hello\nWorld" // string型(エスケープシーケンスが解釈される)
`Hello\nWorld` // string型(エスケープシーケンスが解釈されない)
Loop
ひとつずつ byte のスライスを読み込んでいった場合は、byte を取得できます。
s := "Hello World"
for i := 0; i < len(s); i++ {
fmt.Println(s[i]) // byte
}
range を 使った場合、値には byte ではなく rune が入るので注意が必要です。index には読み込んでいる rune を byte で表現した際の先頭の位置が格納されています。
s := "日本語"
for index, value := range s {
fmt.Printf("%#U starts at byte position %d\n", value, index)
}
// Output
// U+65E5 '日' starts at byte position 0
// U+672C '本' starts at byte position 3
// U+8A9E '語' starts at byte position 6
おまけ:数値を文字列に変換する関数
以下のことを利用すると、数値を文字列に変換する関数なんかも作れます。
- rune は Code Point であり、 int32 のエイリアスでである
- A-Z の文字は Unicode 上で隣あう Code Point で表現されている
- int32 は数値なので演算が可能で
大文字のAを基準に演算することで数値を文字列に変換する関数が以下です。
func IntToCapitalizedChar(i int) string {
// single quote で囲っているので、A は rune型(Code Point)
// rune型は数値なので演算可能
// 'A' から i 番目の rune を計算し、 string 型へキャストする
return string('A' + i)
}
func main() {
for i:=0; i<26; i++ {
fmt.Println(IntToCapitalizedChar(i))
}
}
// Output
// A
// ...
// Z
おまけ:N番目の文字列は抜き出せる
冒頭ではN番目の文字列は抜き出せないかのように表現していますが、 string が byte のスライスであることを理解すると文字列の抜き出しが可能なことに気づきます。
Go は UTF-8 であり、日本語の文字列が3バイトであることを利用すると、3バイトずつスライスを読み込むことでN番目の文字を取得できます。
func main() {
s := "こんにちは"
for i := 0; i < len(s); i += 3 {
fmt.Println(s[i:i+3])
}
}
// Output
// こ
// ん
// に
// ち
// は
まとめ
- string は read-only な byte のスライスである
- Unicode の Code Point とは文字をエンコードした際の16ビットの数値
- rune は Unicode の Code Point であり、 int32 のエイリアスである
- 文字列をリテラルで定義する際の違い
- single quote(’): rune 型
- double quote(”): string型(エスケープシーケンスが解釈される)
- back quote(`): string型(エスケープシーケンスが解釈されない)
- loop 処理は range を使うと、値に byte ではなく rune が渡される