golang~UTF-8

本文结合unicode[2]标准和UTF-8[1](8-bit Unicode Transformation Format)的原理,分析golang标准库中的utf-8实现。

unicode

unicode是计算机科学领域的业界标准。它整理、编码了世界上大部分的文字系统,使得电脑可以用更为简单的方式来呈现和处理文字。

unicode与ISO 10646的通用字符集概念相对应。目前实际应用的统一码版本对应于UCS-2,使用16位的编码空间,每个字符占用2个字节。这样理论上一共最多可以表示216(即65536)个字符。基本满足各种语言的使用。实际上当前版本的统一码并未完全使用这16位编码,而是保留了大量空间以作为特殊使用或将来扩展。

上述16位统一码字符构成基本多文种平面

除了基本多文种平面,unicode还定义了额外16个辅助平面,两者合起来至少需要21位的编码空间。unicode编码选用4字节的编码空间,与UCS-4保持一致。

基本多文种平面的字符的二字节编码为U+hhhh,其中每个h代表一个十六进制数字,与UCS-2编码完全相同。而其对应的4字节UCS-4编码后为U+0000hhhh。,

UTF-8

unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如汉字的编码为U+4E25,转换成计算机能够存储传输的数据,至少需要2字节。而其他涉及编码位更多的汉字,则需要更大的存储空间。

