惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
博客园 - 叶小钗
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
D
DataBreaches.Net
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
Netflix TechBlog - Medium
Microsoft Azure Blog
Microsoft Azure Blog
Microsoft Security Blog
Microsoft Security Blog
B
Blog
U
Unit 42
有赞技术团队
有赞技术团队
博客园 - 聂微东
GbyAI
GbyAI
宝玉的分享
宝玉的分享
F
Full Disclosure
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
MyScale Blog
MyScale Blog
Jina AI
Jina AI
Martin Fowler
Martin Fowler
IT之家
IT之家
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Docker
P
Proofpoint News Feed
A
About on SuperTechFans
I
InfoQ
博客园 - 【当耐特】
C
Check Point Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy & Cybersecurity Law Blog
T
Threat Research - Cisco Blogs
Y
Y Combinator Blog
Project Zero
Project Zero
WordPress大学
WordPress大学
小众软件
小众软件
AWS News Blog
AWS News Blog
博客园 - 司徒正美
T
The Exploit Database - CXSecurity.com
L
LINUX DO - 热门话题
I
Intezer
Engineering at Meta
Engineering at Meta
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks
T
Tenable Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
C
CERT Recently Published Vulnerability Notes

文章列表 on polarisxu

通过一个例子让你彻底掌握 Go 工作区模式 这道题正确率竟然只有 22% Go 开源项目推荐:一个简单的 Go 练手项目 go test 如何禁用缓存? Go1.18 快讯:constraints 包被移除标准库 跟着 Go 作者学泛型 「2022 版」轻松搞定 Go 开发环境 鹅厂小哥用行动反对加班:程序员加班能被终结吗? Go 为什么选择 Gopher 作为吉祥物? 本地如何配置多个 GitHub/Gitee 账号? 为 Java 开发者准备的 Go 教程 02:Java 有而 Go 无 GitHub 发现了 studygolang 项目依赖的漏洞 Typora 宣布收费后,这款开源 Markdown 编辑器火了 为 Java 开发者准备的 Go 教程 01:漫游了 我的 2021 年终总结 泛型版 singleflight:Go 中如何防止缓存击穿? 2021 年你写了多少代码?这个 Go 工具帮你统计 Go1.18 新特性:TryLock Go 泛型入门教程 程序员瑞士军刀:各种结构的转换工具 终于有 Go 版的 Elasticsearch 了 Go泛型系列:maps 包讲解 Go 1.18 中的 any 是什么? Go1.18 快讯:新的 IP 包 Go编程模式:详解函数式选项模式 Go泛型系列:slices 包讲解 Nginx 竟然也有 playground — Go 语言构建的 Go泛型系列:Go1.18 类型约束那些事 Go1.18 快讯:Module 工作区模式 Go1.18 快讯:新增的 Cut 函数太方便了 Go1.18 快讯:废弃了这个 API Go1.18 快讯:新增字符串 Clone API Go:如何获得项目根目录? Go 如何获取和设置环境变量 重磅变动:更快找到微信公众号里的Go文章 Go 这样设置版本号:我们的项目也可以 假期结束,推荐 2 本 manning 出的 Go 图书 Go Fiber 框架系列教程 04:测试应用 Go Fiber 框架系列教程 03:中间件 Go泛型系列:提前掌握Go泛型的基本使用 推荐一个 Go GUI 实战项目 Go Fiber 框架系列教程 02:详解相关 API 的使用 Go Fiber 框架系列教程 01: 和 Express 对比学习 Go Module:私有不合规库怎么解决引用问题 Go1.17 新特性:go get 变了 Go1.17 新特性:testing 包的相关变化 StackOverflow 上关于 Go select 死锁的问题 新书推荐:用 Gin 框架构建分布式应用 Go 官网要变天。。。 Gin 这是要成为 Go 官方框架? 扬眉吐气:刚刚,Go 已经默认支持泛型了 Go 1.17 新特性:Module 有哪些变化? Go 第三方库推荐:类型转换如此简单 担心密码提交到 GitHub?建议使用这个 Go 开源工具 Go 的时间格式化为什么是 2006-01-02 15:04:05? GitHub 为 Go 社区带来安全支持 Go开源项目推荐:500行代码确认请求时间花在哪 一道关于 len 函数的诡异 Go 面试题解析 厉害了我的 Go:推荐系统都有开源实现 Go1.17 新特性:新版构建约束 这个工具真好:看看你的Go项目依赖有无漏洞 网友很强大,发现了Go并发下载的Bug Rust 劝退系列 09:函数 Go项目实战:一步步构建一个并发文件下载器 这本 Go 新书挺期待的:100 个常见错误 Go 启用新的官方问答社区 这是要干嘛?!微软招 Go 编译器全职开发人员 Go1.17 快报之标准库越来越注重易用性 Go1.17 新特性之切片变数组 回顾 Go 官网的演变史 站长8年前的Go代码竟然进入大厂的项目里了 Go图书翻译:一个好消息,一个坏消息 不怕烂尾!决定组织翻译这本 Go 图书 Rust 劝退系列 08:模式匹配 一本花了2.5年写成的Go免费在线图书 被黑惨了:一句话,说明自己会 Go,咋整? Go1.16 中的新函数 signal.NotifyContext 怎么用? Go 1.17 新特性提前学之测试执行随机化 Rust 劝退系列 07:流程控制 官方的 Go 多版本管理:使用和原理 Rust新书:给你一个劝退的理由 周刊题解:常量表达式这个规则应该了解下 盘点那些使用 Go 语言的国外公司 编写了50万行Go代码是一种什么体验 Rust 劝退系列 06:常量 Uber 使用 Go 的规模这么大?!都自己定制的 Go 编译器了 Go Team Leader — rsc 大神新开源了一个库,增强模板功能 再一次看到了 Go 的节制:int128 类型要不要支持? Rust 劝退系列 05:复合数据类型 「卷」有理论依据:海勒姆定律—Go又是怎么卷的 Go 真的也可以进行 GUI 开发:还有这样的图书呢 Rust 劝退系列 04:基本数据类型 我的 Go 语言书单 Rust 劝退系列 03:变量 我又来推荐免费 Go 新书了:一本用 Go 讲架构的书 注释竟然还有特殊用途?一文解惑 //go:linkname 指令 这个功能,公众号、微信群会不会被玩坏? 一道 Go 闭包题,面试官说原来自己答错了:面别人也涨知识 Rust 劝退系列 02:第一个 Rust 程序 Rust 劝退系列 01:打造开发环境
Unicode 和 UTF-8 是什么关系?本文为你解惑
2021-01-21 · via 文章列表 on polarisxu

