携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
从本章节开始,我们来看一看另一个常用的标准库 unicode/utf8 。每当我们需要在 utf-8 字节序列与 rune 字符之间进行转换时,就会使用到它。
备注:本系列文章使用的是 go 1.19 源码:
utf8
常量与类型
const (
RuneError = '\uFFFD'
RuneSelf = 0x80
MaxRune = '\U0010FFFF'
UTFMax = 4
)
首先是一些编码的基本数字,包括:
-
\uFFFD:unicode替换字符,在Unicode与其它字符集转换过程中,用来替换那些Unicode无法表示的文字, -
0x80:小于该字符的都是单字节字符(ASCII字符) -
\U0010FFFF:Unicode 代码点的最大值 -
4:一个 utf-8 字符的最大字节数
utf-8 编码下,英文占1字节,中文占3字节
\uFFFD也是 ‘锟斤拷’ 乱码的成因,因为它在utf-8下编码出来是 \xef\xbf\xbd,重复两遍后在GBK环境中显示就是 锟(0xEFBF)斤(0xBDEF)拷(0xBFBD)
const (
surrogateMin = 0xD800
surrogateMax = 0xDFFF
)
Surrogate 是代理项,是一种仅在 utf-16 中用来表示补充字符的方法,高代理项(前两个字节)的范围是 D800-DBFF ,低代理项(后两个字节)的范围是 DC00-DFFF,它们合起来作为一个代理项对能够表示一个20位的字符,Unicode 又将这20位的编码空间映射到 10000-10FFFF 上使整个编码可以连续表示。
当然,在utf-8编码中是不能使用代理项的。
type acceptRange struct {
lo uint8
hi uint8
}
var acceptRanges = [16]acceptRange{
0: {locb, hicb}, // 10000000-10111111
1: {0xA0, hicb}, // 10100000-10111111
2: {locb, 0x9F}, // 10000000-10011111
3: {0x90, hicb}, // 10010000-10111111
4: {locb, 0x8F}, // 10000000-10001111
}
acceptRange 给出了一个 utf-8 序列中第二个字节的有效值范围。
函数
函数主要分为编解码、添加、查找、计数和合法性检验这几类。
我们先来看看合法性检验的函数。
FullRune(p []byte) bool
FullRune 报告 p 中的字节是否以一个完整的 UTF-8 编码的字符开头。无效的编码会被视为完整的字符,因为它会转换为宽度减一的 \uFFFD。
n := len(p)
if n == 0 {
return false
}
x := first[p[0]]
if n >= int(x&7) {
return true
}
accept := acceptRanges[x>>4]
if n > 1 && (p[1] < accept.lo || accept.hi < p[1]) {
return true
} else if n > 2 && (p[2] < locb || hicb < p[2]) {
return true
}
return false
获取p的长度后,获取第一个字节的信息,包括它在 acceptRanges 中的索引,以及字符的长度。x&7 的意思是取信息中表示字符长度的后四位,如果p的长度大于等于它,则说明第一个字符完整。
如果上述判断不符合,则要么第一个字符不完整,要么是无效的编码,x>>4 获取到字符在 acceptRanges 中的索引,然后判断如果编码无效,返回 true。
如果编码有效,说明字符就是不完整,返回 false。
FullRuneInString(s string) bool 的代码和上面的完全相同,只是输入的是字符串。
RuneStart(b byte) bool
这个函数很短,就一行:func RuneStart(b byte) bool { return b&0xC0 != 0x80 }
它判断你传入的字节是否有可能是某个合法字符的首字节,判断的原理就是非首字节的前两位总是设为10。
ValidRune(r rune) bool
该函数报告 r 是否可以被合法地编码为 utf-8。
switch {
case 0 <= r && r < surrogateMin:
return true
case surrogateMax < r && r <= MaxRune:
return true
}
return false
只要不在代理项的范围内,同时在unicode的表示范围内,就是合法的。
Valid(p []byte) bool
该函数报告 p 是否完全由有效的 utf-8 编码字符组成。
p = p[:len(p):len(p)]
第一行,将 p 的容量设为和长度一样长,以避免在生成 p[8:] 的代码时重新计算容量,在长的ASCII字符串上的速度快20%。
for len(p) >= 8 {
first32 := uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24
second32 := uint32(p[4]) | uint32(p[5])<<8 | uint32(p[6])<<16 | uint32(p[7])<<24
if (first32|second32)&0x80808080 != 0 {
break
}
p = p[8:]
}
然后快速检查并(每次)跳过8个字节的ASCII码。
其中,通过组合两个32位的负载,可以使得本代码在不同的平台上使用。
如果字节与0x80进行与运算后不等于零,说明它大于127,超出了ASCII码表示范围,则 break。
n := len(p)
for i := 0; i < n; {
pi := p[i]
if pi < RuneSelf {
i++
continue
}
x := first[pi]
if x == xx {
return false
}
size := int(x & 7)
if i+size > n {
return false
}
accept := acceptRanges[x>>4]
if c := p[i+1]; c < accept.lo || accept.hi < c {
return false
} else if size == 2 {
} else if c := p[i+2]; c < locb || hicb < c {
return false
} else if size == 3 {
} else if c := p[i+3]; c < locb || hicb < c {
return false
}
i += size
}
return true
然后逐字节处理后面的部分。先跳过ASCII码,然后获取下一个字符的首字节的信息。xx是定义的常量,表示该值不能作为首字节。后面的逻辑和 FullRune 是相似的,只是这里无效的编码也要返回 false 了。另外,注意到这里 if-else 链的写法比较特殊:
if c := p[i+1]; c < accept.lo || accept.hi < c {
// ...
} else if size == 2 {
} else if ... {
// ...
}
为了能把代码写得比较紧凑, else if size==2 用于阻止 size==2 时继续向下判断,这样就保证了大长度能继续往下判断,而小长度到位置就停止了。
ValidString(s string) bool 除了没有第一句以外,剩下的代码与本函数相同。
总结
本篇文章中,我们分析了 utf-8 库的常量以及合法性检验函数,并随文介绍了一些 unicode 的相关概念和知识。下一篇文章,我们会继续分析 utf-8 是如何编解码的。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