From a75bee99eeb31c1f46b9d61fa457822d02a012af Mon Sep 17 00:00:00 2001 From: chai2010 Date: Fri, 18 Dec 2015 13:09:37 +0800 Subject: [PATCH] Update ch3-05-3.md --- ch3/ch3-05-3.md | 155 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/ch3/ch3-05-3.md b/ch3/ch3-05-3.md index 8998fa0..de3edb9 100644 --- a/ch3/ch3-05-3.md +++ b/ch3/ch3-05-3.md @@ -1,4 +1,157 @@ ### 3.5.3. UTF-8 -TODO + +UTF8是一个将Unicode码点编码为字节序列的变长编码. UTF8编码由Go语言之父 Ken Thompson 和 Rob Pike 共同发明, 现在已经是Unicode的标准. UTF8使用1到4个字节来表示每个Unicode码点符号, ASCII部分字符只使用1个字节, 常用字符部分使用2或3个字节. 每个符号编码后第一个字节的高端bit位用于表示总共有多少个字节. 如果第一个字节的高端bit为0, 则表示对应7bit的ASCII字符, 每个字符一个字节, 和传统的ASCII编码兼容. 如果第一个字节的高端bit是110, 则说明需要2个字节; 后续的每个高端bit都以10开头. 更大的Unicode码点也是采用类似的策略处理. + +``` +0xxxxxx runes 0-127 (ASCII) +11xxxxx 10xxxxxx 128-2047 (values <128 unused) +110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused) +1110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused) +``` + +变长的编码无法直接通过索引来访问第n个字符, 但是UTF8获得了很多额外的优点. 首先UTF8编码比较紧凑, 兼容ASCII, 并且可以自动同步: 它可以通过向前回朔最多2个字节就能确定当前字符编码的开始字节的位置. 它也是一个前缀编码, 所以当从左向右解码时不会有任何歧义也并不需要向前查看. 没有任何字符的编码是其它字符编码的子串, 或是其它编码序列的字串, 因此搜索一个字符时只要搜索它的字节编码序列即可, 不用担心前后的上下文会对搜索结果产生干扰. 同时UTF8编码的顺序和Unicode码点的顺序一致, 因此可以直接排序UTF8编码序列. 同业也没有嵌入的NUL(0)字节, 可以很好地兼容那些使用NUL作为字符串结尾的编程语言. + +Go的源文件采用UTF8编码, 并且Go处理UTF8编码的文本也很出色. unicode 包提供了诸多处理 rune 字符相关功能的函数函数(区分字母和数组, 或者是字母的大写和小写转换等), unicode/utf8 包了提供了rune 字符序列的UTF8编码和解码的功能. + +有很多Unicode字符很难直接从键盘输入, 并且很多字符有着相似的结构; 有一些甚至是不可见的字符. Go字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符. 有两种形式, \uhhhh 对应16bit的码点值, \Uhhhhhhhh 对应32bit的码点值, 其中h是一个十六进制数字; 一般很少需要使用32bit的形式. 每一个对应码点的UTF8编码. 例如: 下面的字母串面值都表示相同的值: + +``` +"世界" +"\xe4\xb8\x96\xe7\x95\x8c" +"\u4e16\u754c" +"\U00004e16\U0000754c" +``` + +上面三个转义序列为第一个字符串提供替代写法, 但是它们的值都是相同的. + +Unicode转义也可以使用在rune字符中. 下面三个字符是等价的: + +``` +'世' '\u4e16' '\U00004e16' +``` + +对于小于256码点值可以写在一个十六进制转义字节中, 例如 '\x41' 对应 'A' 字符, 但是对于更大的码点则必须使用 \u 或 \U 转义形式. 因此, '\xe4\xb8\x96' 并不是一个合法的rune字符, 虽然这三个字节对应一个有效的UTF8编码的码点. + +得意于UTF8优良的设计, 诸多字符串操作都不需要解码. 我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀: + +```Go +func HasPrefix(s, prefix string) bool { + return len(s) >= len(prefix) && s[:len(prefix)] == prefix +} +``` + +或者是后缀测试: + +```Go +func HasSuffix(s, suffix string) bool { + return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix +} +``` + +或者是包含子串测试: + +```Go +func Contains(s, substr string) bool { + for i := 0; i < len(s); i++ { + if HasPrefix(s[i:], substr) { + return true + } + } + return false +} +``` + +对于UTF8编码后文本的处理和原始的字节处理逻辑一样. 但是对应很多其它编码则并不是这样的. (上面的函数都来自 strings 字符串处理包, 虽然它们的实现包含了一个用哈希技术优化的 Contains 实现.) + +另以方面, 如果我们真的关心每个Unicode字符, 我们可以使用其它机制. 考虑前面的第一个例子中的字符串, 它包混合了中西两种字符. 图3.5展示了它的内存表示形式. 字符串包含13个字节, 以UTF8形式编码, 但是只对应9个Unicode字符: + +```Go +import "unicode/utf8" + +s := "Hello, 世界" +fmt.Println(len(s)) // "13" +fmt.Println(utf8.RuneCountInString(s)) // "9" +``` + +为了处理这些真实的字符, 我们需要一个UTF8解码器. unicode/utf8 包提供了实现, 我们可以这样使用: + +```Go +for i := 0; i < len(s); { + r, size := utf8.DecodeRuneInString(s[i:]) + fmt.Printf("%d\t%c\n", i, r) + i += size +} +``` + +每一次调用 DecodeRuneInString 函数都返回一个 r 和 长度, r 对应字符本身, 长度对应r采用UTF8编码后的字节数目. 长度可以用于更新第i个字符在字符串中的字节索引位置. 但是这种方式是笨拙的, 我们需要更简洁的语法. 幸运的是, Go的range循环在处理字符串的时候, 会自动隐式解码UTF8字符串. 下面的循环运行如图3.5所示; 需要注意的是对于非ASCII, 索引更新的步长超过1个字节. + +![](../images/ch3-05.png) + +```Go +for i, r := range "Hello, 世界" { + fmt.Printf("%d\t%q\t%d\n", i, r, r) +} +``` + +我们可以使用一个简单的循环来统计字符串中字符的数目, 像这样: + +```Go +n := 0 +for _, _ = range s { + n++ +} +``` + +想其它形式的循环那样, 我们可以忽略不需要的变量: + +```Go +n := 0 +for range s { + n++ +} +``` + +或者我们可以直接调用 utf8.RuneCountInString(s) 函数. + +正如我们前面提到了, 文本字符串采用UTF8编码只是一种惯例,但是对于循环的真正字符串并不是一个惯例, 这是正确的. 如果用于循环的字符串只是一个普通的二进制数据, 或者是含有错误编码的UTF8数据, 将会发送什么? + +每一个UTF8字符解码, 不管是显示地调用 utf8.DecodeRuneInString 解码或在 range 循环中隐式地解码, 如果遇到一个错误的输入字节, 将生成一个特别的Unicode字符 '\uFFFD', 在印刷中这个符号通常是一个黑色六角或钻石形状, 里面包含一个白色的问号(?). 当程序遇到这样的一个字符, 通常是一个信号, 说明输入并不是一个完美没有错误的的UTF8编码字符串. + +UTF8作为交换格式是非常方便的, 但是在程序内部采用rune类型可能更方便, 因为rune大小一致, 支持数组索引和方便切割. + +string 接受到 []rune 的转换, 可以将一个UTF8编码的字符串解码为Unicode字符序列: + +```Go +// "program" in Japanese katakana +s := "プログラム" +fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0" +r := []rune(s) +fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]" +``` + +(在第一个Printf中的 `% x` 参数用于在每个十六进制数字前插入一个空格.) + +如果是将一个 []rune 类型的Unicode字符切片或数组转为string, 则对它们进行UTF8编码: + +```Go +fmt.Println(string(r)) // "プログラム" +``` + +将一个整数转型为字符串意思是生成整数作为Unicode码点的UTF8编码的字符串: + +```Go +fmt.Println(string(65)) // "A", not "65" +fmt.Println(string(0x4eac)) // "京" +``` + +如果对应码点的字符是无效的, 则用'\uFFFD'无效字符作为替换: + +```Go +fmt.Println(string(1234567)) // "(?)" +``` + + + +