はじめに
この記事は、Go言語で文字列処理を書いていて「なんでstringスライスにインデックスアクセスできないの?」と混乱した方、あるいはs[0]と書いたら予想と違う値が返ってきて戸惑った方向けです。
この記事を読むことで、Goにおけるstring型の内部表現と、文字単位でアクセスするための正しい方法(ルーン変換)が身につきます。サンプルコードを交えて実際に動作確認しながら進めるので、すぐに業務や個人開発に活かせるでしょう。
前提知識
- Goの基礎文法(変数宣言、for文、スライス)
- 文字コードについての基礎知識(ASCII以外の言語でも開発した経験があると尚良い)
stringは「バイトのスライス」である
Go言語ではstring型は読み取り専用のバイトスライスです。
つまり、s := "Hello"という変数は内部的に[]byte{72,101,108,108,111}と同じように扱われます。
ASCII文字だけなら1文字1バイトなので「s[0]で'H'が取れる」ように見えますが、日本語などマルチバイト文字が混じると話は別になります。
Gopackage main import "fmt" func main() { s := "こんにちは" fmt.Println(s[0]) // 227 が出力される }
227は「こ」のUTF-8エンコード後の先頭バイトです。人が読みたいのは「こ」というルーンなので、バイト単位で触っても意味が通じません。
正しく文字(ルーン)単位でアクセスする方法
Goでは文字単位の処理をする際、文字列をルーンスライスに変換してから扱います。以下に実装パターンを3種類紹介します。
ステップ1:ルーンスライスへの変換とアクセス
[]rune(string)で文字列をルーンスライスにできます。ルーンはUnicodeコードポイントを表すため、日本語でも1ルーン=1文字です。
Gopackage main import "fmt" func main() { s := "Hello, 世界" r := []rune(s) fmt.Printf("%c\n", r[7]) // 世 }
ステップ2:rangeループで文字単位で処理
インデックスが不要ならfor _, ch := range sで直接ルーンを取得できます。
Gofor i, ch := range "Go言語" { fmt.Printf("%d: %c\n", i, ch) } // 0: G // 1: o // 2: 言 // 5:語
インデックスiはバイト位置で進むため、マルチバイト文字では飛び飛びになることに注意してください。
ハマった点:「stringスライスにアクセスできない」とエラーになるわけではない
コンパイルエラーではなく「思った文字が取得できない」というランタイムの驚きがポイントです。特に他言語(Python/Java/C#など)ではs[i]が文字を返してくれるため、「Goはなんで配列アクセスできないの?」と誤解されがちです。
解決策:文字単位処理には常にルーンを意識する
- 文字列の長さを取得したい →
utf8.RuneCountInString(s) - n番目の文字が知りたい →
[]rune(s)[n] - 部分文字列を取りたい →
string([]rune(s)[0:3]) - バイト位置と文字位置を同時に扱いたい →
for i, w := 0, 0; i < len(s); i += w { r, w = utf8.DecodeRuneInString(s[i:]) … }
まとめ
本記事では、Goでstring型スライスにアクセスできない「わけではない」ことと、正しく文字単位で処理する方法を解説しました。
- stringはバイトスライスであり、マルチバイト文字では単純なインデックスアクセスでは意図した文字が得られない
- 文字単位で処理するには
[]runeへの変換やfor rangeを使う - バイト位置と文字位置を混同しないよう、常にUTF-8を意識する習慣を持つ
この知識があれば、日本語を含む文字列処理でも安全・高速に実装できます。次回は、ルーン変換を避けつつ高速に処理するutf8パッケージの活用法を紹介します。
参考資料
- Go公式ドキュメント - Strings, bytes, runes
- The Go Programming Language Specification - String types
- Unicode標準仕様 日本語訳(PDF)
