抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >
  • 在Go语言中没有字符类型,字符只是整数的特殊用例,使用了byterune作为别名

  • Go的字符串使用了UTF-8的编码来表示,所以要明确好Unicode码和ASCII码的区别

  • 如何使用Go来遍历字符串、修改字符串,这也是一个常见的问题

Github issues:https://github.com/littlejoyo/Blog/issues/

1.Go的byte和rune

Go的源码表示

1
2
3
4
5
6
7
8
9
// 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

由上可知:

  • byteuint8 的别名,长度为 1 个字节,可以表示 2^8 = 255 个字符,在Go中用于表示 ASCII 字符

  • runeint32 的别名,长度为 4 个字节,可以表示 2^32个字符,用于表示以 UTF-8 编码的 Unicode 码点

2.ASCII、Unicode 和 UTF-8字符的区别

2.1 ASCII码

  • ASCII使用了一个字节实现对英语字符与二进制位之间的关系,做了统一规定

  • 每一个二进制位(bit)有01两种状态,又因为一个字节等于8位

  • 所以,八个二进制位就可以组合出2^8 = 256种状态,足够表示255个字符。

  • 英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。

比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示,因此各个国家开始定制各种可以全面表示各国语言的编码,造成了世界上存在着多种编码方式的现象,如果不选择正确的编码方式就会出现乱码。

2.2 Unicode

  • 可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失,所以Unicode码出现了。

  • Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母AinU+0041表示英语的大写字母AU+4E25表示汉字

  • Unicode编码也存在问题,因为Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

两个严重的问题:

  1. 第一个问题是,如何才能区别 Unicode 和 ASCII?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?

  2. 第二个问题是,会出现明显浪费存储的问题,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

这两个问题造成的结果就是:

  1. 出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode

  2. Unicode 在很长一段时间内无法推广,直到互联网的出现。

2.3 UTF-8

  • 互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。

  • UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8 的编码规则很简单,只有二条:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。

2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

下表总结了编码规则,字母x表示可用编码的位:

Unicode符号范围 (十六进制) 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
  • 如果一个字节的第一位是0,则这个字节单独就是一个字符;

  • 如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

以汉字严为例,演示如何实现 UTF-8 编码:

  • Unicode4E25100111000100101

  • 根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx

  • 然后,从的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5

参考链接:阮一峰:字符编码笔记:ASCII,Unicode 和 UTF-8

3.Go 语言中表示字符呢?

在 Go 语言中使用单引号包围来表示字符,例如 ‘a’。

byte

如果要表示 byte 类型的字符,可以使用 byte 关键字来指明字符变量的类型:

  • 直接输出的话会输出字符对应的ASCII码

  • 如果想要输出具体字符,需要格式化说明符%c来输出

代码如下:

1
2
3
var b byte = 'A'
fmt.Println(b) // 65 (A对应的ASCII码)
fmt.Printf("输出字符:%c", b) // 输出字符:A

rune

与 byte 相同,想要声明 rune 类型的字符可以使用 rune 关键字指明:

注:如果在声明一个字符变量时没有指明类型,Go 会默认它是 rune 类型

1
2
3
4
5
6
7
var b rune = 'A'
fmt.Println(b) // 65 (A对应的ASCII码)
fmt.Printf("输出字符:%c", b) // 输出字符:A

var c = 'B'
fmt.Println(c) // 66 (B对应的ASCII码)
fmt.Printf("输出字符:%c", c) // 输出字符:B

4.Go为什么需要两种类型?

在 Go 语言中,使用的是 UTF-8 编码,用 UTF-8 编码来存放一个 ASCII 字符依然只需要一个字节,而存放一个非 ASCII 字符,则需要 2个、3个、4个字节,它是不固定的。

  • byte 占用一个字节,因此它可以用于表示 ASCII 字符。

  • rune占用4个字节,可以用它表示 UTF-8 字符,因为UTF-8 是一种变长的编码方法,字符长度从 1 个字节到 4 个字节不等。

所以:

  • Go 中的字符串存放的是 UTF-8 编码,那么我们使用 s[i] 这样的下标方式获取到的内容就是 UTF-8 编码中的一个字节。

  • 对于非 ASCII 字符而言,这样的一个字节没有实际的意义,除非你想编码或解码 UTF-8 字节流。

  • 在 Go 语言中,已经有很多现成的方法来编码或解码 UTF-8 字节流了。

1
2
3
4
5
6
7
str := "你好,世界"
fmt.Println(str[:2]) // 输出乱码,因为截取了前两个字节
fmt.Println(str[:3]) // 输出「你」,一个中文字符由三个字节表示

s2 := "ABC"
fmt.Println(s2[:2]) // 字母占用一个字节,可以正确输出
fmt.Println(s2[:3]) // 字母占用一个字节,可以正确输出

输出结果:

1
2
3
4


AB
ABC

上面的问题,如何单独截取字符呢?

利用 []rune() 将字符串转为 Unicode 码点再进行截取,这样就无需考虑字符串中含有 UTF-8 字符的情况了

1
2
3
4
5
6
testString := "你好,世界"
fmt.Printf("第一个字符:%s\n", string([]rune(testString)[:1]))
fmt.Printf("第二个字符:%s\n", string([]rune(testString)[1:2]))
fmt.Printf("第三个字符:%s\n", string([]rune(testString)[2:3]))
fmt.Printf("输出字符的Unicode码:%v\n", []rune(testString)[3])
fmt.Printf("输出字符:%c\n", []rune(testString)[3])

输出结果:

1
2
3
4
5
第一个字符:你
第二个字符:好
第三个字符:,
输出字符的Unicode码:19990
输出字符:世