十几年前,国内 UTF-8 还不太流行,新手开发过程中,乱码问题是很常见的。记得我刚毕业那会,公司的系统是这样的:数据库 MySQL 使用的是 Latin-1 编码(ISO8859-1),程序源码使用的是 GBK,这样的情况,乱码真是满天飞~好在对数据库操作做了封装,编码问题在中间层处理了。那时候听到关于乱码问题的一个终极解决方案是:保证各个地方编码一致。

现如今,Unicode 相当普及,中文使用 UTF-8 编码虽然比 GBK 编码占用更多的空间,但一般都不会在乎。特别是使用了 Go 语言后,乱码问题更是没有了。在 Go 语言标准库中有 unicode 包,它下面还有 utf8 和 utf16 两个子包。也许有此疑问:又是 Unicode、又是 UTF-8、UTF-16,它们是什么关系?

本文试着为大家解决关于字符集和字符编码的困惑。

本文目录:

1、什么是字符集

字符集,从字面看,是多个字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。因此不同的字符组合在一起就可以认为是不同的字符集。当然不是瞎组合。国人常见字符集如下:

2、什么是字符编码

大家都知晓,计算机只认 0 和 1 组成的二进制数。所以整数通过进制转换转为二进制就可以被计算机处理。然而像文本、音频、视频等这样的信息,如何转为二进制被计算机处理呢?这就需要进行编码。

编码(encode)是把数据从一种形式转换为另外一种形式的过程,它是一套算法。解码(decode)就是编码的逆过程。

比如对于 ASCII 字符集中的字符 1,转换为二进制是 00110001,这就是一次编码;将 00110001 显示为 1,涉及到一次解码。

字符编码,顾名思义,是对字符进行编码,是字符和二进制数据之间转换的算法,它们之间必须一一对应,这是本文需要讨论的主题。一个二进制叫做位,8 位称为“字节”,根据计算一个字节一共可组合出 256(2 的 8 次方)种不同的状态。

关于字符集和字符编码,使用 ASCII 进行总结说明:

ASCII 字符集是字母、数字、标点符号以及控制符(回车、换行、退格)等组成的 128 个字符。

ASCII 字符编码是将这 128 个字符转换为计算机可识别的二进制数据的一套规则(算法)。

