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

708 阅读4分钟

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

前言

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

从本章节开始,我们来看一看另一个常用的标准库 unicode/utf8 。每当我们需要在 utf-8 字节序列与 rune 字符之间进行转换时,就会使用到它。

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

github.com/golang/go/t…

utf8

github.com/golang/go/b…

常量与类型

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 是如何编解码的。

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