Go 语言类型 字符型

基本概念

字符(rune)用于表示 Unicode 字符,本质上是 int32 类型别名。字符通过使用 int32 类型的数字来保存其 Unicode 编码,Unicode 码点取值范围从 00x10FFFF,总共有 1114112 个码点。

UTF

UTF(Unicode Transformation Format, Unicode 字符集转换格式)是一种编码方式,用于将 Unicode 字符集的字符表示为字节序列,是 Unicode 标准的一部分。目前主流 UTF 编码格式有三种:

  • UTF-8:采用 1 到 4 个字节的可变长度字符编码方式,完全兼容 ASCII 编码。例如,西文字符通常使用 1 个字节,希腊字母需要 2 个字节,而东亚字符(CJK)则需要 3 个字节。Go 语言中默认使用 UTF-8 编码。
  • UTF-16:使用 2 或 4 个字节无符号整数存储 Unicode 字符,有大端(Big Endian)和小端(Little Endian)之分。处理中文时相比于 UTF-8 更加高效。
  • UTF-32:采用 4 个字节固定长度编码,编码和解码简单。适用于处理速度优先于存储效率的场合。

Unicode 编码表示格式(BMP)为 U+hhhh,其中 h 代表一个十六进制数字。在 Go 语言中,可以直接使用这种格式定义 Unicode 字符:

package main

import "fmt"

func main() {
	var ch rune = '\u03B2'                // Unicode 编码表示
	fmt.Printf("%d\n", ch)                // 输出整数:946
	fmt.Printf("%c\n", ch)                // 输出实际字符:β
	fmt.Printf("%X\n", ch)                // 输出 Unicode 编码值:3B2
	fmt.Printf("%U\n", ch)                // 输出 Unicode 标准形式:U+03B2
	utf8Bytes := []byte(string(rune(ch))) // UTF-8 编码多字节字符必须转为字节切片
	fmt.Printf("%#X", utf8Bytes)          // 输出十六进制格式:0XCEB2
}

转义字符

转义字符用来表示在字符串中不能直接表示的字符。转移字符以一个反斜杠 \ 开始,后跟一个或多个字符来构成一个转义序列:

转义序列 说明
\\ 反斜杠字符「\」
\' 单引号字符「'」
\" 双引号字符「"」
\? 疑问号字符「?」
\b 退格符
\f 换页符
\n 换行符
\r 回车符
\t 水平制表符
\v 垂直制表符

声明和初始化

字符值需要用单引号 ' 括起来:

package main

import "fmt"

func main() {
	// 类型零值为 0
	var a rune

	// 使用 Unicode 编码值初始化,支持补充平面
	var b = '\u0063' + '\U00010330'

	// 短变量声明,使用字面量初始化
	c := 'c'

	// 表达式赋值,使用十六进制数字初始化
	d := rune(0x64)

	fmt.Println(a, b, c, d)                  // 打印出整数
	fmt.Printf("%c %c %c %c \n", a, b, c, d) // 打印字符
}

字符型切片

字符型切片用于正确处理可能包含多字节字符的 UTF-8 编码字符串:

package main

import "fmt"

func main() {
	// 直接创建一个字符型切片并初始化其值
	runeSlice := []rune{'G', 'o', '语', '言'}
	// 将字符串转换为字符切片,来处理每个 Unicode 字符
	runeSliceFromString := []rune("你好世界")

	fmt.Println(runeSlice)                  // 输出字符切片的数字表示:[71 111 35821 35328]
	fmt.Printf("%q\n", runeSliceFromString) // 输出字符形式:['你' '好' '世' '界']
}

字符处理

在 Go 语言中,unicode/utf8unicode 包提供了不同但互补的功能集,用于处理 Unicode 字符和 UTF-8 编码。

字符分类

unicode 库定义了多个字符范围表,代表不同字符类别,如字母(unicode.Letter)、数字(unicode.Digit)、标点符号(unicode.Punct)等。每个类别都对应一个特定函数,用于检查字符是否属于该类别。此外还提供检查空白字符、字母大小写、可打印等字符属性的函数:

package main

import (
	"fmt"
	"unicode"
)

func main() {
	// 下面结果都为 true
	fmt.Println(unicode.IsLetter('a')) // 检测字母
	fmt.Println(unicode.IsDigit('1'))  // 检测数字
	fmt.Println(unicode.IsPunct(','))  // 检测标点符号
	fmt.Println(unicode.IsSpace('\t')) // 检测空白字符,包括空格、制表符、回车符等
	fmt.Println(unicode.IsLower('b'))  // 检测是否小写
	fmt.Println(unicode.IsPrint('c'))  // 检测是否可打印
}