因此当我们说 ASCII 时,一般同时指 ASCII 字符集和 ASCII 字符编码。通常,字符集会同时定义一套同名的字符编码规则。然而万事都有特例,比如 Unicode 就只是代表字符集,对应的字符编码有多种,比如 UTF-8、UTF-16 等。

现在知道了 Unicode 只是代表字符集,它的编码规则是通过 UTF 系列定义的。

那 Unicode 是什么?官方 有这么一句话:

Unicode provides a unique number for every character, no matter what the platform, program, or language is.

即 Unicode 为每一个字符提供了一个唯一的数字编码(代号),叫做 Code Point(码点)。这里可以查找你要查询某个字符的码点(Code Point):https://www.unicode.org/cgi-bin/GetUnihanData.pl。注意这里的码点不是字符编码,只是字符集而已。

所以,Unicode 字符集一个最主要的工作就是维护这样一个表,可以把它想象成是一个数据库表,里面存储着每一个字符对应的唯一 ID(即 Code Point),统一用 U+XXXX 来表示(X 为 16 进制的字符,因为字符很多,并不一定所有的字符都是 4 个 16 进制数,比如笑哭的 Emoji 表情,Code Point 是 U+1F602),如 「徐」U+5F90、「新」U+65B0。现在这个数据表已经拥有 100 多万的字符,还在不停的更新。

为什么会出现 Unicode 呢?因为之前的编码方式,大多只考虑自己国家,不同国家的编码方式不同,导致使用中国 GBK 编码的文档,在日本用本地编码打开就乱码了。标准总是在混乱中诞生的,于是一些国际组织制定出了全球统一的编码格式。这就是 Unicode。

大家可能见到过 USC,这是 ISO 制定的一种计算机行业标准,和 Unicode 目的是一样的。他们双方意识到不应该出现两种不同的国际标准。因此你可以认为它们是一样的。

了解了 Unicode,那为什么会有 UTF-8、UTF-16?

UTF 系列

前面说了 Unicode 本身主要工作是维护一个表,它并没有规定一个字符到底用几个字节来表示,只规定了每个字符对应到唯一的码点(code point),码点可以从 0000 ~ 10FFFF 共 1114112 个值

那为什么 Unicode 不规定字符编码呢?如果将 Unicode 码点直接当编码规则会如何?其实是有的,这就是 UTF-32。因为计算机没法确认两字节到底是表示 1 个字符还是 2 个字符,因此 UTF-32 粗暴的取最大值,所有字符都按 4 字节编码。很显然,这在空间上是极浪费的(英文文档直接大 3 倍),因此 UTF-32 很少使用。

UTF 是 Unicode Transformation Format (Unicode 转换格式)的首字母缩写,专门解决 Unicode 的编码问题。根据编码规则的不同有 UTF-8、UTF-16 和 UTF-32 等几种具体的方案。

由于 UTF-32 浪费空间,使用不多,本文着重介绍 UTF-8 和 UTF-16。

4、UTF-8

UTF-8,也可写为 UTF8,是 Unicode 的一种变长编码方案。它完全兼容 ASCII,同时避免了 UTF-16 和 UTF-32 中的字节序等复杂性。UTF-8 能够被广泛接受,跟其完全兼容 ASCII 有很大关系。

为什么叫 -8?因为它将每个 Unicode 字符编码为 1~4 个八位元(octets)(八位元即一个字节),因此叫 -8。其中字节的个数取决于分配给 Unicode 字符的整数值。

当面对 4 个字节,UTF-8 如何知晓应该把它当做 1 个字符解析、还是 2 个?亦或是 4 个?这就是 UTF-8 设计巧妙之处。

  • 单字节可编码的 Unicode 范围:\u0000~\u007F(0~127)
  • 双字节可编码的 Unicode 范围:\u0080~\u07FF(128~2047)
  • 三字节可编码的 Unicode 范围:\u0800~\uFFFF(2048~65535)
  • 四字节可编码的 Unicode 范围:\u10000~\u1FFFFF(65536~2097151)

UTF-8 从首字节就可以判断一个字符的 UTF-8 编码有几个字节, 具体判断逻辑就是,根据首字节二进制的起始内容:

  • 如果以 0 开始,肯定是单字节编码;
  • 如果以 110 开头,肯定是双字节编码;
  • 如果以 1110 开头,肯定是三字节编码;
  • 如果以 11110 开头,肯定是四字节编码;