5.遍历字符串

字符串遍历有两种方式,一种是下标遍历,一种是使用 range。

5.1 下标遍历

  • 由于在 Go 语言中,字符串以 UTF-8 编码方式存储,使用 len() 函数获取字符串长度时,注意获取到的是 UTF-8 编码字符串的字节长度

  • 通过下标索引获取值将会产生一个字节,所以,如果字符串中含有非ASCII编码字符,就会出现乱码,例如中文字符需要1~4不等的字节来表示。

例如,遍历 “Hello,世界”

1
2
3
4
5
6
s := "Hello,世界"

for i := 0; i < len(s); i++ {
c := s[i]
fmt.Printf("%c 的类型是 %s\n", c, reflect.TypeOf(c))
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
H 的类型是 uint8
e 的类型是 uint8
l 的类型是 uint8
l 的类型是 uint8
o 的类型是 uint8
ï 的类型是 uint8
¼ 的类型是 uint8
Œ 的类型是 uint8
G 的类型是 uint8
o 的类型是 uint8
è 的类型是 uint8
¯ 的类型是 uint8
­ 的类型是 uint8
è 的类型是 uint8
¨ 的类型是 uint8
€ 的类型是 uint8
  • 逗号和中文字符就出现了乱码,因为属于非ASCII码,它们不能用ASCII码表示。

  • 另外说明了使用 s[i] 这样的下标方式获取到的内容就是 UTF-8 编码中的一个字节,无法使用byte类型表示,否则出现乱码

  • 如果想要正确表示中文字符,需要使用rune类型,才能对字符进行存储和表示

通过rune类型使用下标遍历:

1
2
3
4
5
6
7
s := "Hello,世界"

ss := []rune(s)
for i := 0; i < len(ss); i++ {
c := ss[i]
fmt.Printf("%c 的类型是 %s\n", c, reflect.TypeOf(c))
}

输出结果:

1
2
3
4
5
6
7
8
9
10
H 的类型是 int32
e 的类型是 int32
l 的类型是 int32
l 的类型是 int32
o 的类型是 int32
, 的类型是 int32
G 的类型是 int32
o 的类型是 int32
语 的类型是 int32
言 的类型是 int32

5.2 range遍历

1
2
3
4
5
s := "Hello,Go语言"

for _, c := range s {
fmt.Printf("%c 的类型是 %s\n", c, reflect.TypeOf(c))
}

输出结果:

1
2
3
4
5
6
7
8
9
10
H 的类型是 int32
e 的类型是 int32
l 的类型是 int32
l 的类型是 int32
o 的类型是 int32
, 的类型是 int32
G 的类型是 int32
o 的类型是 int32
语 的类型是 int32
言 的类型是 int32

6.字符串的修改

  • 在 Go 语言中,字符串的内容是不能修改的,也就是说,你不能用 s[i] 这种方式修改字符串中的 UTF-8 编码

  • 如果你一定要修改,那么你可以将字符串的内容复制到一个可写的缓冲区中,然后再进行修改。

  • 这样的缓冲区一般是 []byte[]rune。如果要对字符串中的字节进行修改,则转换为 []byte 格式,如果要对字符串中的字符进行修改,则转换为 []rune 格式,转换过程会自动复制数据。

使用用 []byte修改字符串中的字节:

1
2
3
4
5
6
7
s := "Hello,Go语言"

b := []byte(s) // 转换为 []byte,自动复制数据
b[5] = '!' // 修改 []byte

fmt.Printf("%s\n", s) // s 不能被修改,内容保持不变
fmt.Printf("%s\n", b) // 修改后的数据

输出结果:

1
2
Hello,Go语言
Hello!��Go语言

转换为[]byte修改的是字符串的字节,一般不常用,因为 []byte 代表的是字节数组,每个元素都是字节而不是字符。

使用用 []rune修改字符串中的字符:

1
2
3
4
5
6
7
8
s := "Hello,Go语言"

r := []rune(s) // 转换为 []rune,自动复制数据
r[6] = 'g' // 修改 []rune
r[7] = 'o' // 修改 []rune

fmt.Println(s) // s 不能被修改,内容保持不变
fmt.Println(string(r)) // 转换为字符串,又一次复制数据

输出结果:

1
2
Hello,Go语言
Hello,go语言

验证了无法直接在原来的字符串上进行字符的修改,需要再缓冲区额外修改。

在 []byte 中处理 Rune 字符(需要用到 utf8 包中的解码函数)

1
2
3
4
5
6
7
8
9
10
11
func main {
s := "Hello,Go语言"
b := []byte(s)

for len(b) > 0 {
r, n := utf8.DecodeRune(b) // 解码 b 中的第一个字符

fmt.Printf("%c\n", r) // 显示读出的字符
b = b[n:] // 丢弃已读取的字符
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
H
e
l
l
o

G
o


总结:

  • Go 语言中没有字符的概念,一个字符就是一堆字节,它可能是单个字节(ASCII 字符集),也有可能是多个字节(Unicode 字符集)

  • byte 是 uint8 的别名,长度为 1 个字节,用于表示 ASCII 字符

  • rune 则是 int32 的别名,长度为 4 个字节,用于表示以 UTF-8 编码的 Unicode 码点

  • 字符串的截取是以字节为单位的,使用下标索引字符串只能获取到字节,获取字符需要进行准换

  • 想要遍历 rune 类型的字符则使用 range 方法进行遍历。

微信公众号

扫一扫关注Joyo说公众号,共同学习和研究开发技术。

weixin-a

评论