Go 言語で文字列の文字数を取得する方法
Go の len() はバイト数を返すため、日本語や絵文字では文字数と一致しません。utf8.RuneCountInString() で rune 数を取得する方法と、絵文字での注意点を解説します。
はじめに
この記事では Go の基本的な構文(変数宣言、関数呼び出し、import 文など)を理解していることを前提としています。
Go の文字列には「バイト数」「文字数(rune 数)」「見た目の文字数(書記素クラスタ数)」の 3 つの「長さ」があります。
Go の文字列は内部的に UTF-8 エンコードされたバイトのスライス(バイト列)です。そのため、len() 関数は文字数ではなくバイト数を返します。日本語や絵文字を含む文字列では、バイト数と文字数が一致しません。
文字数(rune 数)を取得するには unicode/utf8 パッケージを使います。さらに、絵文字や結合文字を正しく数えるには外部ライブラリが必要です。
Go 公式ブログでも文字列の内部表現について詳しく解説されています。
| 方法 | 取得できる値 | 用途 |
|---|---|---|
len(s) | バイト数 | バッファサイズ計算、I/O 処理 |
utf8.RuneCountInString(s) | rune 数(Unicode コードポイント数) | 一般的な文字数カウント |
バイト数を取得する(len 関数)
len 関数の基本
len() は Go の組み込み関数です。文字列に対して使うとバイト数を返します。
package main
import "fmt"
func main() {
s := "Hello"
fmt.Println(len(s)) // 5
}
ASCII 文字列の場合
ASCII 文字(英字、数字、記号)は 1 文字 = 1 バイトで表現されます。そのため、ASCII 文字のみの文字列では len() の結果が文字数と一致します。
package main
import "fmt"
func main() {
// ASCII 文字は 1 文字 = 1 バイト
fmt.Println(len("Hello")) // 5
fmt.Println(len("Go 1.26")) // 7
fmt.Println(len("")) // 0
}
日本語を含む場合の注意点
日本語の文字は UTF-8 で 3 バイトを使用します。そのため、len() はバイト数を返すので、日本語の文字数とは一致しません。
package main
import "fmt"
func main() {
s := "こんにちは"
// 日本語 1 文字 = 3 バイト(UTF-8)
fmt.Println(len(s)) // 15(5 文字 × 3 バイト)
}
UTF-8 では文字の種類によってバイト数が異なります。
| 文字の種類 | バイト数 | 例 |
|---|---|---|
| ASCII(英数字・記号) | 1 バイト | A, 1, ! |
| ラテン文字(アクセント付き) | 2 バイト | é, ñ |
| 日本語・中国語・韓国語 | 3 バイト | あ, 漢, 한 |
| 絵文字 | 4 バイト | 😀, 🌍 |
文字数(rune 数)を取得する
rune とは
rune は Go の型で、int32 のエイリアス(別名)です。1 つの rune は 1 つの Unicode コードポイント(Unicode が各文字に割り当てた固有の番号)に対応します。Go では文字のことを「rune」と呼びます。
// rune は int32 のエイリアス type rune = int32
rune リテラルはシングルクォートで囲んで記述します。
package main
import "fmt"
func main() {
// rune リテラルはシングルクォートで囲む
var r rune = 'あ'
fmt.Println(r) // 12354(Unicode コードポイントの数値)
fmt.Printf("%c\n", r) // あ
fmt.Printf("U+%04X\n", r) // U+3042
}
utf8.RuneCountInString 関数
unicode/utf8 パッケージの RuneCountInString() 関数は、文字列に含まれる rune 数(Unicode コードポイントの数)を返します。メモリ割り当てなしで効率的に動作します。
func RuneCountInString(s string) (n int)
基本的な使い方は以下のとおりです。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Hello, 世界"
fmt.Println("バイト数:", len(s)) // 13
fmt.Println("文字数:", utf8.RuneCountInString(s)) // 9
}
さまざまな文字列でバイト数と rune 数を比較してみましょう。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
examples := []string{
"Hello",
"こんにちは",
"Hello, 世界",
"",
}
for _, s := range examples {
fmt.Printf("\"%s\"\n", s)
fmt.Printf(" バイト数: %d, 文字数: %d\n\n",
len(s), utf8.RuneCountInString(s))
}
}
出力:
"Hello" バイト数: 5, 文字数: 5 "こんにちは" バイト数: 15, 文字数: 5 "Hello, 世界" バイト数: 13, 文字数: 9 "" バイト数: 0, 文字数: 0
[]rune に変換して数える方法
[]rune(s) で文字列を rune のスライスに変換し、len() で長さを取得する方法もあります。
package main
import "fmt"
func main() {
s := "こんにちは"
runes := []rune(s)
fmt.Println(len(runes)) // 5
fmt.Println(runes[0]) // 12371(「こ」の Unicode コードポイント)
fmt.Printf("%c\n", runes[0]) // こ
}
文字数を数えるだけなら utf8.RuneCountInString() を使いましょう。[]rune への変換は、変換後のスライスに対して個別の文字にアクセスしたい場合に使います。
utf8.RuneCountInString(s)- メモリ割り当てなし、高速len([]rune(s))- メモリ割り当てあり、スライスが必要な場合に使用
絵文字や結合文字の扱い
rune 数と見た目の文字数が異なるケース
絵文字や結合文字では、rune 数と見た目の文字数が一致しない場合があります。具体的な例で確認してみましょう。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
examples := []string{"🇯🇵", "👨👩👧👦", "👍🏼"}
for _, s := range examples {
fmt.Printf("\"%s\"\n", s)
fmt.Printf(" バイト数: %d\n", len(s))
fmt.Printf(" rune 数: %d\n", utf8.RuneCountInString(s))
fmt.Println()
}
}
出力:
"🇯🇵" バイト数: 8 rune 数: 2 "👨👩👧👦" バイト数: 25 rune 数: 7 "👍🏼" バイト数: 8 rune 数: 2
見た目はすべて 1 文字ですが、rune 数は 1 ではありません。これは、1 つの絵文字が複数の Unicode コードポイント(rune)を組み合わせて構成されているためです。
- 国旗絵文字
🇯🇵は 2 つの Regional Indicator Symbol(地域指示記号)で構成される - 家族絵文字
👨👩👧👦は 4 つの絵文字が ZWJ(ゼロ幅接合子、見えない結合用の文字)で結合されている - 肌色付き絵文字
👍🏼は基本絵文字 + 肌色修飾子で構成される
このように、複数の rune がまとまって 1 つの「見た目の文字」を構成する単位を書記素クラスタと呼びます。
書記素クラスタ(grapheme cluster)とは
書記素クラスタ(grapheme cluster)は、人間が「1 文字」と認識するテキストの最小単位です。Unicode Standard Annex #29 で定義されています。上の例で見たように、家族絵文字 👨👩👧👦 は 7 つの rune で構成されていますが、書記素クラスタとしては 1 つです。
Go の標準ライブラリには書記素クラスタを扱う機能がないため、見た目どおりの文字数が必要な場合は外部ライブラリを利用します。
外部ライブラリを使う方法
見た目どおりの文字数を取得するには、書記素クラスタに対応した外部ライブラリが必要です。たとえば rivo/uniseg では GraphemeClusterCount() 関数で書記素クラスタ数を取得できます。
package main
import (
"fmt"
"github.com/rivo/uniseg"
)
func main() {
fmt.Println(uniseg.GraphemeClusterCount("👨👩👧👦")) // 1
}
用途別の使い分けガイド
どの方法を使うべきか迷ったときは、以下の表を参考にしてください。
| 用途 | 使用する関数 | パッケージ |
|---|---|---|
| バッファサイズの計算 | len(s) | 組み込み |
| ファイル I/O のサイズ指定 | len(s) | 組み込み |
| 一般的な文字数カウント | utf8.RuneCountInString(s) | unicode/utf8 |
| 文字数制限のバリデーション | utf8.RuneCountInString(s) | unicode/utf8 |
| 絵文字を含むテキストの文字数 | 書記素クラスタ対応の外部ライブラリを使用 | - |
まとめ
この記事で学んだ内容を振り返ります。
len(s)はバイト数を返す。ASCII 文字のみの場合は文字数と一致するutf8.RuneCountInString(s)は rune 数(Unicode コードポイント数)を返す。一般的な文字数カウントに使う- 絵文字を含むテキストで見た目の文字数が必要な場合は、書記素クラスタ対応の外部ライブラリを使う
- 目的に応じて適切な方法を選ぶことが重要