0%

《Golang》Rune 原理

看看 rune 类型的定义

1
type rune = int32

rune 其实就是 4 个字节的 int32,表示字符根据 UTF-8 对应的数字,还没有根据 UTF-8 编码规则转换成字节

初步看来 rune 占用 4 个字节,就以为字符也占用 4 个字节,其实不一定,rune 字符最多占用 4 个字节

看下面实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"unsafe"
)

func main() {
a := "a你"
fmt.Println([]byte(a)) // [97 228 189 160]
for _, r := range a {
fmt.Println(unsafe.Sizeof(r)) // 这里都输出 4,指的是 rune 类型的大小,不是实际占用空间的大小
}
fmt.Println(len(a)) // a 字符串有两个字符,“a” 字符占用 1 个字节,“你” 字符占用 3 个字节,总共占用 4 个字节
}

从上面例子看,“a” 和 “你” 字符都是 rune 字符,一个占用 1 字节,一个占用 3 字节,并不是每个都占用 4 字节

这是为什么呢?

根据 UTF-8 编码,“你” 对应的数字是 20320(十六进制是 0x4F60),可以去 https://tool.chinaz.com/tools/utf-8.aspx 在线验证

0x4F60 在 0x0800-0xFFFF 之间,UTF-8 使用 3 字节模板:1110xxxx 10xxxxxx 10xxxxxx

0x4F60 二进制表示是:0100111101100000

放入模版就是:11100100 10111101 10100000,也就是三个字节 228 189 160

所以 rune 字符 “你” 根据 UTF-8 编码规则,编码成字节就成了 3 个字节 228 189 160,所以这个字符实际占用 3 个字节,而不是 4 个字节

UTF-8 编码解码的源码给大家看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
func decoderune(s string, k int) (r rune, pos int) {  // range string的时候会翻译成这个方法,第一个参数是迭代的string,第二个参数是index。这个方法的含义就是从k的位置读取一个rune字符,自动判断是2字节字符还是3字节还是4字节字符 
pos = k

if k >= len(s) {
return runeError, k + 1
}

s = s[k:]

switch {
case t2 <= s[0] && s[0] < t3:
// 0080-07FF two byte sequence
if len(s) > 1 && (locb <= s[1] && s[1] <= hicb) {
r = rune(s[0]&mask2)<<6 | rune(s[1]&maskx)
pos += 2
if rune1Max < r {
return
}
}
case t3 <= s[0] && s[0] < t4:
// 0800-FFFF three byte sequence
if len(s) > 2 && (locb <= s[1] && s[1] <= hicb) && (locb <= s[2] && s[2] <= hicb) {
r = rune(s[0]&mask3)<<12 | rune(s[1]&maskx)<<6 | rune(s[2]&maskx)
pos += 3
if rune2Max < r && !(surrogateMin <= r && r <= surrogateMax) {
return
}
}
case t4 <= s[0] && s[0] < t5:
// 10000-1FFFFF four byte sequence
if len(s) > 3 && (locb <= s[1] && s[1] <= hicb) && (locb <= s[2] && s[2] <= hicb) && (locb <= s[3] && s[3] <= hicb) {
r = rune(s[0]&mask4)<<18 | rune(s[1]&maskx)<<12 | rune(s[2]&maskx)<<6 | rune(s[3]&maskx)
pos += 4
if rune3Max < r && r <= maxRune {
return
}
}
}

return runeError, k + 1
}

// encoderune writes into p (which must be large enough) the UTF-8 encoding of the rune.
// It returns the number of bytes written.
func encoderune(p []byte, r rune) int {
// Negative values are erroneous. Making it unsigned addresses the problem.
switch i := uint32(r); {
case i <= rune1Max:
p[0] = byte(r)
return 1
case i <= rune2Max:
_ = p[1] // eliminate bounds checks
p[0] = t2 | byte(r>>6)
p[1] = tx | byte(r)&maskx
return 2
case i > maxRune, surrogateMin <= i && i <= surrogateMax:
r = runeError
fallthrough
case i <= rune3Max:
_ = p[2] // eliminate bounds checks
p[0] = t3 | byte(r>>12)
p[1] = tx | byte(r>>6)&maskx
p[2] = tx | byte(r)&maskx
return 3
default:
_ = p[3] // eliminate bounds checks
p[0] = t4 | byte(r>>18)
p[1] = tx | byte(r>>12)&maskx
p[2] = tx | byte(r>>6)&maskx
p[3] = tx | byte(r)&maskx
return 4
}
}

for range

for range 的特殊之处在于,是按照 UTF-8 编码去遍历字符串的,而不是常规的按照字节去遍历

看下面例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main() {
str := `fd哈`
for _, rune_ := range str {
fmt.Println(string(rune_)) // 依次输出 f d 哈
}
for i := 0; i < len(str); i++ {
fmt.Println(string(str[i])) // 输出 f d,后面三个乱码
}
}

通过前面的学习知道,string 类型底层是一个字节数组,第二种方式遍历其实是遍历这个底层的字节数组,包括 len(str) 得到的也是底层数组的长度,str[index] 得到的也是一个字节

这个字按照 UTF-8 编码是由 3 个字节表示的,所以会输出 3 个乱码,因为这 3 个字节单独转成 string 肯定是不正常的字




微信关注我,及时接收最新技术文章