第10章:字符是怎么存储的——Unicode 和 Unicode/UTF8
第10章:字符是怎么存储的——Unicode 和 Unicode/UTF8
“程序员的三大错觉:1. 字符串长度就是字符数;2. 一个字符就是一个字节;3. emoji 不过是彩色的 ASCII。”
本章将无情粉碎你的第三大错觉,顺便让你认清前两个。
10.1 Unicode/UTF8 包解决什么问题:程序不只处理英文字母,还要处理中文、日文、emoji
想象一下,你的程序只认识英文字母。它看到 Hello 会很开心,看到 你好 就傻眼了——“这是什么鬼符号?”
这就是 Unicode 要解决的问题:给世界上所有字符分配一个唯一编号,无论它是英文、汉字、日文假名,还是那个让你在群里社死的 😂。
而 UTF-8 则是把这个编号存储到计算机里的方式。Go 语言从诞生之日起就选择 UTF-8 作为字符串的编码方式,这是一个让很多语言羡慕嫉妒恨的决定。
专业词汇解释:
- Unicode:国际标准化组织(ISO)制定的一套字符集,为世界上每一种文字系统中的每一个字符分配唯一的编号(码点)。
- UTF-8:一种变长编码格式,用 1 到 4 个字节来存储 Unicode 码点。ASCII 字符用 1 字节,汉字用 3 字节,emoji 用 4 字节。
- 码点(Code Point):Unicode 中每个字符的唯一标识,格式为
U+XXXX,如 U+0041 代表字母 A。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| package main
import "fmt"
func main() {
// Go 字符串默认是 UTF-8 编码
s := "Hello 你好 👋"
fmt.Println("字符串内容:", s)
// 打印结果:字符串内容: Hello 你好 👋
// 字节长度 vs 字符长度
fmt.Println("字节长度:", len(s))
// 打印结果:字节长度: 16(emoji 占用 4 字节,中文每个占用 3 字节)
// 按字符遍历
count := 0
for range s {
count++
}
fmt.Println("字符数量:", count)
// 打印结果:字符数量: 9
}
|
10.2 Unicode/UTF8 核心原理:Go 字符串是 UTF-8 编码的字节序列,变长 1~4 字节
在 Go 的世界里,字符串是一个只读的字节切片。每个字符(rune)可能占用 1 到 4 个字节——这就是为什么你不能简单地用 len(s) 来获取"字符数"。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| package main
import "fmt"
func main() {
// 1 字节字符(ASCII)
a := "A"
fmt.Printf("'A' -> 字节数:%d, 字节内容:%v\n", len(a), []byte(a))
// 打印结果:'A' -> 字节数:1, 字节内容:[65]
// 2 字节字符(部分欧洲文字,如é)
e := "é"
fmt.Printf("'é' -> 字节数:%d, 字节内容:%v\n", len(e), []byte(e))
// 打印结果:'é' -> 字节数:2, 字节内容:[195 169]
// 3 字节字符(汉字)
z := "中"
fmt.Printf("'中' -> 字节数:%d, 字节内容:%v\n", len(z), []byte(z))
// 打印结果:'中' -> 字节数:3, 字节内容:[228 184 173]
// 4 字节字符(emoji)
emoji := "😀"
fmt.Printf("'😀' -> 字节数:%d, 字节内容:%v\n", len(emoji), []byte(emoji))
// 打印结果:'😀' -> 字节数:4, 字节内容:[240 159 152 128]
}
|
┌─────────────────────────────────────────────────────────────┐
│ UTF-8 编码长度规则 │
├─────────┬───────────────┬───────────────────────────────────┤
│ 字节数 │ 首字节格式 │ 有效码点范围 │
├─────────┼───────────────┼───────────────────────────────────┤
│ 1 字节 │ 0xxxxxxx │ U+0000 ~ U+007F (ASCII) │
│ 2 字节 │ 110xxxxx │ U+0080 ~ U+07FF │
│ 3 字节 │ 1110xxxx │ U+0800 ~ U+FFFF │
│ 4 字节 │ 11110xxx │ U+10000 ~ U+10FFFF │
└─────────┴───────────────┴───────────────────────────────────┘
专业词汇解释:
- 字节切片(Byte Slice):
[]byte,Go 中表示一段原始二进制数据。 - 符文(Rune):
int32 的别名,代表一个 Unicode 码点。
10.3 Unicode 基础:U+4E2D 代表汉字"中",U+0041 代表"A",U+1F600 代表"😀"
Unicode 码点就像是字符的身份证号。U+4E2D 就是汉字"中"的身份证号,U+1F600 是 emoji “😀“的身份证号。
记住这个规律:Unicode 码点范围从 U+0000 到 U+10FFFF,涵盖了人类所有文字符号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| package main
import "fmt"
func main() {
// 获取字符的 Unicode 码点
fmt.Printf("'A' 的码点:U+%04X\n", 'A')
// 打印结果:'A' 的码点:U+0041
fmt.Printf("'中' 的码点:U+%04X\n", '中')
// 打印结果:'中' 的码点:U+4E2D
fmt.Printf("'😀' 的码点:U+%04X\n", '😀')
// 打印结果:'😀' 的码点:U+1F600
// 从码点反向查询
// U+0041 = 65 (字符 'A')
fmt.Printf("U+0041 对应的字符:%c\n", 0x0041)
// 打印结果:U+0041 对应的字符:A
// U+4E2D = 20013 (汉字"中")
fmt.Printf("U+4E2D 对应的字符:%c\n", 0x4E2D)
// 打印结果:U+4E2D 对应的字符:中
// U+1F600 = 128512 (emoji "😀")
fmt.Printf("U+1F600 对应的字符:%c\n", 0x1F600)
// 打印结果:U+1F600 对应的字符:😀
}
|
10.4 UTF-8 编码原理:0xxxxxxx(1字节)、110xxxxx 10xxxxxx(2字节)、1110xxxx 10xxxxxx 10xxxxxx(3字节)、11110xxx 10xxxxxx 10xxxxxx 10xxxxxx(4字节)
UTF-8 的设计堪称优雅与效率的完美结合:
- 兼容 ASCII:ASCII 字符(0-127)保持单字节存储,高位为 0
- 自同步:任何字符的首字节都能让你知道它需要多少字节
- 无歧义:后续字节都以
10 开头,不会与首字节混淆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| package main
import "fmt"
func main() {
// 演示不同字节数字符的二进制结构
demonstrate := func(s string, desc string) {
fmt.Printf("\n%s '%s':\n", desc, s)
fmt.Printf(" 字节数:%d\n", len(s))
fmt.Printf(" 字节内容(十六进制):")
for _, b := range []byte(s) {
fmt.Printf("%02X ", b)
}
fmt.Printf("\n 字节内容(二进制):")
for _, b := range []byte(s) {
fmt.Printf("%08b ", b)
}
fmt.Printf("\n")
}
demonstrate("A", "1字节字符(ASCII)")
// 字节内容(二进制):01000001
// 规则:0xxxxxxx
demonstrate("é", "2字节字符")
// 字节内容(二进制):11000011 10101001
// 规则:110xxxxx 10xxxxxx
demonstrate("中", "3字节字符(汉字)")
// 字节内容(二进制):11100100 10111000 10101101
// 规则:1110xxxx 10xxxxxx 10xxxxxx
demonstrate("😀", "4字节字符(emoji)")
// 字节内容(二进制):11110000 10011111 10011000 10000000
// 规则:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
}
|
┌──────────────────────────────────────────────────────────────────┐
│ UTF-8 编码规则图解 │
├──────┬──────────────────────────────────────────────────────────┤
│ 1字节 │ 0xxxxxxx │
│ │ ↑ │
│ │ 固定0,表示单字节 │
├──────┼──────────────────────────────────────────────────────────┤
│ 2字节 │ 110xxxxx 10xxxxxx │
│ │ ↑↑ │
│ │ 11表示需要2字节,10表示是后续字节 │
├──────┼──────────────────────────────────────────────────────────┤
│ 3字节 │ 1110xxxx 10xxxxxx 10xxxxxx │
│ │ ↑↑↑ │
│ │ 111表示需要3字节,后两个10是后续字节 │
├──────┼──────────────────────────────────────────────────────────┤
│ 4字节 │ 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx │
│ │ ↑↑↑↑ │
│ │ 1111表示需要4字节,后三个10是后续字节 │
└──────┴──────────────────────────────────────────────────────────┘
专业词汇解释:
- 自同步(Self-Synchronizing):UTF-8 的一个优良特性,任何字符的起始字节都是唯一的,不会出现在其他字符的后续字节中。
- 首字节(Leading Byte):字符编码的第一个字节,决定了字符占用多少字节。
- 后续字节(Continuation Byte):字符编码的第 2、3、4 个字节,格式固定为
10xxxxxx。
10.5 UTF-16 代理对:为什么需要、UTF-16 用 2 字节,超过 65535 的字符需要代理对
UTF-16 曾经是 Windows、Java 和 JavaScript 的心头好——它用 2 字节表示所有字符。但这有个致命问题:2 字节只能表示 65536 个字符,而 Unicode 有超过 100 万个字符!
解决方案就是代理对(Surrogate Pair):用一对 2 字节的值来表示一个超出 BMP(基本多文种平面)的字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| package main
import "fmt"
func main() {
// 在 Go 中处理 UTF-16(需要编码转换)
// 注意:Go 原生字符串是 UTF-8,不是 UTF-16
// UTF-16 代理对原理
// emoji "😀" 的码点是 U+1F600
// 它超出了 BMP (U+0000 ~ U+FFFF),需要用代理对表示
// 代理对计算公式:
// 高代理 = 0xD800 + ((codePoint - 0x10000) >> 10)
// 低代理 = 0xDC00 + ((codePoint - 0x10000) & 0x3FF)
emoji := '😀'
codePoint := uint32(emoji)
highSurrogate := 0xD800 + ((codePoint-0x10000)>>10)
lowSurrogate := 0xDC00 + ((codePoint-0x10000)&0x3FF)
fmt.Printf("emoji '😀' 码点:U+%X\n", codePoint)
// 打印结果:emoji '😀' 码点:U+1F600
fmt.Printf("UTF-16 代理对:高代理 U+%X,低代理 U+%X\n", highSurrogate, lowSurrogate)
// 打印结果:UTF-16 代理对:高代理 U+D83D,低代理 U+DE00
// 在 UTF-16 中,"😀" 表示为两个字节:[D83D DE00]
// 而在 UTF-8 中,同一个字符表示为四个字节:[F0 9F 98 80]
}
|
┌─────────────────────────────────────────────────────────────┐
│ Unicode 平面与 UTF-16 代理对关系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ BMP (基本多文种平面) │
│ U+0000 ~ U+FFFF │
│ ├─ ASCII: U+0000 ~ U+007F (0xxxxxxx) │
│ ├─ 中文: U+4E00 ~ U+9FFF │
│ └─ 代理对范围: U+D800 ~ U+DFFF (保留给代理对!) │
│ │
│ 补充平面 (SMP) │
│ U+10000 ~ U+1FFFF │
│ └─ emoji、一些古文字等 │
│ 需要用 UTF-16 代理对表示 │
│ │
│ 编码对比: │
│ ┌─────────┬──────────┬──────────┐ │
│ │ 字符 │ UTF-8 │ UTF-16 │ │
│ ├─────────┼──────────┼──────────┤ │
│ │ 'A' │ 41 │ 0041 │ │
│ │ '中' │ E4B8AD │ 4E2D │ │
│ │ '😀' │ F09F9880 │ D83DDE00 │ ← 代理对! │
│ └─────────┴──────────┴──────────┘ │
└─────────────────────────────────────────────────────────────┘
专业词汇解释:
- BMP(Basic Multilingual Plane):Unicode 的基本多文种平面,U+0000 到 U+FFFF,包含世界上大部分常用字符。
- 代理对(Surrogate Pair):UTF-16 用于表示超出 BMP 范围字符的特殊机制,使用 U+D800-U+DFFF 这个保留范围来编码。
- SMP(Supplementary Multilingual Plane):Unicode 补充多文种平面,U+10000 到 U+1FFFF,包含 emoji、历史文字等。
10.6 Unicode 包:字符分类与判断
unicode 包是 Go 标准库的瑞士军刀,专门用于 Unicode 字符的分类、转换和判断。无论你是想判断一个字符是数字、字母还是标点,这个包都能帮你搞定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| package main
import (
"fmt"
"unicode"
)
func main() {
// unicode 包的核心功能:字符分类
// 这些函数都接受一个 rune(Unicode 码点)作为参数
testChar := func(r rune, name string) {
fmt.Printf("\n字符 '%c' (U+%04X):\n", r, r)
fmt.Printf(" IsDigit: %v (是数字?)\n", unicode.IsDigit(r))
fmt.Printf(" IsLetter: %v (是字母?)\n", unicode.IsLetter(r))
fmt.Printf(" IsUpper: %v (是大写?)\n", unicode.IsUpper(r))
fmt.Printf(" IsLower: %v (是小写?)\n", unicode.IsLower(r))
fmt.Printf(" IsSpace: %v (是空格?)\n", unicode.IsSpace(r))
fmt.Printf(" IsPunct: %v (是标点?)\n", unicode.IsPunct(r))
fmt.Printf(" IsPrint: %v (可打印?)\n", unicode.IsPrint(r))
fmt.Printf(" IsControl:%v (控制字符?)\n", unicode.IsControl(r))
}
testChar('7', "数字")
// 打印结果:IsDigit: true, IsLetter: false, ...
testChar('A', "大写字母")
// 打印结果:IsDigit: false, IsLetter: true, IsUpper: true, ...
testChar('a', "小写字母")
// 打印结果:IsDigit: false, IsLetter: true, IsUpper: false, IsLower: true, ...
testChar('中', "汉字")
// 打印结果:IsDigit: false, IsLetter: true, IsUpper: false, IsLower: false, ...
testChar(' ', "空格")
// 打印结果:IsSpace: true, ...
testChar('😂', "emoji")
// 打印结果:IsLetter: false, IsDigit: false, IsPunct: false, ...
}
|
专业词汇解释:
- 字符分类(Character Classification):根据 Unicode 标准,将字符划分为不同类别,如数字(Nd)、字母(Ll)、标点(P)等。
- 码点(Code Point):Unicode 中每个字符的唯一标识,即
rune 类型。
10.7 unicode.IsDigit:是否是数字
IsDigit 函数用于判断一个字符是否是十进制数字(Unicode 类别 Nd)。它不仅能识别 0-9,还能识别中文数字 〇一二三、阿拉伯数字 ١٢٣ 等各种Unicode 数字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| package main
import (
"fmt"
"unicode"
)
func main() {
// 测试各种"数字"
chars := []rune{
'0', // ASCII 数字
'9',
'①', // 上标数字
'½', // 分数(不是数字!)
'七', // 中文数字
'〇', // 中文零
'١', // 阿拉伯数字
}
fmt.Println("=== IsDigit 测试 ===")
for _, c := range chars {
fmt.Printf("'%c' (U+%04X): IsDigit = %v\n", c, c, unicode.IsDigit(c))
}
// 打印结果分析:
// '0': IsDigit = true(ASCII 数字)
// '9': IsDigit = true
// '①': IsDigit = true(上标数字)
// '½': IsDigit = false(分数不是数字,是数字符号)
// '七': IsDigit = true(中文数字)
// '〇': IsDigit = true(中文零)
// '١': IsDigit = true(阿拉伯数字)
}
|
专业词汇解释:
- Unicode 类别 Nd(Decimal Number):十进制数字字符,包括各种文字中的数字形式。
- 数字符号(Number Symbol):如分数
½、数字运算符,它们不是数字字符。
10.8 unicode.IsLetter:是否是字母
IsLetter 函数用于判断一个字符是否是字母。这里的"字母"定义比较宽泛,涵盖了各种文字系统的字母、汉字、日文假名等。简单来说,只要你认为它是"文字”,它大概率就是 true。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| package main
import (
"fmt"
"unicode"
)
func main() {
chars := []rune{
'A', // 英文字母
'a',
'中', // 汉字
'あ', // 日文平假名
'ア', // 日文片假名
'α', // 希腊字母
'①', // 上标数字(不是字母!)
' ', // 空格(不是字母!)
'_', // 下划线(不是字母!)
}
fmt.Println("=== IsLetter 测试 ===")
for _, c := range chars {
fmt.Printf("'%c' (U+%04X): IsLetter = %v\n", c, c, unicode.IsLetter(c))
}
// 打印结果:
// 'A': IsLetter = true
// 'a': IsLetter = true
// '中': IsLetter = true
// 'あ': IsLetter = true
// 'ア': IsLetter = true
// 'α': IsLetter = true
// '①': IsLetter = false
// ' ': IsLetter = false
// '_': IsLetter = false
}
|
10.9 unicode.IsUpper、unicode.IsLower:是否是大写/小写字母
这两个函数专门用于判断拉丁字母的大小写。它们只对有大小写之分的字符返回 true,对中文、日文等没有大小写概念的文字,返回 false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| package main
import (
"fmt"
"unicode"
)
func main() {
chars := []rune{
'A', // 大写英文字母
'a', // 小写英文字母
'Z',
'z',
'中', // 汉字(无大小写)
'α', // 希腊字母(有小写)
'Α', // 希腊字母(大写)
'①', // 数字(无大小写)
}
fmt.Println("=== IsUpper / IsLower 测试 ===")
for _, c := range chars {
fmt.Printf("'%c' (U+%04X): IsUpper = %-5v IsLower = %v\n",
c, c, unicode.IsUpper(c), unicode.IsLower(c))
}
// 打印结果:
// 'A': IsUpper = true IsLower = false
// 'a': IsUpper = false IsLower = true
// 'Z': IsUpper = true IsLower = false
// 'z': IsUpper = false IsLower = true
// '中': IsUpper = false IsLower = false
// 'α': IsUpper = false IsLower = true
// 'Α': IsUpper = true IsLower = false
// '①': IsUpper = false IsLower = false
}
|
10.10 unicode.IsSpace:哪些字符算空格
IsSpace 函数会识别所有 Unicode 中的空白字符,不仅仅是常见的空格键产生的空格,还包括 Tab、换行、回车以及各种语言中的特殊空白字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| package main
import (
"fmt"
"unicode"
)
func main() {
chars := []struct {
r rune
name string
}{
{' ', "普通空格",},
{'\t', "Tab制表符",},
{'\n', "换行符",},
{'\r', "回车符",},
{'\u00A0', "不间断空格(NBSP)",},
{'\u3000', "全角空格(日文)",},
{' ', "中文全角空格",},
{'\v', "垂直制表符",},
{'\f', "换页符",},
}
fmt.Println("=== IsSpace 测试 ===")
for _, item := range chars {
fmt.Printf("%s '%c' (U+%04X): IsSpace = %v\n",
item.name, item.r, item.r, unicode.IsSpace(item.r))
}
// 打印结果:
// 普通空格 ' ': IsSpace = true
// Tab制表符: IsSpace = true
// 换行符: IsSpace = true
// 回车符: IsSpace = true
// 不间断空格: IsSpace = true
// 全角空格: IsSpace = true
// 中文全角空格: IsSpace = true
// 垂直制表符: IsSpace = true
// 换页符: IsSpace = true
}
|
专业词汇解释:
- NBSP(No-Break Space):
U+00A0,不间断空格,用于防止行首出现孤立字符。 - 全角空格(Ideographic Space):
U+3000,宽度等于一个汉字的空格,常见于日文排版。
10.11 unicode.IsControl:是否是控制字符
控制字符是那些不显示在屏幕上,但对文本处理有特殊意义的字符。比如换行符 \n、回车符 \r、Tab \t 等。IsControl 函数帮助你识别它们。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| package main
import (
"fmt"
"unicode"
)
func main() {
chars := []struct {
r rune
name string
}{
{'\n', "换行符 LF"},
{'\r', "回车符 CR"},
{'\t', "Tab制表符"},
{'\0', "空字符 NUL"},
{' ', "普通空格(不是控制字符!)"},
{'a', "字母 'a'(不是控制字符!)"},
{'中', "汉字(不是控制字符!)"},
{'\x1B', "ESC 转义符"},
{'\a', "响铃符 BEL"},
}
fmt.Println("=== IsControl 测试 ===")
for _, item := range chars {
fmt.Printf("%s (U+%04X): IsControl = %v\n",
item.name, item.r, unicode.IsControl(item.r))
}
// 打印结果:
// 换行符 LF (U+000A): IsControl = true
// 回车符 CR (U+000D): IsControl = true
// Tab制表符 (U+0009): IsControl = true
// 空字符 NUL (U+0000): IsControl = true
// 普通空格: IsControl = false ← 空格是图形字符!
// 字母 'a': IsControl = false
// 汉字: IsControl = false
// ESC 转义符 (U+001B): IsControl = true
// 响铃符 BEL (U+0007): IsControl = true
}
|
专业词汇解释:
- 控制字符(Control Character):Unicode 类别 Cc(Control),包括 ASCII 控制字符(0-31 和 127),用于控制设备或文本格式。
- 图形字符(Graphic Character):包括字母、数字、标点、符号、空格等可见或可打印的字符。
10.12 unicode.IsPrint:是否是可打印字符
IsPrint 函数判断一个字符是否是可打印字符。可打印字符包括字母、数字、标点、符号和空格。不可打印的包括控制字符和尚未分配的码点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| package main
import (
"fmt"
"unicode"
)
func main() {
chars := []struct {
r rune
name string
}{
{' ', "空格",},
{'a', "字母",},
{'中', "汉字",},
{'!', "标点",},
{'\n', "换行符",},
{'\t', "Tab",},
{'\x00', "空字符",},
{'\uFFFD', "替换字符(显示为�)",},
}
fmt.Println("=== IsPrint 测试 ===")
for _, item := range chars {
fmt.Printf("%s (U+%04X): IsPrint = %v\n",
item.name, item.r, unicode.IsPrint(item.r))
}
// 打印结果:
// 空格: IsPrint = true
// 字母: IsPrint = true
// 汉字: IsPrint = true
// 标点: IsPrint = true
// 换行符: IsPrint = false
// Tab: IsPrint = false
// 空字符: IsPrint = false
// 替换字符: IsPrint = true(虽然显示为�,但它是合法字符)
}
|
10.13 unicode.IsPunct:是否是标点符号
IsPunct 函数用于判断一个字符是否是标点符号。这里的标点包括常见的英文标点(如 !?,.)以及各种语言中的引号、括号、连接号等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| package main
import (
"fmt"
"unicode"
)
func main() {
chars := []rune{
'.', ',', '!', '?', ';', ':', "'", '"',
'(', ')', '[', ']', '{', '}',
'-', '—', '…', '《', '》', '「', '」',
'/', '\\', '@', '#', '$', '%', '&',
'中', 'a', '1', ' ', '\n',
}
fmt.Println("=== IsPunct 测试 ===")
for _, c := range chars {
fmt.Printf("'%c' (U+%04X): IsPunct = %v\n", c, c, unicode.IsPunct(c))
}
// 打印结果:
// '.': IsPunct = true
// ',': IsPunct = true
// '!': IsPunct = true
// '《': IsPunct = true(中文书名号)
// '「': IsPunct = true(中文引号)
// '/': IsPunct = true
// '@': IsPunct = true
// '中': IsPunct = false(汉字不是标点)
// 'a': IsPunct = false(字母不是标点)
// '1': IsPunct = false(数字不是标点)
// ' ': IsPunct = false(空格不是标点)
// '\n': IsPunct = false(换行符不是标点)
}
|
10.14 unicode.ToUpper、unicode.ToLower:大小写转换
这两个函数用于拉丁字母的大小写转换。对于没有大小写概念的文字(如中文),它们会返回原字符不变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| package main
import (
"fmt"
"unicode"
)
func main() {
chars := []rune{
'a', 'z', 'A', 'Z',
'ö', // 德语字母
'α', // 希腊字母
'中', // 汉字
'1', // 数字
}
fmt.Println("=== 大小写转换测试 ===")
for _, c := range chars {
upper := unicode.ToUpper(c)
lower := unicode.ToLower(c)
fmt.Printf("'%c' (U+%04X) → ToUpper: '%c', ToLower: '%c'\n",
c, c, upper, lower)
}
// 打印结果:
// 'a' → ToUpper: 'A', ToLower: 'a'
// 'z' → ToUpper: 'Z', ToLower: 'z'
// 'A' → ToUpper: 'A', ToLower: 'a'
// 'Z' → ToUpper: 'Z', ToLower: 'z'
// 'ö' → ToUpper: 'Ö', ToLower: 'ö'(德语特殊字母转换)
// 'α' → ToUpper: 'Α', ToLower: 'α'(希腊字母转换)
// '中' → ToUpper: '中', ToLower: '中'(汉字不变)
// '1' → ToUpper: '1', ToLower: '1'(数字不变)
}
|
10.15 unicode.ToTitle:转换为首字母大写
ToTitle 函数将字符转换为标题形式。对于拉丁字母,这通常与大写形式相同;但对于某些特殊字母,标题形式可能与大写不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| package main
import (
"fmt"
"unicode"
)
func main() {
chars := []rune{
'a', 'h', 'o',
'ß', // 德语 sharp S(ß → SS)
'dz', // 拉丁字母 Dz 的单字符
'①', // 数字
'中', // 汉字
}
fmt.Println("=== ToTitle 测试 ===")
fmt.Println("字符 ToUpper ToTitle")
for _, c := range chars {
upper := unicode.ToUpper(c)
title := unicode.ToTitle(c)
fmt.Printf("'%c' (U+%04X) '%c' '%c'\n", c, c, upper, title)
}
// 打印结果:
// 'a' (U+0061) 'A' 'A'
// 'h' (U+0068) 'H' 'H'
// 'o' (U+006F) 'O' 'O'
// 'ß' (U+00DF) 'S' 'S'(大写/标题都变成 S)
// 'dz' (U+01F3) 'DZ' 'Dz'(标题形式与大写不同)
// '①' (U+2460) '①' '①'(数字不变)
// '中' (U+4E2D) '中' '中'(汉字不变)
}
|
专业词汇解释:
- 标题形式(Title Case):主要用于多字母单词的首字母大写形式。在 Unicode 中,某些特殊字母的标题形式可能与普通大写形式不同。
10.16 unicode.SimpleFold:Unicode 简单折叠
SimpleFold 是一个有趣的函数,它返回与给定字符等价的下一个字符,用于大小写不敏感的比较。这个"下一个"遵循 Unicode 的简单折叠规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| package main
import (
"fmt"
"unicode"
)
func main() {
// SimpleFold 返回下一个"折叠"字符
// 对于 ASCII 字母,它在大小写之间切换
fmt.Println("=== SimpleFold 测试 ===")
// ASCII 字母的折叠路径
fmt.Printf("'a' 的折叠: '%c'\n", unicode.SimpleFold('a'))
// 打印结果:'a' 的折叠: 'A'
fmt.Printf("'A' 的折叠: '%c'\n", unicode.SimpleFold('A'))
// 打印结果:'A' 的折叠: 'a'
// 多次调用 SimpleFold 会形成一个循环
fmt.Println("\n'a' 的折叠循环:")
r := 'a'
for i := 0; i < 10; i++ {
fmt.Printf(" %d: '%c' (U+%04X)\n", i, r, r)
r = unicode.SimpleFold(r)
}
// 打印结果:a → A → a → A → ...(在两个字符之间循环)
// 德语 ß 的折叠
fmt.Println("\n'ß' 的折叠循环:")
r = 'ß'
for i := 0; i < 5; i++ {
fmt.Printf(" %d: '%c' (U+%04X)\n", i, r, r)
r = unicode.SimpleFold(r)
}
// 打印结果:ß → S → s → ß(形成3个字符的循环)
}
|
专业词汇解释:
- Unicode 折叠(Unicode Folding):大小写不敏感比较时使用的字符映射规则。
SimpleFold 按照 Unicode 标准定义的顺序遍历这些等价字符。
10.17 unicode/utf8.DecodeRune:把 UTF-8 字节序列解码成 Unicode 码点
utf8.DecodeRune 是将 UTF-8 字节序列**转换为人话(Unicode 码点)**的函数。它接收一个字节切片的前 1~4 个字节,并返回对应的 rune 值和实际消费的字节数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// DecodeRune 接收 []byte,返回 (rune, int)
// 第二个返回值表示消费了多少字节
tests := [][]byte{
[]byte{'A'}, // 1 字节 ASCII
[]byte("中"), // 3 字节汉字
[]byte("😀"), // 4 字节 emoji
[]byte("Hello"), // 混合
}
fmt.Println("=== DecodeRune 测试 ===")
for _, data := range tests {
if len(data) == 0 {
continue
}
r, size := utf8.DecodeRune(data)
fmt.Printf("字节: %v (% X) → 字符: '%c' (U+%04X), 占用 %d 字节\n",
data, data, r, r, size)
}
// 打印结果:
// 字节: [65] (41) → 字符: 'A' (U+0041), 占用 1 字节
// 字节: [228 184 173] (E4 B8 AD) → 字符: '中' (U+4E2D), 占用 3 字节
// 字节: [240 159 152 128] (F0 9F 98 80) → 字符: '😀' (U+1F600), 占用 4 字节
// 字节: [72 101 108 108 111] → 字符: 'H' (U+0048), 占用 1 字节
// 部分字节序列
fmt.Println("\n=== 不完整字节序列 ===")
partial := []byte{0xE4} // "中" 的首字节(不完整)
r, size := utf8.DecodeRune(partial)
fmt.Printf("不完整序列 [% X]: rune='%c' (U+%X), size=%d\n",
partial, r, r, size)
// 打印结果:不完整序列 [E4]: rune='?' (U+FFFD), size=1
// 当输入不完整时,返回替换字符 U+FFFD
}
|
专业词汇解释:
- 替换字符(Replacement Character):
U+FFFD,通常显示为 �,用于表示无法解码的字节序列。 - 解码(Decode):将二进制数据(字节)转换为更高层次的表示(码点)。
10.18 unicode/utf8.EncodeRune:把 Unicode 码点编码成 UTF-8 字节序列
EncodeRune 是 DecodeRune 的逆操作:接收一个 Unicode 码点(rune),返回其 UTF-8 编码的字节序列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| package main
import (
"fmt"
"unicode/utf8"
)
func main() {
runes := []rune{
'A', // U+0041
'中', // U+4E2D
'😀', // U+1F600
0x80, // U+0080(第一个非 ASCII 字符)
0xFFFF, // U+FFFF(BMP 最后一个字符)
}
fmt.Println("=== EncodeRune 测试 ===")
for _, r := range runes {
var buf [utf8.UTFMax]byte
n := utf8.EncodeRune(buf[:], r)
fmt.Printf("U+%04X '%c' → % X (占用 %d 字节)\n", r, r, buf[:n], n)
}
// 打印结果:
// U+0041 'A' → [41] (占用 1 字节)
// U+4E2D '中' → [E4 B8 AD] (占用 3 字节)
// U+1F600 '😀' → [F0 9F 98 80] (占用 4 字节)
// U+0080 → [C2 80] (占用 2 字节)
// U+FFFF → [EF BF BF] (占用 3 字节)
// EncodeRune 会截断超出 UTFMax 的部分
fmt.Println("\n=== 超出有效码点范围 ===")
invalid := rune(0x110000) // 超出 Unicode 范围(最大 U+10FFFF)
var buf [utf8.UTFMax]byte
n := utf8.EncodeRune(buf[:], invalid)
fmt.Printf("U+%X → % X (占用 %d 字节)\n", invalid, buf[:n], n)
// 打印结果:U+110000 → [EF BF BD] (替换字符!)
}
|
10.19 unicode/utf8.RuneCountInString:按字符计算字符串长度
这是一个拯救无数新手程序员的函数。当你 len("你好") 得到 6 而不是 2 而抓狂时,RuneCountInString 就是你的救星。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| package main
import (
"fmt"
"unicode/utf8"
)
func main() {
strings := []string{
"Hello",
"你好",
"Hello 你好",
"😀😂😜",
"a中b英c文d",
}
fmt.Println("=== RuneCountInString 测试 ===")
for _, s := range strings {
byteLen := len(s)
runeCount := utf8.RuneCountInString(s)
fmt.Printf("%q: 字节长度=%d, 字符数量=%d\n", s, byteLen, runeCount)
}
// 打印结果:
// "Hello": 字节长度=5, 字符数量=5
// "你好": 字节长度=6, 字符数量=2
// "Hello 你好": 字节长度=12, 字符数量=8
// "😀😂😜": 字节长度=12, 字符数量=3
// "a中b英c文d": 字节长度=13, 字符数量=7
// 手动遍历对比
fmt.Println("\n=== 手动遍历 vs RuneCountInString ===")
s := "a中b"
count := 0
for range s {
count++
}
fmt.Printf("手动遍历 count=%d, RuneCountInString=%d\n",
count, utf8.RuneCountInString(s))
}
|
10.20 unicode/utf8.Valid、unicode/utf8.ValidString:验证是否是合法 UTF-8
当你从网络、文件或用户输入获取数据时,最好验证一下它是不是合法的 UTF-8。Valid 和 ValidString 就是你的UTF-8 质检员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// ValidString 接收字符串,返回是否合法
tests := []string{
"Hello",
"你好",
"😀",
"a\xffb", // 包含非法字节 FF
"a\xc0\x80b", // 包含非规范字节序列
string([]byte{0xF0, 0x9F, 0x98}), // 不完整的 emoji
}
fmt.Println("=== ValidString 测试 ===")
for _, s := range tests {
valid := utf8.ValidString(s)
fmt.Printf("%q: 合法=%v\n", s, valid)
}
// 打印结果:
// "Hello": 合法=true
// "你好": 合法=true
// "😀": 合法=true
// "a\xffb": 合法=false(非法字节)
// "a\xc0\x80b": 合法=false(非规范编码)
// "a\xe2\x98": 合法=false(不完整序列)
// Valid 接收字节切片
fmt.Println("\n=== Valid 测试(字节切片)===")
valid := utf8.Valid([]byte("Hello"))
fmt.Printf("[]byte(\"Hello\"): 合法=%v\n", valid)
invalid := []byte{0x80} // 孤立的延续字节
valid = utf8.Valid(invalid)
fmt.Printf("[]byte{0x80}: 合法=%v\n", valid)
}
|
专业词汇解释:
- 合法 UTF-8(Valid UTF-8):符合 UTF-8 编码规范的字节序列,包括正确的字节长度、首字节格式和后续字节格式。
- 非规范编码(Non-Canonical Encoding):虽然能解码出正确的字符,但不符合 UTF-8 的最优编码规则,如
0xC0 0x80 解码为 U+0000(应该用 0x00)。
10.21 unicode/utf8.DecodeLastRune、DecodeLastRuneInString:解码最后一个字符
DecodeLastRune 和 DecodeLastRuneInString 从字符串或字节切片的末尾开始解码,返回最后一个完整的 Unicode 码点。这在日志处理、路径解析等场景很有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| package main
import (
"fmt"
"unicode/utf8"
)
func main() {
tests := []string{
"Hello",
"你好",
"😀Hello",
"a中b英",
"😀",
"ab", // 只有 ASCII
}
fmt.Println("=== DecodeLastRuneInString 测试 ===")
for _, s := range tests {
r, size := utf8.DecodeLastRuneInString(s)
fmt.Printf("%q: 最后字符='%c' (U+%04X), 占用 %d 字节\n",
s, r, r, size)
}
// 打印结果:
// "Hello": 最后字符='o' (U+006F), 占用 1 字节
// "你好": 最后字符='好' (U+597D), 占用 3 字节
// "😀Hello": 最后字符='o' (U+006F), 占用 1 字节
// "a中b英": 最后字符='英' (U+82F1), 占用 3 字节
// "😀": 最后字符='😀' (U+1F600), 占用 4 字节
// "ab": 最后字符='b' (U+0062), 占用 1 字节
// 字节切片版本
fmt.Println("\n=== DecodeLastRune 测试(字节切片)===")
data := []byte("Hello 😀")
r, size := utf8.DecodeLastRune(data)
fmt.Printf("最后字符='%c' (U+%04X), 占用 %d 字节\n", r, r, size)
// 打印结果:最后字符='😀' (U+1F600), 占用 4 字节
}
|
10.22 unicode/utf8.RuneStart:判断字节是否是 UTF-8 字符的首字节
RuneStart 是一个低调但实用的函数,它告诉你一个字节是否可能是 UTF-8 字符的首字节。如果你在遍历字节时想知道当前位置是否是一个新字符的起点,这个函数就派上用场了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// 测试各种字节值
bytes := []byte{
0x00, 0x41, 0x7F, // ASCII 范围(0x00-0x7F)
0x80, 0xBF, // 延续字节范围
0xC0, 0xC1, // 非法首字节
0xC2, 0xDF, // 2字节首字节范围
0xE0, 0xEF, // 3字节首字节范围
0xF0, 0xF4, // 4字节首字节范围
0xF5, 0xFF, // 非法首字节(超出 Unicode 范围)
}
fmt.Println("=== RuneStart 测试 ===")
for _, b := range bytes {
isStart := utf8.RuneStart(b)
fmt.Printf("0x%02X (%08b): RuneStart=%v\n", b, b, isStart)
}
// 打印结果(关键点):
// 0x41 (01000001): RuneStart=true ← ASCII 字符的首字节
// 0x80 (10000000): RuneStart=false ← 延续字节!
// 0xBF (10111111): RuneStart=false ← 延续字节!
// 0xC2 (11000010): RuneStart=true ← 2字节字符的首字节
// 0xE0 (11100000): RuneStart=true ← 3字节字符的首字节
// 0xF0 (11110000): RuneStart=true ← 4字节字符的首字节
// 0xC0 (11000000): RuneStart=true ← 虽然是首字节格式,但是非法的
// 0xF5 (11110101): RuneStart=true ← 虽然是首字节格式,但超出 Unicode 范围
// 实际应用:检查字符串中的字符边界
fmt.Println("\n=== 实际应用:标记字符边界 ===")
s := "Hi中😀"
fmt.Printf("字符串: %q\n", s)
fmt.Printf("字节位置: ")
for i, b := range []byte(s) {
marker := " "
if utf8.RuneStart(b) {
marker = "|"
}
fmt.Printf("%s%02X", marker, b)
}
fmt.Println()
// 打印结果:|48|69|E4|B8|AD|F0|9F|98|80
// ↑ ↑ ↑ ↑ ↑
// H 中的首字节 emoji的首字节
}
|
10.23 遍历字符串的两种方式:for i, b := range []byte(s) vs for i, r := range s
这是 Go 语言中最容易踩坑的地方之一!range 遍历字符串时,索引是字节索引,值是 rune(码点);而 range 遍历字节切片时,索引还是字节索引,值是单个字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| package main
import "fmt"
func main() {
s := "Hi中😀"
fmt.Println("=== 遍历字符串(for i, r := range s)===")
for i, r := range s {
fmt.Printf("字节索引: %d, 字符: '%c', 码点: U+%04X\n", i, r, r)
}
// 打印结果:
// 字节索引: 0, 字符: 'H', 码点: U+0048
// 字节索引: 1, 字符: 'i', 码点: U+0069
// 字节索引: 2, 字符: '中', 码点: U+4E2D
// 字节索引: 5, 字符: '😀', 码点: U+1F600 ← 注意!索引跳过了"中"的3字节
fmt.Println("\n=== 遍历字节切片(for i, b := range []byte(s))===")
for i, b := range []byte(s) {
fmt.Printf("字节索引: %d, 字节值: 0x%02X ('%c')\n", i, b, b)
}
// 打印结果:
// 字节索引: 0, 字节值: 0x48 ('H')
// 字节索引: 1, 字节值: 0x69 ('i')
// 字节索引: 2, 字节值: 0xE4
// 字节索引: 3, 字节值: 0xB8
// 字节索引: 4, 字节值: 0xAD
// 字节索引: 5, 字节值: 0xF0
// 字节索引: 6, 字节值: 0x9F
// 字节索引: 7, 字节值: 0x98
// 字节索引: 8, 字节值: 0x80
// 错误演示:直接用字节索引访问字符串
fmt.Println("\n=== 危险操作:s[n] 是字节,不是字符 ===")
fmt.Printf("s[0] = '%c' (正确,是 'H')\n", s[0])
fmt.Printf("s[2] = 0x%02X (错误!这是'中'的首字节,不是字符)\n", s[2])
fmt.Printf("s[2:3] = '%c' (正确,用切片截取完整的 UTF-8 字符)\n", s[2])
// 如果一定要按字节访问后再解码:
r, _ := utf8.DecodeRune(s[2:])
fmt.Printf("utf8.DecodeRune(s[2:]) = '%c' (安全解码)\n", r)
}
|
┌─────────────────────────────────────────────────────────────┐
│ 字符串遍历方式对比 │
├──────────────────────┬──────────────────────────────────────┤
│ for i, r := range s │ for i, b := range []byte(s) │
├──────────────────────┼──────────────────────────────────────┤
│ i: 字节索引 │ i: 字节索引 │
│ r: rune (码点) │ b: byte (单字节) │
├──────────────────────┼──────────────────────────────────────┤
│ 自动处理多字节字符 │ 需要手动处理多字节字符 │
├──────────────────────┼──────────────────────────────────────┤
│ 推荐遍历方式 │ 需要按字节操作时使用 │
└──────────────────────┴──────────────────────────────────────┘
10.24 字符索引 vs 字节索引:s[3] 可能踩雷
想象一下,你想知道字符串第 3 个字符是什么,然后自信满满地写了 s[3]——恭喜你,你可能拿到的是一个 emoji 的中间字节,然后一脸问号地看着输出 ?。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// 这个字符串的字节布局:
// H i 中 😀
// 48 69 E4 B8 AD F0 9F 98 80
// 0 1 2 3 4 5 6 7 8
// ──────────── ────────────────
// "中" (3字节) "😀" (4字节)
s := "Hi中😀"
fmt.Println("=== 字节索引 vs 字符索引 ===")
fmt.Printf("字符串: %q\n", s)
fmt.Printf("字节长度: %d, 字符长度: %d\n\n", len(s), utf8.RuneCountInString(s))
fmt.Println("字节布局:")
for i, b := range []byte(s) {
fmt.Printf(" [%d] 0x%02X\n", i, b)
}
fmt.Println("\n=== 危险操作演示 ===")
fmt.Printf("s[0] = 0x%02X = '%c' (正确,ASCII 'H')\n", s[0], s[0])
fmt.Printf("s[1] = 0x%02X = '%c' (正确,ASCII 'i')\n", s[1], s[1])
fmt.Printf("s[2] = 0x%02X (错误!这是'中'的首字节,不是完整字符)\n", s[2])
fmt.Printf("s[3] = 0x%02X (错误!这是'中'的第二个字节)\n", s[3])
fmt.Printf("s[4] = 0x%02X (错误!这是'中'的第三个字节)\n", s[4])
fmt.Printf("s[5] = 0x%02X (正确!'😀'的首字节)\n", s[5])
// 正确做法:用 utf8.DecodeRune 解码
fmt.Println("\n=== 正确做法 ===")
// 获取第 N 个字符(0-indexed)
getNthChar := func(s string, n int) rune {
for i, r := range s {
if i == n {
return r
}
}
return 0
}
fmt.Printf("第 0 个字符: '%c'\n", getNthChar(s, 0)) // 'H'
fmt.Printf("第 1 个字符: '%c'\n", getNthChar(s, 1)) // 'i'
fmt.Printf("第 2 个字符: '%c'\n", getNthChar(s, 2)) // '中'
fmt.Printf("第 3 个字符: '%c'\n", getNthChar(s, 3)) // '😀'
}
|
专业词汇解释:
- 字节索引(Byte Index):字符串中每个字节的位置编号,从 0 开始。
- 字符索引(Character Index):字符串中每个 Unicode 字符的位置编号,从 0 开始。
- 多字节字符(Multi-byte Character):需要 2 个或更多字节表示的字符,如汉字(3字节)和 emoji(4字节)。
10.25 unicode/utf16 包:UTF-16 编码与解码
虽然 Go 原生使用 UTF-8,但有时候你不得不与 UTF-16 打交道——比如 Windows API、JavaScript 的 String 类型,或者某些网络协议。unicode/utf16 包就是你的翻译官。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| package main
import (
"fmt"
"unicode/utf16"
)
func main() {
// UTF-16 编码的核心函数
// 1. Encode:把 Unicode 码点序列转成 UTF-16
runes := []rune{'H', 'i', '中', '😀'}
utf16Arr := utf16.Encode(runes)
fmt.Println("=== utf16.Encode ===")
fmt.Printf("原始 rune: %c %c %c %c\n", runes[0], runes[1], runes[2], runes[3])
fmt.Printf("UTF-16 编码: % X\n", utf16Arr)
// 打印结果:UTF-16 编码: [48 69 4E2D D83D DE00]
// ↑↑↑↑
// 代理对!
// 2. Decode:把 UTF-16 转回 Unicode 码点
decoded := utf16.Decode(utf16Arr)
fmt.Println("\n=== utf16.Decode ===")
fmt.Printf("UTF-16: % X\n", utf16Arr)
fmt.Printf("解码 rune: %c %c %c %c\n", decoded[0], decoded[1], decoded[2], decoded[3])
// 打印结果:解码 rune: H i 中 😀
// 3. Append runes 到 []uint16 切片
fmt.Println("\n=== utf16.AppendRune ===")
buf := []uint16{'H', 'i'}
buf = utf16.AppendRune(buf, '中')
buf = utf16.AppendRune(buf, '😀')
fmt.Printf("追加后的 UTF-16: % X\n", buf)
// 打印结果:追加后的 UTF-16: [48 69 4E2D D83D DE00]
// 4. 判断是否需要代理对(是否是补充平面字符)
fmt.Println("\n=== 代理对判断 ===")
testRunes := []rune{'A', '中', '😀', '你'}
for _, r := range testRunes {
isSurrogate := r >= 0x10000
fmt.Printf("'%c' (U+%X): 需要代理对 = %v\n", r, r, isSurrogate)
}
// 打印结果:
// 'A': 需要代理对 = false
// '中': 需要代理对 = false
// '😀': 需要代理对 = true
// '你': 需要代理对 = false
}
|
┌─────────────────────────────────────────────────────────────┐
│ UTF-8 vs UTF-16 对比 │
├──────────────────────────┬──────────────────────────────────┤
│ UTF-8 │ UTF-16 │
├──────────────────────────┼──────────────────────────────────┤
│ 变长编码:1-4 字节 │ 基本定长:2 字节(代理对除外) │
├──────────────────────────┼──────────────────────────────────┤
│ ASCII 兼容:1 字节 │ ASCII 不兼容:'A' = 00 41 │
├──────────────────────────┼──────────────────────────────────┤
│ 自同步:首字节可判断长度 │ 无自同步:需要额外机制 │
├──────────────────────────┼──────────────────────────────────┤
│ Go 语言默认编码 │ Windows、Java、JavaScript 默认 │
├──────────────────────────┼──────────────────────────────────┤
│ '中' = E4 B8 AD (3字节) │ '中' = 4E 2D (2字节) │
│ '😀' = F0 9F 98 80 (4字节)│ '😀' = D8 3D DE 00 (代理对) │
└──────────────────────────┴──────────────────────────────────┘
专业词汇解释:
- UTF-16 编码:一种使用 2 字节表示大多数字符的编码格式。对于超出 BMP 的字符,使用代理对(4字节)表示。
- 代理对(Surrogate Pair):UTF-16 中用两个 16 位值(高代理和低代理)表示一个补充平面字符的机制。
- 补充平面(Supplementary Planes):Unicode 中 U+10000 及以上的字符,包括 emoji、历史文字等。
本章小结
恭喜你!你已经完成了 Unicode 和 UTF-8 的扫盲之旅。让我们来回顾一下今天学到的"生存技能”:
核心概念
| 概念 | 说明 |
|---|
| Unicode | 给世界上所有字符分配唯一编号(码点)的国际标准 |
| UTF-8 | Go 字符串的编码格式,1-4 字节变长 |
| 码点(Code Point) | Unicode 字符的唯一标识,如 U+4E2D |
| Rune | Go 中的 int32 类型,代表一个 Unicode 码点 |
常用函数速查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 判断类
unicode.IsDigit(r) // 是数字?
unicode.IsLetter(r) // 是字母?
unicode.IsUpper(r) // 是大写?
unicode.IsLower(r) // 是小写?
unicode.IsSpace(r) // 是空白?
unicode.IsPunct(r) // 是标点?
unicode.IsControl(r) // 是控制字符?
unicode.IsPrint(r) // 是可打印字符?
// 转换类
unicode.ToUpper(r) // 转大写
unicode.ToLower(r) // 转小写
unicode.ToTitle(r) // 转标题形式
unicode.SimpleFold(r) // 下一个等价字符
// UTF-8 编解码
utf8.DecodeRune(p) // 解码一个 rune
utf8.EncodeRune(p, r) // 编码一个 rune
utf8.RuneCountInString(s) // 按字符计数的字符串长度
utf8.ValidString(s) // 验证是否合法 UTF-8
utf8.DecodeLastRuneInString(s) // 解码最后一个 rune
utf8.RuneStart(b) // 是否是首字节?
// UTF-16
utf16.Encode(runes) // Unicode → UTF-16
utf16.Decode(utf16Arr) // UTF-16 → Unicode
|
避坑指南
- 永远不要用
len(s) 获取字符数——它返回的是字节数 - 永远不要用
s[n] 获取第 n 个字符——它返回的是第 n 个字节 - 遍历字符串用
for _, r := range s——而不是遍历字节切片 - 处理外部输入时验证 UTF-8——用
utf8.ValidString()
一句话总结
Go 的字符串是 UTF-8 编码的字节序列,字符数 ≠ 字节数,索引 ≠ 字符位置。
“懂得了这些,你终于可以自信地说:我知道 emoji 为什么占 4 个字节了。”