从源码学习 Go 标准库(三):utf-8(2)

173 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第30天,点击查看活动详情

前言

本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。

本章节,我们要介绍的是标准库 unicode/utf8 。每当我们需要在 utf-8 字节序列与 rune 字符之间进行转换时,就会使用到它。

上一篇文章中,我们分析了 utf-8 库的常量以及合法性检验函数,并随文介绍了一些 unicode 的相关概念和知识(替换字符、代理项)。本篇文章,我们继续分析 utf-8 的其他函数。

备注:本系列文章使用的是 go 1.19 源码:

github.com/golang/go/t…

函数

RuneCount(p []byte) int

该函数返回 p 中的字符数,错误和不完整的编码会被当做单字节的字符。

github.com/golang/go/b…

整个函数其实和 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

tmask 是一些二进制码,例如和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 库的代码中有很多巧妙的设计以及对位运算的灵活应用,值得我们好好借鉴学习。

最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