Go
unicode

Go で UTF-8 の文字列を扱う

More than 3 years have passed since last update.

Go で UTF-8 の文字列の扱いに慣れるために練習で書いたコードをまとめました。


文字列を表示する

package main

func main() {
str := "あいうえお"
buf := []byte("あいうえお")
runes := []rune("あいうえお")

println(str)
println(string(buf))
println(string(runes))
}


文字列リテラルを使う

package main

func main() {
str := "か\u3099\u3099\u3099\u3099\u3099"
str2 := "DOG FACE \U0001F436"

println(str)
println(str2)
}

文字列リテラルを使う場合、次のとおり。

package main

func main() {
str := "\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88\xe3\x81\x8a"
println("あいうえお" == str)
}


数値から文字列を生成する

次のようにコードポイントをあらわす数値から rune スライスをつくることができる。

package main

func main() {
runes := []rune{0x3042, 0x3044, 0x3046, 0x3048, 0x304A}
println(string(runes))
}

バイト列をあらわす数値から byte スライスを次のようにつくることができる。

package main

func main() {
buf := []byte{0xe3, 0x81, 0x82, 0xe3, 0x81, 0x84, 0xe3, 0x81, 0x86, 0xe3, 0x81, 0x88, 0xe3, 0x81, 0x8a}
println(string(buf))
}


Go の仕様


string、byte、rune の相互変換ルール

Go の仕様ドキュメントの Conversions to and from a string type にまとめられています。

符号つきもしくは符号なしの整数値を string 型に変換すると、整数の UTF-8 表現を含む文字列が生成されます。有効な Unicode コードポイントの範囲の外にある値は "\uFFFD" に変換されます。

string('a')       // "a"

string(-1) // "\ufffd" == "\xef\xbf\xbd"
string(0xf8) // "\u00f8" == "ø" == "\xc3\xb8"
type MyString string
MyString(0x65e5) // "\u65e5" == "日" == "\xe6\x97\xa5"

byte のスライスを string 型に変換すると、スライスの要素が連続したバイト列である文字列が生成されます。

string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'})   // "hellø"

string([]byte{}) // ""
string([]byte(nil)) // ""

type MyBytes []byte
string(MyBytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"

rune のスライスを string 型に変換すると、それぞれの rune の値を文字に変換して連結した文字列が生成されます。

string([]rune{0x767d, 0x9d6c, 0x7fd4})   // "\u767d\u9d6c\u7fd4" == "白鵬翔"

string([]rune{}) // ""
string([]rune(nil)) // ""

type MyRunes []rune
string(MyRunes{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"

string 型の値を byte のスライスに変換すると、連続した要素が文字列のバイトであるスライスが生成されます。

[]byte("hellø")   // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}

[]byte("") // []byte{}

MyBytes("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}

string 型の値を rune のスライスに変換すると、それぞれの文字の Unicode コードポイントを保有するスライスが生成されます。

[]rune(MyString("白鵬翔"))  // []rune{0x767d, 0x9d6c, 0x7fd4}

[]rune("") // []rune{}

MyRunes("白鵬翔") // []rune{0x767d, 0x9d6c, 0x7fd4}


バイト数を求める

package main

func main() {
str := "あいうえお"
buf := []byte("あいうえお")
runes := []rune("あいうえお")

println(15 == len(str))
println(15 == len(buf))
println(15 == len(string(runes)))
}


文字数を求める

package main

import "unicode/utf8"

func main() {
str := "あいうえお"
buf := []byte("あいうえお")
runes := []rune("あいうえお")

println(5 == utf8.RuneCountInString(str))
println(5 == utf8.RuneCount(buf))
println(5 == len(runes))
}


1文字ずつアクセスする

package main

func main() {
str := "あいうえお"

for index, runeValue := range str {
println("位置:", index, "文字:", string([]rune{runeValue}))
}
}

rune 型のスライスの場合、次のようになる。

package main

import (
"fmt"
)

func main() {
runes := []rune("あいうえお")

for i := range runes {
fmt.Printf("%c\n", runes[i])
}
}

utf8.DecodeRuneInString を使う場合、次のように書ける。

package main

import (
"fmt"
"unicode/utf8"
)

func main() {
str := "あいうえお"

for len(str) > 0 {
r, size := utf8.DecodeRuneInString(str)
fmt.Printf("%c\n", r)

str = str[size:]
}
}


不正なバイト列


不正なバイト列が含まれるかチェックする

package main

import "unicode/utf8"

func main() {
str := "あいうえお\x80"
buf := []byte("あいうえお\x80")

println(false == utf8.ValidString(str))
println(false == utf8.Valid(buf))
}


不正なバイト列を代替文字の U+FFFD に置き換える

package main

func main() {
str := "あいうえお\x80"
buf := []byte("")

for _, runeValue := range str {
buf = append(buf, string(runeValue)...)
}

println(string(buf))
}


東アジアの文字幅


文字幅の種類を調べる

文字幅の種類をあらわす定数として EastAsianAmbiguousEastAsianWideEastAsianNarrowEastAsianFullwidthEastAsianHalfwidth が定義される。

package main

import (
"golang.org/x/text/width"
)

func main() {
s := "あいうえお"
p, _ := width.LookupString(s)

println(width.EastAsianWide == p.Kind())
}


全角文字と半角文字の相互変換

package main

import (
"golang.org/x/text/width"
"fmt"
)

func main() {
s := "ハンカクカタカナ"
w := width.Widen.String(s)
fmt.Printf("%U: %s\n", []rune(s), s)
fmt.Printf("%U: %s\n", []rune(w), w)
}

package main

import (
"golang.org/x/text/width"
"fmt"
)

func main() {
s := "ゼンカクカタカナ"
w := width.Narrow.String(s)
fmt.Printf("%U: %s\n", []rune(s), s)
fmt.Printf("%U: %s\n", []rune(w), w)
}


Unicode 照合

package main

import (
"golang.org/x/text/collate"
"golang.org/x/text/language"
)

func main() {
c := collate.New(language.Japanese)

println(0 == c.CompareString("a", "a"))
println(-1 == c.CompareString("a", "A"))
println(1 == c.CompareString("A", "a"))

println(0 == c.CompareString("が", "か\u3099"))
}


Unicode 正規化

package main

import "golang.org/x/text/unicode/norm"

func main() {
buf := norm.NFD.Bytes([]byte("がぎぐげご"))
str := norm.NFD.String("がぎぐげご")
expected := "か\u3099\u3099\u3099\u3099\u3099"

println(expected == string(buf))
println(expected == str)
}

U+FDFA に NFKD を適用すると18文字になる。Unicode 正規化を適用して長くなる文字の例は 「What are the maximum expansion factors for the different normalization forms?」(Unicode.org)の記事を参照。

package main

import (
"golang.org/x/text/unicode/norm"
"unicode/utf8"
)

func main() {
str := norm.NFKD.String("\uFDFA")

println(18 == utf8.RuneCountInString(str))
}

基底文字の後に続く装飾記号の個数が31以上である場合、正規化は適用されない。

package main

import (
"strings"
"golang.org/x/text/unicode/norm"
"unicode/utf8"
)

func main() {
str := "か" + strings.Repeat("\u3099", 30)
ret := norm.NFC.String(str)

println(30 == utf8.RuneCountInString(ret))

str2 := "か" + strings.Repeat("\u3099", 32)
ret2 := norm.NFC.String(str2)

println(33 == utf8.RuneCountInString(ret2))
}