携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第30天,点击查看活动详情
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
本章节,我们要介绍的是标准库 unicode/utf8 。每当我们需要在 utf-8 字节序列与 rune 字符之间进行转换时,就会使用到它。
在上一篇文章中,我们分析了 utf-8 库的常量以及合法性检验函数,并随文介绍了一些 unicode 的相关概念和知识(替换字符、代理项)。本篇文章,我们继续分析 utf-8 的其他函数。
备注:本系列文章使用的是 go 1.19 源码:
函数
RuneCount(p []byte) int
该函数返回 p 中的字符数,错误和不完整的编码会被当做单字节的字符。
整个函数其实和 Valid 函数后面部分是相同的,这里就不放代码了。它只是将逻辑改为:每循环一次字符数加一,然后遇到错误时跳过一个字节并继续下一次循环。
RuneCountInString(s string) (n int) 的代码和它相同。
RuneLen(r rune) int
该函数返回字符编码为 utf-8 需要的字节数。
func RuneLen(r rune) int {
switch {
case r < 0:
return -1
case r <= rune1Max: // 1<<7 - 1
return 1
case r <= rune2Max: // 1<<11 - 1
return 2
case surrogateMin <= r && r <= surrogateMax:
return -1
case r <= rune3Max: // 1<<16 - 1
return 3
case r <= MaxRune:
return 4
}
return -1
}
函数本身比较简单,这里主要普及一下 utf-8 的编码规则。
-
对于单字节的字符,字节的第一位为0,后面7位是字符的编码值,值和ASCII码是相同的,所以utf-8可以兼容ASCII
-
对于
n(n>1)字节的字符,第一个字节的前n位为1,n+1位为0,第2-n个字节的前两位均为10,剩下的位就是该字符的码值。
所以单字节最大7位,双字节最大11位(16-3-2),三字节最大16位(24-4-2*2),四字节最大位数是unicode字符集编码范围的最大值 0x10FFFF,并没有占满(32-5-2*3)=21位。
AppendRune(p []byte, r rune) []byte
该函数将字符 r 追加进 p 中,如果字符超出范围,则替换成 0xFFFD。
if uint32(r) <= rune1Max {
return append(p, byte(r))
}
对于单字节字符,直接追加;多字节字符,调用 appendRuneNonASCII(p []byte, r rune) []byte 函数追加。
switch i := uint32(r); {
case i <= rune2Max:
return append(p, t2|byte(r>>6), tx|byte(r)&maskx)
case i > MaxRune, surrogateMin <= i && i <= surrogateMax:
r = RuneError
fallthrough
case i <= rune3Max:
return append(p, t3|byte(r>>12), tx|byte(r>>6)&maskx, tx|byte(r)&maskx)
default:
return append(p, t4|byte(r>>18), tx|byte(r>>12)&maskx, tx|byte(r>>6)&maskx, tx|byte(r)&maskx)
}
t1 = 0b00000000
tx = 0b10000000
t2 = 0b11000000
t3 = 0b11100000
t4 = 0b11110000
t5 = 0b11111000
maskx = 0b00111111
mask2 = 0b00011111
mask3 = 0b00001111
mask4 = 0b00000111
t 和 mask 是一些二进制码,例如和t2做或运算是将前两位设为1,和maskx做与运算是将前两位设为0,与运算的优先级比或运算高。
EncodeRune(p []byte, r rune) int
该函数是将字符 r 写入 p 中,并返回写入的字节数。
这里的代码就是将上面 AppendRune 代码的每个 case 中的语句拆开并一个字节一个字节地写入到 p 中。其中,会通过在 case 开头写 _ = p[3] 这样的语句去消除后面代码的边界检测。
DecodeRune(p []byte) (r rune, size int)
该函数从 p 中解码出第一个 utf-8 编码,并返回字符和它的字节数。如果 p 为空,则返回 (RuneError, 0)。如果编码无效,则返回 (RuneError, 1)。
当 utf-8 编码不正确,编码的字符超出范围,或者不是该字符值的最短可能的 utf-8 编码的时候,编码无效。不执行任何其他的验证。
n := len(p)
if n < 1 {
return RuneError, 0
}
p0 := p[0]
x := first[p0]
if x >= as {
mask := rune(x) << 31 >> 31
return rune(p[0])&^mask | RuneError&mask, 1
}
第一个值只有两种可能:
-
xx=0xF1:大于as,此时是无效的编码,mask=0xFFFF,最后结果是RuneError -
as=0xF0:等于as,此时是ASCII码,mask=0x0000,最后结果是rune(p[0])
这段代码通过位运算巧妙地将两种情况融合在一起,减少了一个分支。
sz := int(x & 7)
accept := acceptRanges[x>>4]
if n < sz {
return RuneError, 1
}
b1 := p[1]
if b1 < accept.lo || accept.hi < b1 {
return RuneError, 1
}
if sz <= 2 {
return rune(p0&mask2)<<6 | rune(b1&maskx), 2
}
判断第二个字节是否在合法的范围内,然后通过与 EncodeRune 相反的位运算获取到字符值。
第三个和第四个字节也是同理。
DecodeRuneInString(s string) (r rune, size int) 的代码和它相同。
DecodeLastRune(p []byte) (r rune, size int)
该函数从 p 中解码出最后一个 utf-8 编码,并返回字符和它的字节数。其余的和上一个函数相同。
end := len(p)
if end == 0 {
return RuneError, 0
}
start := end - 1
r = rune(p[start])
if r < RuneSelf {
return r, 1
}
先判断空切片以及单字节字符,直接返回对应的结果。
lim := end - UTFMax
if lim < 0 {
lim = 0
}
for start--; start >= lim; start-- {
if RuneStart(p[start]) {
break
}
}
if start < 0 {
start = 0
}
为了避免向后遍历一长串无效的 utf-8 的 o(n^2) 行为,这里最多只往后遍历四个字节。通过 RuneStart 判断字节是否可能是某个 utf-8 字符的首字节。
r, size = DecodeRune(p[start:end])
if start+size != end {
return RuneError, 1
}
return r, size
通过 DecodeRune 解码出字符和字节数,并判断解码是否成功,返回相应的结果。
DecodeLastRuneInString(s string) (r rune, size int) 和它的代码相同。
总结
本篇文章,我们分析完了所有的 unicode/utf8 标准库的内容,并普及了 utf-8 的编码知识。整个 utf8 库的代码中有很多巧妙的设计以及对位运算的灵活应用,值得我们好好借鉴学习。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