Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Go语言】:区分字符和字节,掌握字符串遍历 #36

Open
littlejoyo opened this issue Jun 14, 2020 · 0 comments
Open

【Go语言】:区分字符和字节,掌握字符串遍历 #36

littlejoyo opened this issue Jun 14, 2020 · 0 comments
Assignees
Labels
Go Go语言知识点归档

Comments

@littlejoyo
Copy link
Owner

个人博客:https://joyohub.com/

微信公众号:Joyo说

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

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

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

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

1.Go的byte和rune

Go的源码表示

// 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来输出

代码如下:

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

rune

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

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

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 字节流了。

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

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

输出结果:

�
你
AB
ABC

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

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

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])

输出结果:

第一个字符:你
第二个字符:好
第三个字符:,
输出字符的Unicode码:19990
输出字符:世

5.遍历字符串

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

5.1 下标遍历

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

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

例如,遍历 "Hello,世界"

s := "Hello,世界"

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

输出结果:

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类型使用下标遍历:

s := "Hello,世界"

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

输出结果:

H 的类型是 int32
e 的类型是 int32
l 的类型是 int32
l 的类型是 int32
o 的类型是 int32的类型是 int32
G 的类型是 int32
o 的类型是 int32
 的类型是 int32
 的类型是 int32

5.2 range遍历

s := "Hello,Go语言"

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

输出结果:

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修改字符串中的字节:

s := "Hello,Go语言"

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

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

输出结果:

HelloGo语言
Hello!��Go语言

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

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

s := "Hello,Go语言"

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

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

输出结果:

HelloGo语言
Hellogo语言

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

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

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:] // 丢弃已读取的字符
    }
}

输出结果:

H
e
l
l
oG
o

总结:

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

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

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

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

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

微信公众号

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

weixin-a

@littlejoyo littlejoyo added the Go Go语言知识点归档 label Jun 14, 2020
@littlejoyo littlejoyo self-assigned this Jun 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Go Go语言知识点归档
Projects
None yet
Development

No branches or pull requests

1 participant