unicode完全没有约束其编码形式的具体实现方式,这往往会导致不同系统中相同unicode字符对应的实现不同,为异构系统对接引入了潜在的巨大问题。基于此,为Unicode转换格式(Unicode Transformation Format,简称为UTF)应运而生,UTF-8(正是其中应用最广泛的一种,[1]截止到2019年11月, 在所有网页中,UTF-8编码应用率高达94.3%(其中一些仅是ASCII编码,因为它是UTF-8的子集),而在排名最高的1000个网页中占96%

UTF-8是一种针对Unicode的可变长度字符编码,最初由肯·汤普逊和罗布·派克提出,是当今互联网环境中应用最广泛的编码形式,采用1-4字节的字节序列对字符进行编码:

  • unicode字符的比特被分割为数个部分,并分配到UTF-8的字节串中较低的比特的位置。
  • 单字节编码,首比特位为0,表示一个单字节长度的ASCII码。
  • 多字节编码,首字节中开头连续为1的比特位决定了编码长度(例如:10xx yyyy表示长度为2字节),非首字节的比特位以10开头表示编码长度。

详情见下表UTF-8编码表:

UTF-8编码表
编码范围 UTF-8 注释
0x00-0x007F 0zzzzzzz z为UTF-8可使用编码范围,ASCII字符范围
0x80-0x7FF 110yyyyy 10zzzzzz y为首字节UTF-8编码可使用范围,z为第二字节可使用UTF-8编码范围
0x800-0xD7FF 1110xxxx 10yyyyyy 10zzzzzz x为首字节UTF-8编码可使用范围,y为第二字节可使用UTF-8编码范围,z为第三字节UTF-8编码可使用范围
0xD800-0xDFFF 不可使用 Unicode在范围D800-DFFF中不存在任何字符
0xE000-0xFFFF 1110xxxx 10yyyyyy 10zzzzzz x为首字节UTF-8编码可使用范围,y为第二字节可使用UTF-8编码范围,z为第三字节UTF-8编码可使用范围
0x10000-0x10FFFF 11110www 10xxxxxx 10yyyyyy 10zzzzzz w为首字节UTF-8编码可使用范围,x为第二字节可使用UTF-8编码范围,y为第三字节UTF-8编码可使用范围,z为第四字节UTF-8编码可使用范围
UTF-8无效编码段
首字节编码 编码长度 无效原因
[C0, C1] 2字节 编码范围是0x80-0x7FF,需要8-11位的编码。[C0, C1]只包含7位的编码段
[F5, F7] 4子节 码点超过10FFFF的头字节(RFC 3629规范)
[F8, FD] 5或6字节 5或6字节序列的头字节(RFC 3629规范)
[FE, FF] 7或8字节 UTF-8只能使用原来Unicode定义的区域0x00-0x10FFFF(RFC 3629规范),最多使用21位,不可能到7或8字节

UTF-8编码样例

汉字的编码为U+4E25

  • U+4E25属于<>UTF-8编码表<>中的三字节区域0x800-0xD7FF,可用编码区域1110xxxx 10yyyyyy 10zzzzzz

  • U+4E25转换为二进制100 1110 0010 0101,等价于1001 110001 00101

  • 这15位数按顺序放入xyz部分:1110 0100 1011 1000 1010 0101,16进制结果0xE4B8A5

UTF-8编码有效长度

UTF-8分段编码表
编码范围 编码位 首字节编码范围 次字节编码范围
0x00-0x007F 7 0x00-0x007F
0x80-0x7FF 11 0xC2-0xDF 0x80-0xBF
0x800-0xFFF 16 0xE0 0xA0-0xBF(编码范围从0x800开始)
0x1000-0xCFFF 16 0xE1-0xEC 0x80-0xBF
0xD000-0xD7FF 16 0xED 0x80-0x9F(0xD800-0xDFFF编码段不可使用)
0xE000-0xFFFF 16 0xEE-0xEF 0x80-0xBF
0x10000-0x3FFFF 21 0xF0 0x90-0xBF(编码范围从0x10000开始)
0x40000-0xFFFFF 21 0xF1-0xF2 0x80-0xBF
0x100000-0x10FFFF 21 0xF3 0x80-0x8F(编码范围到0x10FFFF截止)

go和UTF-8

go的rune

很多语言默认将string类型的数据编码为char(类型一般为uint8或者byte),可golang却不是,golang引入了特殊的字符类型rune[6]对所有的字符进行统一编码,下面通过一个例子来看:

// example
for _, c := range "H" {
    fmt.Println(reflect.TypeOf(c))
}
// 输出:int32

这个例子通过反射获取string每一个字符的种类,打印的结果出乎意料,既非uint8也非rune。通过阅读源码可以发现rune的类型正是打印的int32的别名:


…… // https://github.com/golang/go/blob/master/src/builtin/builtin.go#L92
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
……

从上可以发现,runebyte是完全不同的两种数据结构。然而在实际的应用过程中,笔者遇到了如下的写法:

first := "first"
fmt.Printf("%X\n", []rune(first))
fmt.Printf("%X\n", []byte(first))
// 输出结果 [] rune [66 69 72 73 74]
// 输出结果 [] byte 6669727374 

上面的例子似乎表明,[]rune[]byte在数值上是相同的。但是笔者又遇到了新的例子:

first := "社区"
fmt.Printf("%X\n", []rune(first))
fmt.Printf("%X\n", []byte(first))
// 输出结果 [] rune [793E 533A]
// 输出结果 [] byte E7A4BEE58CBA

通过阅读golang的源码,笔者发现,rune实质上就是unicode的码点,下面的例子可以完全说明这个问题:

first := "社区"
for index := 0; index < len(first); index++ {
    fmt.Printf("%v %X\n", reflect.TypeOf(first[index]), first[index])
}
size := 0
for i := 0; i < len(first); i += size {
    r, s := utf8.DecodeRuneInString(first[i:])
    size = s
    fmt.Printf("%X\n", r)
}
// 输出
// uint8 E7
// uint8 A4
// uint8 BE
// uint8 E5
// uint8 8C
// uint8 BA
// 793E
// 533A

go的UTF-8实现

UTF-8编码实际是unicode到可变长编码的映射关系,golang的标准库采用了一种优雅的实现方案——查表,从而避免了写大量的意大利面条式[4]的代码。

下面是标准库中首字节信息表,通过对首字节携带的UTF-8编码长度信息和次字节取值范围进行了二度编码s1、s2、s3、s4、s5、s6、s7(编码模型编码长度信息|(次字节长度信息<<4)),构建首字节信息表,从而提供了快速又优雅的信息查询操作。

……
    // locb 表示UTF-8编码非首字节的数值下限,hicbs表示UTF-8编码非首字节的数值上限
    locb = 0x80 // 1000 0000
    hicb = 0xBF // 1011 1111
    // 首字节+次字节信息编码段,用于绑定不同首字节对应的次字节信息
    xx = 0xF1 // invalid: size 1
    as = 0xF0 // ASCII: size 1
    s1 = 0x02 // accept 0, size 2 
    s2 = 0x13 // accept 1, size 3
    s3 = 0x03 // accept 0, size 3
    s4 = 0x23 // accept 2, size 3
    s5 = 0x34 // accept 3, size 4
    s6 = 0x04 // accept 0, size 4
    s7 = 0x44 // accept 4, size 4
……

var first = [256]uint8{
    //   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
    as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F
    as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F
    as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F
    as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F
    as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F
    as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F
    as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F
    as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F
    //   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
    xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F
    xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F
    xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF
    xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF
    xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF
    s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF
    s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF
    s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF
}

……
// 第二个字节的比特取值范围
type acceptRange struct {
    lo uint8 // 最小值
    hi uint8 // 最大值
}

// UTF-8标准设置的第二字节的取值范围
var acceptRanges = [...]acceptRange{
    0: {locb, hicb}, // 首字符有效范围和其他字符相同
    1: {0xA0, hicb}, // 0x1010 0000 - 0x1011 1111 0xA0-0xBF
    2: {locb, 0x9F}, // 0x1000 0000 - 0x1001 1111 0x80-0x9F
    3: {0x90, hicb}, // 0x1001 0000 - 0x1011 1111 0x90-0xBF
    4: {locb, 0x8F}, // 0x1000 0000 - 0x1000 1111 0x80-0x8F
}
……

下面将使用golang标准库的DecodeRuneEncodeRune为例来分析整个UTF-8的实现细节,源码传送门

DecodeRune使用了移位运算来优化执行效率。go语言包含integer << unsigned integerinteger >> unsigned integer两种运算符。移位运算在被移操作数位无符号数时,进行逻辑位移;否则,采用算数位移。

……
const (
    RuneError = '\uFFFD'     // the "error" Rune or "Unicode replacement character"
    RuneSelf  = 0x80         // characters below Runeself are represented as themselves in a single byte.
    MaxRune   = '\U0010FFFF' // Maximum valid Unicode code point.
    UTFMax    = 4            // maximum number of bytes of a UTF-8 encoded Unicode character.
)
……

func DecodeRune(p []byte) (r rune, size int) {
    n := len(p) 
    if n < 1 {
        return RuneError, 0
    }
    p0 := p[0]
    x := first[p0] // 查询首字节信息表
    if x >= as { // 如果编码信息as表示,内容为ASCII(0xF0)或者无效信息(0xF1)
        mask := rune(x) << 31 >> 31 // Create 0x0000 or 0xFFFF.
        return rune(p[0])&^mask | RuneError&mask, 1 // 通过对```xx```和```as```的特殊编码,可以使得,返回有效数据 or 返回RuneError
    }
    sz := x & 7 // 提取有效的UTF-8字节长度编码信息
    accept := acceptRanges[x>>4] // 提取有效字节范围
    if n < int(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 // 返回golang的UTF-8编码字符rune
    }
    b2 := p[2]
    if b2 < locb || hicb < b2 {
        return RuneError, 1
    }
    if sz == 3 {
        return rune(p0&mask3)<<12 | rune(b1&maskx)<<6 | rune(b2&maskx), 3
    }
    b3 := p[3]
    if b3 < locb || hicb < b3 {
        return RuneError, 1
    }
    return rune(p0&mask4)<<18 | rune(b1&maskx)<<12 | rune(b2&maskx)<<6 | rune(b3&maskx), 4
}

DecodeRune 是将UTF-8编码的字节信息解码为rune,它的反向操作是EncodeRune,输出编码后的byte流。

…… 
// 无效unicode范围
const (
    surrogateMin = 0xD800
    surrogateMax = 0xDFFF
)

……
const (
    ……
    rune1Max = 1<<7 - 1 // 单比特位rune上限
    rune2Max = 1<<11 - 1 // 双比特位rune上限
    rune3Max = 1<<16 - 1 // 三比特位rune上限
    ……
)
……
func EncodeRune(p []byte, r rune) int {
    // Negative values are erroneous. Making it unsigned addresses the problem.
    switch i := uint32(r); {
    case i <= rune1Max: // ASCII码直接返回
        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
    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
    }
}

go的UTF-8单元测试

utf-8的单元测试代码集中于repo

主要采用的流程架构:

  1. 在单元测试文件定义测试数据(输入,输出)
    • 特殊的边界数据
    • 输出错误结果的数据
    • 输出正确数据
    • 常量数据
  2. 针对各个export的函数构建独立的单元测试,以输入数据和相关函数为维度向量。

下面是单元测试数据源:

……
// 输入和输出的数据结构
type Utf8Map struct {
    r   rune// 输出的rune
    str string // 输入的string
}
// 普通例子的数据结构
var utf8map = []Utf8Map{
    {0x0000, "\x00"},
    {0x0001, "\x01"},
    {0x007e, "\x7e"},
	……
}
// 边界数据
var surrogateMap = []Utf8Map{
    {0xd800, "\xed\xa0\x80"}, // surrogate min decodes to (RuneError, 1)
    {0xdfff, "\xed\xbf\xbf"}, // surrogate max decodes to (RuneError, 1)
}
// 字符集合测试
var testStrings = []string{
    "",
    "abcd",
    "☺☻☹",
    "日a本b語ç日ð本Ê語þ日¥本¼語i日©",
    "日a本b語ç日ð本Ê語þ日¥本¼語i日©日a本b語ç日ð本Ê語þ日¥本¼語i日©日a本b語ç日ð本Ê語þ日¥本¼語i日©",
    "\x80\x80\x80\x80",
}
……

下面是一些测试函数:

……
// TestEncodeRune 测试编码函数
func TestEncodeRune(t *testing.T) {
    for _, m := range utf8map {
        b := []byte(m.str)
        var buf [10]byte
        n := EncodeRune(buf[0:], m.r)
        b1 := buf[0:n]
        if !bytes.Equal(b, b1) {//编码结果比较
            t.Errorf("EncodeRune(%#04x) = %q want %q", m.r, b1, b)
        }
    }
}

// TestDecodeRune 测试解码
func TestDecodeRune(t *testing.T) {
    for _, m := range utf8map {
        b := []byte(m.str)
        r, size := DecodeRune(b) // 解码byte流为rune
        if r != m.r || size != len(b) {
            t.Errorf("DecodeRune(%q) = %#04x, %d want %#04x, %d", b, r, size, m.r, len(b))
        }
        s := m.str
        r, size = DecodeRuneInString(s)
        if r != m.r || size != len(b) {
            t.Errorf("DecodeRuneInString(%q) = %#04x, %d want %#04x, %d", s, r, size, m.r, len(b))
        }
        ……
}

// TestDecodeSurrogateRune 测试DecodeRune上下界
func TestDecodeSurrogateRune(t *testing.T) {
    for _, m := range surrogateMap {
        b := []byte(m.str)
        r, size := DecodeRune(b)
        if r != RuneError || size != 1 {
            t.Errorf("DecodeRune(%q) = %x, %d want %x, %d", b, r, size, RuneError, 1)
        }
        s := m.str
        r, size = DecodeRuneInString(s)
        if r != RuneError || size != 1 {
            t.Errorf("DecodeRuneInString(%q) = %x, %d want %x, %d", b, r, size, RuneError, 1)
        }
    }
}
……

参考文献

[1] UTF-8编码:https://zh.wikipedia.org/wiki/UTF-8

[2] unicode编码:https://zh.wikipedia.org/wiki/Unicode

[3] 字符编码笔记:ASCII,Unicode 和 UTF-8:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

[4] 面条式代码:https://zh.wikipedia.org/zh-hans/面条式代码

[5] 移位操作:https://blog.csdn.net/u011070169/article/details/53894154

[6] 什么是rune:https://stackoverflow.com/questions/19310700/what-is-a-rune

posted @ 2022-04-30 15:19  code_wk  阅读(504)  评论(0)    收藏  举报