除首字节之外,其他字节也有规定,如下:

字节数 用来表示码点的位数 Unicode 十六进制码点范围 字节1 字节2 字节3 字节4
1 7 0000 0000 - 0000 007F 0xxxxxxx
2 11 0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
3 16 0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 21 0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

具体如何编码和解码呢?以中文“徐”为例,说明这个过程。

「徐」的 Unicode 码点是 0x5F90(二进制:101 1111 1001 0000),对照上面的表发现 0x5F90 位于第三行的范围,所以格式是 1110xxxx 10xxxxxx 10xxxxxx。接着从「徐」Unicode 码点的二进制数最后一位开始,按从右向左依次填充这个格式中的 x,多出的 x 用 0 补上。这样就得到了「徐」这个汉字的 UTF-8 编码:11100101 10111110 10010000,转成十六进制是 0xE5 0xBE 0x90。

解码的过程也十分简单:如果一个字节(编码的二进制表示)的第一位是 0 ,则说明这个字节对应一个字符;如果一个字节的第一位是 1,那么连续有多少个 1,就表示该字符占用多少个字节。还是以「徐」字为例,它的 UTF-8 编码的二进制表示是:11100101 10111110 10010000,根据规则,第一个字节的第一位是 1,且有三个 1,因此占用 3 个字节。根据上面表中第三行,将第一个字节开头的 1110 去掉,第二、第三个字节开头的 10 去掉,剩下的组合在一起,即:1011 11110 010000,这就是 0x5F90。

关于 UTF-8 的规范参考 rfc3629 ,这是 Ken Thompson 和 Rob Pike 等制定的,这两位也是 Go 语言的作者。

5、UTF-16

UTF-16 是 16-bit Unicode Transformation Format。有一点需要特别强调,它跟 UTF-8 一样,是可变长度的。代码点(Code Point)是用一个或两个 16 位代码单元编码的。至于网上有些资料说的 UTF-16 是固定 2 个字节长度编码,那其实不是 UTF-16,而是 UCS-2 (用于 2 字节通用字符集)。UTF-16 可以看做是它的父集。

这里涉及一个概念,简单介绍下。

基本多语言平面(BMP):UCS-2 只对这些字符进行了编码,一共能编码 65,536 个字符。后来不够用了,怎么办?IEEE 引入了 UCS-4,所有字符都用 4 字节编码,这太浪费空间了,因此出现了一个折中方案:UTF-16,即超出 U+FFFF 的部分使用 4 字节,这部分叫做辅助平面(SMP),码点范围 U+010000 到 U+10FFFF。

注意 UCS-2 已经过时了。

因为是变长的方案,就需要有办法标识到底是 2 个字节还是 4 个字节。上文讲解 UTF-8 时,判断首字节字节开头位即可。类似的,看如下一张表:

字节数 16 进制码点范围 16-bit code 1 16-bit code 2
2 U+0000 - U+D7FF xxxxxxxxxxxxxxxx
2 U+E000 - U+FFFF xxxxxxxxxxxxxxxx
4 U+00010000 - U+0010FFFF 110110xxxxxxxxxx 110111xxxxxxxxxx

使用 UTF-16 最知名的是 Java,JVM 内部使用的就是 UTF-16 编码。因为 UTF-16 不兼容 ASCII,因此网络传输一般不会使用它。

但 Java 的编码方式其实没那么简单。一般地 Java 中是这样的:(来源于 《Java 核心技术手册》 一书)

  • Java 源文件,可能使用本地编码(比如中国是 GB);
  • 类文件(class 文件),使用 modified UTF-8(即做了部分修改的 UTF-8);
  • 虚拟机,使用过 UTF-16

接下来,以汉字"?“为例,说明 UTF-16 编码方式是如何工作的。

汉字”?“的 Unicode 码点为 0x20BB7,该码点显然超出了基本平面的范围(0x0000 - 0xFFFF),因此需要使用四个字节表示。首先用 0x20BB7 - 0x10000 计算出超出的部分,然后将其用 20 个二进制位表示(不足前面补 0 ),结果为0001000010 1110110111。接着,将前 10 位映射到 U+D800 到 U+DBFF 之间,后 10 位映射到 U+DC00 到 U+DFFF 即可。U+D800 对应的二进制数为 1101100000000000,直接填充后面的 10 个二进制位即可,得到 1101100001000010,转成 16 进制数则为 0xD842。同理可得,低位为 0xDFB7。因此得出汉字”?“的 UTF-16 编码为 0xD842 0xDFB7