字符属性检查函数都返回布尔值,表示字符是否属于相应分类。

大小写转换

unicode 包中大小写转换功能基于 Unicode 标准,因此支持多种语言字符,包括希腊语、俄语等小语种语言:

package main

import (
	"fmt"
	"unicode"
)

func main() {
	fmt.Println(string(unicode.ToUpper('a'))) // 英语,输出大写:A
	fmt.Println(string(unicode.ToUpper('ł'))) // 波兰语,输出大写:Ł
	fmt.Println(string(unicode.ToUpper('μ'))) // 希腊语,输出大写:Μ
	fmt.Println(string(unicode.ToUpper('б'))) // 俄语,输出大写:Б
	fmt.Println(string(unicode.ToUpper('啊'))) // 东亚文字没有大小写之分,输出不变:啊
	fmt.Println(string(unicode.ToUpper('𝗲'))) // 符号也没有大小写之分,输出不变:𝗲
}

语言检测

上面提到 unicode 库内有多个字符范围表(RangeTable),有些范围表代表某种语言字母表,可以直接配合 unicode.Is 函数使用,检测字符是否属于该语言:

package main

import (
	"fmt"
	"unicode"
)

func main() {
	// 示例字符
	chars := []rune{'α', 'β', 'A', 'Ω', 'π', 'z'}

	// 遍历字符并检查是否是希腊字符
	for _, r := range chars {
		// 希腊字符表包含在 unicode.Greek 中
		if unicode.Is(unicode.Greek, r) {
			fmt.Printf("%c is a Greek character.\n", r)
		} else {
			fmt.Printf("%c is not a Greek character.\n", r)
		}
	}
}

但并不是每种语言都有专用范围表,例如中文横跨多个 Unicode 字符区块,并且和日语有共用字符,这种情况可以自定义一个 RangeTable 来表达:

package main

import (
	"fmt"
	"unicode"
)

// 日语字符范围
var japanese = &unicode.RangeTable{
	R16: []unicode.Range16{
		{0x3040, 0x309F, 1}, // 平假名
		{0x30A0, 0x30FF, 1}, // 片假名
		{0xFF66, 0xFF9D, 1}, // 半角片假名
	},
	R32: []unicode.Range32{
		{0x1B000, 0x1B0FF, 1}, // Kana Supplement
		{0x1B100, 0x1B12F, 1}, // Kana Extended-A
	},
}

// 中文字符范围
var chinese = &unicode.RangeTable{
	R16: []unicode.Range16{
		{0x4E00, 0x9FFF, 1}, // 常用汉字
	},
	R32: []unicode.Range32{
		{0x20000, 0x2A6DF, 1}, // 扩展 B
		{0x2A700, 0x2B73F, 1}, // 扩展 C
		{0x2B740, 0x2B81F, 1}, // 扩展 D
		{0x2B820, 0x2CEAF, 1}, // 扩展 E
		{0x2CEB0, 0x2EBEF, 1}, // 扩展 F
	},
}

func main() {
	// 遍历字符并检查是否是在字符集中
	for _, r := range []rune{'a', 'β', '你', 'ん'} {
		if unicode.In(r, japanese, chinese) {
			fmt.Printf("%c is a Japanese or Chinese character.\n", r)
		}
	}
}

如果要求没那么严格,也可以使用预定义字符集。例如 unicode.Han 中包括简体字、繁体字、日文汉字和韩文汉字,unicode.Hiragana 包含日文平假名,应付一般语言判断场合够用。

UTF-8 编码

unicode/utf8 包专注于处理 UTF-8 编码文本,常用功能包括验证、编码和解码:

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	// 验证字符串编码是否有效,对无效编码解码会得到 \uFFFD
	fmt.Println(utf8.ValidString("Hello, 世界"))                      // 输出:true
	fmt.Println(utf8.ValidString(string([]byte{0xff, 0xfe, 0xfd}))) // 输出:false

	// 解码单个 Unicode 字符,返回单个字符和字符占用字节数
	r, size := utf8.DecodeRuneInString("こんにちは")
	fmt.Printf("%c %v\n", r, size) // 输出:'こ' 和 3

	// 编码单个 Unicode 字符,返回编码后占用字节数
	buf := make([]byte, 4)             // 创建目标字节切片
	n := utf8.EncodeRune(buf, 'あ')     // 将 UTF-8 编码写入切片
	fmt.Printf("% x %v\n", buf[:n], n) // 输出:e3 81 82 和 3
}