目录
Go当中的字符串为何物?
- 不像Java中有String,以及Python中有str类型的数据结构,在Go语言中没有字符类型,字符只是整数的特殊用例,使用了byte(uint8)和rune(int32)作为别名
- Go的字符串默认使用UTF-8的编码来表示
byte和rune
byte和rune在Go内部分别为uint8以及int32的别名:
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
为什么只有uint8和int32这么“特殊”的拥有别名呢,这和unicode以及ASCII编码有很大的关系。并且在官方的写法可以看出二者分别是为了字节和字符的表示和转换的方便。
- byte 是 uint8 的别名,长度为 1 个字节,可以表示 2^8 = 255 个字符,在Go中用于表示 ASCII 字符
- rune 是 int32 的别名,长度为 4 个字节,可以表示 2^32个字符,用于表示以 UTF-8 编码的 Unicode 码点
ASCII、Unicode、UTF-8编码
ASCII 编码
计算机出现的时候最早只有ASCII编码,它使用了一个字节实现对英语、数字字符与二进制位之间的映射关系,使用一个字节(8位,256种可能)表示ASCII码绰绰有余。然而用于表示其他的语言文字是远远不够的,例如中文和法语。如果每一种语言都单独的提出一种编码,那么不统一的编码是十分恐怖的。
Unicode 编码
为了消除不统一编码所带的问题,进而引入了Unicode编码,Unicode使用4个字节表示信息,理论上一共可以表示 2^{32}=4294967296 种信息,可以概括世界上所有的符号。但是Unicode只是理论上可以解决ASCII编码空间不足的问题,没有指明应当如何储存Unicode编码。如果有一个4字节的Unicode编码,那么计算机怎么操作才是识别为一个Unicode编码,而不是4个ASCII编码呢?
一种简单的方法就是强制所有的字符都使用3或4字节的储存方式,不足的部分使用前置0填充,但是这样一来会使得储存英文字符的时候浪费大量的空间。
UTF-8 编码
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,而非采用定长的编码风格。
UTF-8的编码规则主要有两点:
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字符,UTF-8 编码和 ASCII 码是相同的。
- 对于n(n>1)字节的符号编码,第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
编码规则如下,其中X表示可以填充的位,其余已经固定 01 的位置不可填充:
Unicode 符号范围(16进制) | Utf-8编码方式(二进制) |
---|---|
0000 0000 ~ 0000 007F | 0xxxxxxx |
0000 0080 ~ 0000 07FF | 110xxxxx 10xxxxxx |
0000 0800 ~ 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 ~ 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
以中国的“国”字为例,它的Unicode编码为U+56FD
位于第三行的范围(0000 0800 ~ 0000 FFFF
),因此基本的格式为1110xxxx 10xxxxxx 10xxxxxx
,将56FD
转为二进制后为101011011111101
,一次向1110xxxx 10xxxxxx 10xxxxxx
中填充,最终可以得到:1110<101> 10<011011> 10<111101>
,其中<>
中为填充的二进制。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
Go当中的字符串
首先需要明确一点,Go当中的字符串使用双引号"String"
表示,字符则使用单引号表示'S'
表示。
Byte:='S'
String:="String"
fmt.Printf("Byte type is %T, String type is %T", Byte, String)
// Byte type is int32, String type is string
如果要表示 byte 类型的字符,可以使用 byte 关键字来声明字符变量的类型:直接输出的话会输出字符对应的ASCII码如果想要输出具体字符,需要格式化说明符%c来输出。与 byte 相同,想要声明 rune 类型的字符可以使用 rune 关键字指明。
为什么需要区分byte以及rune
在 Go 语言中,使用的是 UTF-8 编码,用 UTF-8 编码来存放一个 ASCII 字符依然只需要一个字节,而存放一个非 ASCII 字符,则需要 2个、3个、4个字节,它是不固定的。byte 占用一个字节,因此它可以用于表示 ASCII 字符。rune占用4个字节,可以用它表示 UTF-8 字符,因为UTF-8 是一种变长的编码方法,字符长度从 1 个字节到 4 个字节不等。
因此,在遍历ASCII字符的时候,直接使用下标获取元素是正确的,但是使用下标获取非ASCII字符的时候,获取的知识其中的一个字节,这对于非ASCII字符是没有意义的(PS: len()函数获取的就是字节数,一个中文占3字节,一个ASCII字符占用一个字节)。
S:="Hello,你好世界"
for i:=0;i<len(S);i++{
fmt.Println(S[:i])
}
// 最后会输出乱码
如果需要将byte转为可以截取的字符,那么可以先转为rune,再进行截取。
S := "Hello,你好世界" // 字符存储
for i := 0; i < len(S); i++ {
fmt.Println(S[:i])
}
Sconv := []rune(S)
for i := 0; i < len(Sconv); i++ {
fmt.Printf("Type is %T, value is %s \n", string(Sconv[i]), string(Sconv[i]))
} // 直接输出字符,H e l l o , 你 好 世 界
字符串的遍历
字符串遍历有两种方式,一种是下标遍历,一种是使用 range。
下标遍历
由于在 Go 语言中,字符串以 UTF-8 编码方式存储,使用 len() 函数获取字符串长度时,注意获取到的是 UTF-8 编码字符串的字节长度.通过下标索引获取值将会产生一个字节,所以,如果字符串中含有非ASCII编码字符,就会出现乱码,例如中文字符需要1~4不等的字节来表示。
如果需要对中文进行正确的储存和下标遍历,那么可以使用rune进行类型转换,[]rune()
之后进行下标遍历就可以正常输出中文字符
range遍历
如果一定要对字符串进行字符(非UTF-8码)的遍历,那么可以采用以下的range方式
S := "Hello,你好世界"
for _, R := range S {
r:=string(R)
fmt.Printf("Type is %T, value is %s \n", r, r)
}
总结一下如果要进行字符方式的遍历,可以先将字符串转为[]rune切片方式,随后对rune切片进行下标遍历,之后使用string(r)将rune转换为字符串类型;或者是进行range进行遍历,在循环里面使用string(r)转换,那么也可以转换为string类型
修改字符串
无论是Python还是Go当中,字符串都是不可变的数据类型,但是Go中可以进行间接修改,方法就是将字符串先缓存到可变区,例如[]byte或者[]rune切片,再进行修改,但是[]byte适用于ASCII码,修改字符可以达到目的,但是如果是非ASCII码,那么修改其中的一个字节达不到目的。
X:="Hello, 世界"
Bx:=[]byte(X)
Bx[8]='8'
fmt.Println(string(Bx))
// 输出 -- Hello, �8�界 // 乱码
使用rune切片进行修改,避免非ASCII码的问题:
X:="Hello, 世界"
Rx:=[]rune(X)
Rx[8]='8'
fmt.Println(len(Rx), string(Rx))
总结
- Go 语言中没有字符的概念,一个字符就是一堆字节,它可能是单个字节(ASCII 字符集),也有可能是多个字节(Unicode 字符集)
- byte 是 uint8 的别名,长度为 1 个字节,用于表示 ASCII 字符
- rune 则是 int32 的别名,长度为 4 个字节,用于表示以 UTF-8 编码的 Unicode 码点
- 字符串的截取是以字节为单位的,使用下标索引字符串只能获取到字节,获取字符需要进行准换
- 想要遍历 rune 类型的字符则使用 range 方法进行遍历。