6、Go 中 Unicode 相关包

Go 语言让复杂的编码问题变得简单很多,极大的减轻了程序员的心智负担。为了方便对 unicode 字符串进行处理,Go 语言标准库提供三个包:unicode、unicode/utf8 和 unicode/utf16。

这里简单介绍下三个包的功能:

  • unicode:unicode 提供数据和函数来测试 Unicode 代码点(Code Point,用 rune 存储)的某些属性。
  • unicode/utf8:用于处理 UTF-8 编码的文本,提供一些常量和函数,包括在 rune(码点) 和 UTF-8 字节序列之间的转换。
  • unicode/utf16:函数比较少,主要是 UTF-16 序列的编码和解码。

具体函数不讲解了,大家可以看标准库文档或通过我的开源书阅读相关章节:https://github.com/polaris1119/The-Golang-Standard-Library-by-Example

着重介绍下 Go 中字符串的写法。

在 Go 语言中,字符串字面值有 4 种写法,比如「徐新华」可以这么写:

s1 := "徐新华"
s2 := "\u5F90\u65B0\u534E"
s3 := "\U00005F90\U000065B0\U0000534E"
s4 := "\xe5\xbe\x90\xe6\x96\xb0\xe5\x8d\x8e"

简单来生活就是 \u 紧跟四个十六进制数,\U 紧跟八个十六进制数。其中 \u 或 \U 代表后面是 Unicode 码点。而 \x 紧跟两个十六进制数,这些十六进制不是 Unicode 码点,而是 UTF-8 编码。

下面的代码有利于你的理解:

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := `徐新华`
	var (
		buf = make([]byte, 4)
		n   int
	)

	fmt.Println("字符\tUnicode码点\tUTF-8编码十六进制\tUTF-8编码二进制")
	for _, r := range s {
		n = utf8.EncodeRune(buf, r)
		fmt.Printf("%q\t%U\t\t%X\t\t%b\n", r, r, buf[:n], buf[:n])
	}
	
	s2 := "\u5F90\u65B0\u534E"
	s3 := "\U00005F90\U000065B0\U0000534E"
	s4 := "\xe5\xbe\x90\xe6\x96\xb0\xe5\x8d\x8e"
	
	fmt.Println(s2)
	fmt.Println(s3)
	fmt.Println(s4)
}

运行结果:

字符	Unicode码点	UTF-8编码十六进制	UTF-8编码二进制
'徐'	U+5F90		E5BE90		[11100101 10111110 10010000]
'新'	U+65B0		E696B0		[11100110 10010110 10110000]
'华'	U+534E		E58D8E		[11100101 10001101 10001110]
徐新华
徐新华
徐新华

此外,关于字符串其他方面的处理,比如编码转换等,可以到 https://pkg.go.dev/golang.org/x/text 里找。

7、大小端问题(Little endian 和 Big endian)

最后聊一下大小端的问题。

一个字符使用多字节存储时,涉及到哪个在前哪个在后。以汉字「徐」为例,Unicode 码点是 5F90,需要用两个字节存储,一个字节是5F,另一个字节是90。存储的时候,5F在前,90 在后,这就是 Big endian 方式;90在前,5F在后,这是 Little endian 方式。

这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

第一个字节在前,就是”大端方式”(Big endian),第二个字节在前就是”小端方式"(Little endian)。

那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用 FEFF 表示。这正好是两个字节,而且 FF 比 FE 大1。

如果一个文本文件的头两个字节是 FE FF,就表示该文件采用大端方式;如果头两个字节是 FF FE,就表示该文件采用小端方式。

但从上面关于 UTF-8 编码的说明可以看出,虽然 UTF-8 存在多字节表示一个字符的情况,但顺序是固定的,没有字节序的问题。Unix 系统下,UTF-8 没有任何前置字符,但 Windows 下记事本保存的 UTF-8 文件会带上 BOM(Byte Order Mark),即 EF BB BF 这三个字节。关于这一点,Unicode 之父 Rob Pike 明确说 UTF-8 不需要 BOM,所以一开始 Go 源文件是不允许有 BOM 的,否则编译不通过,不过现在已经可以有了。但建议还是别带 BOM。

UTF-8 带 BOM 说不是为了区分字节序,而是为了更方便的知晓这是一个 UTF-8 文件。

你可以通过 hexdump 工具查看文件开始的字符。

8、参考文献