一、Golang字符类型及字符编码简介
1.1 Golang字符类型
字符 并不是 Go语言的一个类型,只是整数的特殊用例。
在Golang中的字符类型有以下两种:
- 一种是
uint8
类型,或者叫byte
型,该类型变量可存放 标准 ASCII码 的一个字符; - 另一种是
rune
类型,代表一个 UTF-8字符,当需要处理中文、日文或者其它复合字符时,则需要用到rune
类型。
Tips: rune 类型实际是一个 int32类型。
byte
类型是 uint8
的别名,对于只占用 1 个字节的传统 ASCII编码 的字符来说,使用 byte
完全没有问题。
例如:
|
|
在 ASCII 码表中,字符 M
的值是 77,使用 16 进制表示则为 4d
,使用 8 进制表示则为 115
,所以下面的写法是等效的:
|
|
Golang 同时也支持 Unicode编码,因此,在Golang中字符同样称为 Unicode 代码点 或者 runes
,在内存中实际是使用 int
类型来表示。
中文字符在Unicode下占2个字节,在UTF-8编码下占3个字节。(Golang默认编码是UTF-8)。
在文档中,一般使用格式 U+hhhh
来表示一个字符,其中 h
表示一个 16 进制数[0-9,A-F]。
Tips:
rune
也是 Go 当中的一个类型,并且是int32
的别名。
在书写 Unicode字符时,需要在 16 进制数之前加上前缀 \u 或者 \U。
因为 Unicode 一般至少占用 2 个字节,所以使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则会加上 \u 前缀;前缀 \u 总是紧跟着长度为 4 的 16 进制数,前缀 \U 紧跟着长度为 8 的 16 进制数。
|
|
格式化说明符:
- %c 用于输出字符;当和字符配合使用时,
- %v 或 %d 会输出用于表示该字符的整数;
- %U 输出格式为 U+hhhh 的字符串;
unicode包 包含了一些针对测试字符的非常有用的函数(其中 ch 代表字符):
- 判断是否为字母:unicode.IsLetter(ch)
- 判断是否为数字:unicode.IsDigit(ch)
- 判断是否为空白符号:unicode.IsSpace(ch)
字符类型使用细节:
- 字符常量是用单引号(’’)括起来的单个字符
例如:
var c1 byte = 'a'
- GO中允许使用转义字符’\‘来将后面的字符转换为特殊型常量
例如:
var c3 byte = '\n'
\n 表示换行符 - Go语言的字符使用UTF-8编码
- 在Go中,字符的本质是一个整数,直接输出时,是该字符对应的UTF-8编码的值
- 可以直接给某个变量赋一个数字,然后按
%c
格式化输出时,会输出该数字对应的unicode字符 - 字符类型是可以进行运算的,相当于一个整数,因为它都对应有Unicode码(一个整数数值)
Tips:
- UTF-8 是 Unicode 的实现方式之一,UTF-8 最大的一个特点,就是它是一种变长的编码方式,它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度。
- UTF-8编码 字符集完全兼容 标准ASCII码,也就是说 标准ASCII码 是 UTF8编码集的子集。
1.2 计算机字符编码集简介
比特(bit)是计算机处理的最小单位、值为 0 或者 1,一个字节包含8个bit,最大值(11111111)为 255,最小值(00000000)为 0;
一个字节能代表 256(0-255)个数字,二个字节可以表示 65536(0-65535)个数字,更多的字节可以有更多种组合,也就可以表示更大的数值范围,整数可以这样存,那么字符呢?
一堆二进制 0
或者 1
,无论怎么也算不出 A
,那就通过数字中转一下,只要给 A
指定一个数值编号(字符与编号组成一种映射关系),存储 A
时就存储这个映射的编号值,读取时也是读取到编号数值、再按照这个编号数值与字符的映射关系找到这个字符 A
,像这样 收录 许多字符然后给它们一一编号(建立映字符与编码的映射),就得到一个字符编号对照表,这就是一个 字符集。
知道字符,通过字符集映射就可找到对也得编码,反之,找到字符编号,通过字符集映射也可以找到字符。
常见字符集相关发展过程如下:
- ASCII字符集:标准ASSCII字符集只收录了128个字符,其扩展字符集也只有256个;
- GB2312字符集:由于ASCII里没有汉字,所以为了存储汉字出现了 GB2312字符集;
- BIG5字符集:由于GB2312里没有繁体字,所以出现了BIG5字符集;
- Unicode字符集:但是BIG5还有许多字符没有被收录,与其不断的推出收录更多字符的字符集,不如本着全球化统一标准的目的,制作一个通用字符集,Unicode学术学会就是这样做的,这个字符集就是 Unicode;Unicode字符集于1990年开始研发并于1994年正式公布,实现了跨语言跨平台的文本转换与处理。
下图为字符集发展历程:
1.3 计算机字符编码简介
1)定长编码 所谓定长编码就是用固定的长度去存储编译字符,最常见和广泛使用的的定长编码就是 ASCII码。
ASCII码一览表,ASCII码对照表: https://c.biancheng.net/c/ascii/
在ACSII中将一个字符表示成8位特定的二进制数,举例说明如下:
字符 | 十进制 | 二进制 |
---|---|---|
A | 65 | 01000001 |
B | 66 | 01000010 |
C | 67 | 01000011 |
定长编码 的优点是 简单、高效,不需要额外的数据协商和传输开销。但是,它也有一些缺点:
- 首先,它不够灵活,不能适应变长的数据结构和协议;
- 其次,它浪费空间,因为某些数据元素可能没有充分利用其分配的固定长度;
例如,ASCII编码是1个字节,而Unicode编码通常是2个字节:
- 字母
A
用ASCII编码是十进制的65
,二进制的01000001
; - 字符
0
用ASCII编码是十进制的48
,二进制的00110000
; - 汉字
中
已经超出了ASCII编码的范围,用Unicode编码是十进制的20013
,二进制的01001110 00101101
;
如果把ASCII编码的 A
用Unicode编码,只需要在前面补0就可以,因此,A
的Unicode编码是 00000000 01000001
。
2)变长编码 及 UTF8编码规则简介 在定长编码下,如果写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算,所以出现了 变长编码。
变长编码(Variable-length encoding) 是指在编码时,不同的符号可能占用不同的比特位数,它的主要特点是可以提高编码效率,因为在实际应用中不同的符号出现频率可能是不同的(小编号少占字节,大编号多占字节)。如果使用固定长度编码,则会浪费很多比特,降低数据传输效率。
在计算机科学中,常见的 变长编码 有 霍夫曼编码
、算术编码
、游程编码
等。在文本编码中广泛使用的 UTF-8
也是一种变长编码。
在 UTF-8 编码中,一个字符所占用的字节数是变化的,可以是 1、2、3 或 4 个字节,具体取决于字符的 Unicode 编码值,UTF-8 是一种非常常用的变长编码方式。
UTF-8编码模版格式如下:
占用字节数 | 十进制编码范围 | 模版 |
---|---|---|
1字节 | [0, 127] | 0xxxxxxx |
2字节 | [128,2047] | 110xxxxx 10xxxxxx |
3字节 | [2048, 65535] | 1110xxxx 10xxxxxx 10xxxxxx |
4字节 | [65536, 2097151] | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
5字节 | [2097152, 67108863] | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
6字节 | [67108863, 2147483647] | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
上述UTF-8字符编码字节模版的使用规则:
-
对于单字节字符,最高位为 0,后面7位为这个字符的Unicode码; 例如:字母
e
,属于标准ASCII码,UTF-8编码 兼容 标准ASCII码,两者在 0-127编码值对应的字符是一致的;字母e
的编码为101,该编码在 [0,127] 范围内、属于单字节字符,按照UTF-8编码规则对应模版 0xxxxxxx,现将101转为二进制:1100101,则UTF-8编码为:01100101, UNICODE编码为:U+0065,如下图所示: -
对于n字节字符(n > 1),第一个字节的前n位均为1,第n+1位为0,后面n-1字节的前两位均为10,剩下的位则为这个字符的Unicode码; 例如:汉字
“好”
字的Unicode编码十进制编码为:22909,该编码十进制在 [2048, 65535]范围内,属于3字节长度编码,使用三字节模版 1110xxxx 10xxxxxx 10xxxxxx,将十进制 22909 转为二进制转为二进制 1011001 01111101,最终得到UTF-8编码为:11100101 10100101 10111101,UNICODE编码为:U+597D,如图:
1.4 Golang UTF-8字符的判定
Go语言实现了 UTF-8编码验证算法 用于 检查UTF-8编码数据,主要基于UTF-8的可变长编码特点设计了验证算法,UTF-8编码使用1到4个字节 为每个字符编码,ASCII标准编码部分跟UTF-8编码一致,占用长度为1个字节。
Tips: UTF-8除了开始128个单字节编码 与 ASCII标准相同(兼容)以外,其它的使用自己的编码;
在Go中检验字符串是否为符合UTF-8规则的函数为:utf8.ValidString,源码如下:
|
|
函数流程大致为:
- 遍历字符串
s
,每次取 8 个字节编码数据判断其中是否存在非标准ASCII的编码数据。
为什么每次取8个字节呢?为什么不一个一个字节取去判断呢? 在现实环境中,在校验一篇长篇英文文章的场景下(验证大量标准ASCII编码数据),如果验证算法采用单个字符比较的方式检查编码,直到循环检查完整个数据,算法的运行耗时大,性能有待提升。 针对UTF-8编码验证算法中处理标准ASCII编码字符检查次数多、运行耗时大的问题,可以利用并行化编程思想,一次同时处理多个标准ASCII编码字符的检查,减少比较的次数,加快验证速度,提升算法性能。 Go语言的UTF-8验证算法应用了基于并行化编程思想的算法优化方案,一次同时检查8个标准ASCII编码,大大提升了算法的运行性能。
2. 那如何实现一次同时检查8个标准ASCII编码呢?这时候位运算就显示出它的特点来了。 前面讲到,单个标准ASCII编码字符十进制最大的值为127(即0x80),最简单的办法直接比较字符编码的十进制值大小,如下:
|
|
如果字节的数值小于 0x80(128)即为ASCII编码。
还有一种办法就是利用位运算:
- 将要比较的字符转为二进制模式,再跟0x80(10000000 )的二进制进行 & 运算,如果符合ASCII标准,则结果为0,如果结果不是0,则这个字符并不是标准ASCII编码范围内,则进行其它类型检测。
一次如何处理8个字节呢?举个例子说明golang中是怎么处理的。例如:字符串 “ABCDEFGH你好世界”, 要同时检查8个标准ASCII编码,则流程如下:
- 每次取8个字节数据进行操作,此处首8个字节数据为: ABCDEFGH, ASCII码以及二进制如下:
- 将取到的8字节数据按每4个字节组成一组组成一个unit32的整数,此处 ABCD组成一个4字节(32bit)二进制数据first32, 而EFGH组成另外一个4字节二进制数据second32, 方法如下:
|
|
- 将两组4字节的二进制数据进行
|
操作(first32 |second32 )
,并将最终结果中的每个字节数据与0x80
进行&
操作,四个字节则 与0x80808080
进行一次&
运算,如果值为0,则8字节数据全部为标准ASCII编码,否则这些字符中含有其它非标准ASCII编码数据。
|
|
- 如果该字节数据编码非标准ASCII编码(超出127范围),检查是否符合UTF-8编码的其它码点规则:
UTF-8编码码点判断规则如下:
- UTF-8最多可用到6个字节,其有效bit数为31,而一般文字以及符号编码都用到了1-4字节;
- 0xC0(192), 0xC1(193), 0xF5—0xFF(245-255)不会出现在UTF8编码中;
- 首字节不会存在 0x80—0xBF(128-191)范围,而次字节范围必须在0x80—0xBF(128-191)范围内;
- 首字节值为 0xE0(224),次字节取值必须在 0xA0 - 0xBF(160-191)之间;
- 首字节值为 0xED(237),次字节取值必须在 0x80 - 0x9F(128-159)之间;
- 首字节值为 0xF0 (240),次字节取值必须在 0x90 - 0xBF(144-191)之间;
- 首字节值为 0xF4 (244),次字节取值必须在 0x80 - 0x8F(128-143)之间;
根据这些规则,Go语言定义出了次字节取值范围的 acceptRanges
变量以及一些常用的相关变量:
|
|
除了定义上述变量以外,Go还将编码值为【0-255】范围内的所有首字节编码根据字节长度以及次字节取值范围等信息进行进行分类, 这样可以根据UTF-8首字节就能很快确定该编码的字节长度以及次字节等信息。
首字节编码分为:xx、as、s1、s2、s3、s4、s5、s6、s7
共九类,用十六进制常量的高位和低位分别表示。
- 高位:“次字节取值范围列表” 的索引,如果高位是
F
则表示字符是单字节字符; - 低位:字符的编码长度,如果高位是 F 则低位表示单字节字符的状态:0(有效)、1(无效) 分类代码如下:
|
|
除了 utf8.ValidString 函数外,其实 utf8.Valid函数判断也类似。
二、Golang字符串(string)类型
2.1 Golang中的字符串简介
在Golang中,字符串是一种基本数据类型,它由一串Unicode字符(UTF-8)组成。
Golang中 string的底层是通过byte数组实现的(所以获取字符串长度是按照字节来的)。
中文字符在unicode下占2个字节,在utf-8编码下占3个字节(golang默认编码是UTF-8)。
字符串定义:
|
|
string 类型的零值为长度为零的字符串,即空字符串 “"。
Golang中的字符串值是不可变的,也就是说,一旦创建了一个此类型的值,就不可能再对它本身做任何修改。
Golang 中 字符串(string) 是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节),每个中文汉字占用三个字节。
字符串有两种表示方式:双引号("") 和 反引号(``),反引号也叫原意符号,反引号表示的字符串叫原意字符串。
双引号用于创建普通字符串,而反引号用于创建原始字符串(它们可以包含特殊字符和换行符等)。
如下代码:
|
|
Tips: 反引号 定义的字符,会以字符串的原生形式输出,包括换行符和特殊字符,可以防止攻击、输出源代码等效果。
Golang中一个字符串类型的值可以代表一个字符序列,这些字符必须是被Unicode编码规范支持的。
虽然从表象上来说是字符序列,但是在底层,一个字符串值却是由若干个字节来表现和存储的。
一个字符串(也可以说字符序列)会被Go语言用Unicode编码规范中的UTF-8编码格式编码为字节数组。
一个字符串值或者一个字符串类型的变量之上应用Go语言的内置函数 len() 函数将会得到存储它的那个字节数组的长度。
len("hello 世界")
获得的是 hello 世界 占用的内存空间为 12(字节)。
计算字符串的长度(字符个数):
- ASCII 字符串长度使用 内置 len() 函数获取,此时字符串占用的字节数和字符数相同;
- Unicode 字符串长度使用 utf8.RuneCountInString() 函数 或 使用
[]rune
对其进行强制转换后再使用 len()函数获取;
for 索引 / for range 遍历字符串:
- ASCII 字符串遍历直接使用 下标索引 或 for range 均可;
- Unicode 字符串遍历用 for range 或 使用
[]rune
对其进行强制转换后再使用 for 索引;
处理有中文(Unicode编码)的问题([]rune
)
|
|
字符串转 byte
与 byte
转字符串
|
|
字符串转二进制[]byte: var bytes = []byte("hello go)
, 强制转换为切片,用于将字符串写入二进制文件
2.2 Golang中string底层原理
当查看string类型的变量所占的空间大小时,会发现是16字节(64位机器)。
|
|
字符串(string) 是 Go 语言提供的一种基础数据类型,它是一个不可变的字符序列。
实际上,string 是一个结构体,在Golang 1.17及之前版本中定义的字符串底层结构为:
|
|
在 Golang 1.18 中引入的字符串底层数据结构,与 stringStruct 基本相同,但是将指向字符串数据的指针类型从 unsafe.Pointer
改为了 uintptr
,使得更方便地在不同平台上进行字符串内存地址的转换,定义为:
|
|
- Data:是一个指针,指向存储实际字符串的内存地址,该内存地址存储的值是一个[]byte类型切片,指针在64位机器下占8个字节;
- Len: 字符串的长度,64位机器下int占8个字节。与切片类似,在代码中可以使用len()函数获取这个值。注意,len存储实际的字节数,而非字符数,对于非单字节编码的字符,结果可能让人疑惑。
需要注意的是,stringStruct
和 StringHeader
虽然定义方式不同,但它们所表示的字符串底层结构是等价的。
- 在 Go 1.18 之前的版本中,可以通过
(*stringStruct)(unsafe.Pointer(&str))
将字符串转换为stringStruct
,然后进行底层操作; - 在 Go 1.18 及之后版本中,可以通过
(*StringHeader)(unsafe.Pointer(&str))
将字符串转换为StringHeader
,然后进行底层操作;
示例:
|
|
执行 go build -gcflags=-S main.go 查看 hello 字符串底层结构的内存存储的信息为:
|
|
从上述编译信息可以看出:
- 存储字符串的实际上是一片连续的内存空间;
- 如上信息
go:string."hello" SRODATA dupok size=5
,SRODATA
标识代表只读,意味着字符串会分配到只读的内存空间,是一个不可改变的字节序列,不可修改; - 由于只读的特性,相同字符串都被存储为同一个地址上,可以得出相同字符串面值常量通常对应同一个字符串常量;
2.3 字符串部分函数
1)不区分大小写的字符串比较函数
|
|
使用了 不区分大小写的EqualFold
函数比较字符串是否相同。
2)将字符串的字母进行大小写的转换的函数
|
|
3)按照指定的某个字符为分割标识,将一个字符串拆分成数组
|
|
4)将指定的子串替换成另一个字符串
|
|
5)将字符串左右的空格去掉
|
|
6)将字符串两边指定的字符串去掉
|
|
7)判断字符串是否以指定的字符串开头
|
|
8)判断字符串是否以指定的字符串结束
|
|
9)将指定的子串替换成另一个字符串
|
|