概念梳理
ASCII 字符集
ASCII:ASCII 是美国标准信息交换码的缩写,是一种基于拉丁字母的字符编码标准。ASCII 使用 7 位二进制数(也就是 0-127)来表示每个字符,因此它最多可以表示 128 个不同的字符。ASCII 是所有现代字符编码方案的基础。
ASCII 字符编码其实只使用了 7 位,取值范围是 0-127。原因在于,ASCII 是在很早的计算机系统中设计的编码系统,那时的计算机系统资源非常有限。使用 7 位可以表示 128 个不同的字符,这已经足够表示所有的英文字母(大小写)、数字、常见的标点符号以及一些控制字符了。
然而,计算机中的数据通常以字节(8 位)为最小单位进行处理,所以每个 ASCII 字符通常还会有一个额外的 0 位,使得它们的实际编码在二进制表示下有 8 位。这个额外的位在 ASCII 中并没有被使用,所以有些系统会使用这个额外的位来存储额外的信息(比如校验位)。
在 ASCII 之后,有很多扩展的字符编码标准被开发出来,这些标准通常会使用这个额外的位来增加可表示的字符数量。例如,ISO 8859-1 (也被称为 Latin-1)就是一种这样的编码,它使用全部的 8 位,因此可以表示 256 个不同的字符。同样,UTF-8 也是一种可以表示超过 ASCII 范围的字符的编码。
总的来说,ASCII 只使用 7 位是因为历史原因,当时的计算机资源有限,而 7 位已经足够表示所有的基本字符。随着计算机技术的发展,现在的编码标准已经可以表示更多的字符了。
Unicode 字符集
Unicode:Unicode 并不是一种编码,而是一种字符集(Character Set)。它旨在包含全世界所有的字符。在 Unicode 中,每个字符都对应一个唯一的数字,这个数字称为这个字符的 Unicode 码点(Code Point)。Unicode 码点的范围是 0-1,114,111(0x10FFFF)。
因为Unicode兼容ASCII,所以 0 ~ 127 这些 ASCII 编码也可以称为是Unicode码点。例如“Hello”对应的ASCII编码为“72 101 108 108 111”,这些数字既可以被看作是 ASCII 编码,也可以被看作是 Unicode 码点。只不过,在 ASCII 编码中,它们表示的是字节序列,在 Unicode 中,它们表示的是字符。
字符和字节序列的区别
"字符"和"字节序列"这两个概念描述的是数据的两种不同的视角。
-
字符:字符是书写系统中的最小单位,代表了一个语义上的单位,如字母、数字、标点符号和其他符号等。例如,英文字母 'a',汉字 '中',标点符号 ',' 都是字符。字符的表述和语言、文化等有关。
-
字节序列:字节序列是计算机用于存储和操作数据的方式。计算机内部并不直接理解字符的概念,它们只能处理数字。因此,为了在计算机中使用字符,我们需要某种方式将字符映射到数字上,这就是字符编码的任务。一个字符在计算机中可能被编码为一个或者多个字节,这个或者这些字节的组合就构成了该字符的字节序列。
-
Unicode码点对应的字符可以被UTF-8编码为字节序列。例如:字符 "世界" 对应的Unicode码点是 “19990” 和 “30028” ,它们在 UTF-8 编码下的字节序列为 "228 184 150 231 149 140"。
-
当我们在计算机中处理字符时,通常需要在字符(和它们对应的 Unicode 码点)与它们的字节序列之间进行转换。
-
对比这两个概念,我们可以看到他们描述的是同一件事情的不同侧面。字符是面向人类的,关注的是语义;而字节序列则是面向计算机的,关注的是如何存储和传输数据。
举一个例子,假设我们要在计算机中处理字符 'A'。在 ASCII 编码中,'A' 对应的是数字 65,因此,它在计算机中就被存储为一个字节,这个字节的值就是 65。因此,我们可以说,字符 'A' 在计算机中的字节序列是 65。
但如果我们要处理的是字符 '中',情况就变得复杂了。'中' 不在 ASCII 的范围内,我们需要使用能够表示更多字符的编码,比如 UTF-8。在 UTF-8 编码中,'中' 对应的字节序列是 "228 184 173"。这说明,虽然 '中' 只是一个字符,但在计算机中,我们需要三个字节来表示它。
UTF-8 编码方案
UTF-8:UTF-8 是 Unicode 的一种具体实现(即一种编码方式),它是一种变长的编码方式,每个字符可以使用 1 到 4 个字节来表示。UTF-8 的好处是它向后兼容 ASCII:也就是说,所有的 ASCII 字符在 UTF-8 中的表示和 ASCII 中完全一样,这使得许多只能处理 ASCII 的软件无需修改就可以处理 UTF-8。
UTF-8 是一种用于编码 Unicode 字符的方式。它的特点是变长,也就是说每个字符可能由一个、两个、三个甚至四个字节组成。它是以字节为单位进行编码的,因此每个字节的取值范围是 0-255。ASCII 编码的字符在 UTF-8 中仍然保持不变(也就是说,0-127 的值在 UTF-8 中有同样的意义),但 UTF-8 可以编码更多的字符。
例如 "世界" 这两个汉字对应的Unicode码点(字符)是 "19990" 和 "30028" ,然后经过UTF-8转换后得到字节序列 "228 184 150 231 149 140" 。因此,UTF-8 编码实际上是一种将字符(在这里,具体来说就是 Unicode 码点)转换为字节序列的方法。
UTF-8 编码最为常见,但还有其他的编码实现,比如 UTF-16 和 UTF-32。
总结
ASCII 和 Unicode 是两种字符集,它们定义了字符与数字之间的对应关系;而 UTF-8 是一种编码方案,它定义了如何把 Unicode 码点转化为一串字节,并从字节串中恢复出原来的 Unicode 码点。
字符编码的基本原理
当我们说 "Unicode 码点" 的时候,我们是在谈论一个抽象的概念:在 Unicode 字符集中,每个字符都对应一个唯一的数字。这个数字就是这个字符的 Unicode 码点。
然而,当我们把这些字符存储在计算机中,或者在网络上进行传输时,我们不能直接使用这些码点,因为这些码点可能会占用很多的空间(Unicode 码点的范围是 0 到 1,114,111),并且不利于处理(例如,如何表示一个 Unicode 码点的结束?)。所以我们需要一种方式来"编码"这些 Unicode 码点,这就是 UTF-8 的作用。
UTF-8 是一种把 Unicode 码点编码为字节序列的方式,这种方式具有一些优良的特性,比如向后兼容 ASCII,以及具有自同步的能力(也就是说,如果我们在一个 UTF-8 编码的字节流中任意一点开始解码,我们都可以正确地找到字符的边界)。
比如,"世" 这个字符的 Unicode 码点是 19990,但是在 UTF-8 编码中,它被编码为三个字节:228,184,150。这三个字节才是我们在计算机中实际存储和处理的内容。同样,"界" 这个字符的 Unicode 码点是 30028,但是在 UTF-8 编码中,它被编码为三个字节:231,149,140。
Unicode 码点的存在主要是为了给每个字符提供一个唯一的标识,不依赖于任何具体的编码方式,可以被人类理解,并且方便在不同的编码方式之间进行转换。
"19990 30028" 这两个 Unicode 码点是 "世界" 这两个字符的唯一标识。它们不依赖于任何具体的编码方式,不管是 UTF-8、UTF-16 还是其他的编码方式,这两个字符的 Unicode 码点都是 "19990 30028"。
然而,当我们需要在计算机中存储这两个字符,或者在网络上进行传输的时候,我们需要选择一种具体的编码方式,比如 UTF-8,来把这两个 Unicode 码点转化为一串字节。在 UTF-8 编码下,"19990 30028" 就会被转化为 "228 184 150 231 149 140"。
所以,"19990 30028" 这两个 Unicode 码点的存在主要是为了方便我们理解和操作字符,而 "228 184 150 231 149 140" 这串字节则是实际存储和传输数据时使用的编码。
Go语言中的byte类型和rune类型
在 Go 语言中,byte
和 rune
是两种特殊的类型,它们在处理字符时有一些关键的区别:
-
byte
:这是一个别名,本质上是uint8
类型。byte
类型用于处理 ASCII 字符,因为 ASCII 字符的编码 (0~127) 都是在 0-255 之间,这恰好一个byte
能够表示。这样,每一个byte
就对应一个 ASCII 字符。 -
rune
:rune
是int32
的别名。rune
类型用于处理 Unicode 字符。Unicode 是一种可以表示全世界所有字符的编码方案,包括 ASCII 在内的各种语言的字符。由于 Unicode 字符的编码范围比 ASCII 字符的编码范围要大得多,因此需要更多的位来表示,这就是为什么rune
是基于int32
的。
在处理字符时,这两者的区别主要体现在它们能够处理的字符范围上。如果只需要处理 ASCII 字符,那么使用 byte
就足够了。但如果需要处理包括各种国家语言在内的 Unicode 字符,那么就需要使用 rune
。
此外,在 Go 语言中使用 for range
遍历字符串时,Go 语言会自动对 UTF-8 编码的字符串进行解码,然后给我们提供每个字符的 Unicode 码点,得到的实际上是 rune
类型的字符。这是因为 Go 语言内置对 Unicode 的支持,使我们可以非常容易地处理 Unicode 字符。如果我们用普通的 for
循环遍历字符串,得到的就是原始的 UTF-8 编码的字节,实际上是 byte
类型的字符。
以下是一个简单的例子来说明这两者的区别:
package main
import "fmt"
func main() {
s := "Hello, 世界"
fmt.Println(len(s))
for i := 0; i < len(s); i++ {
fmt.Printf("%v ", s[i]) // 这里得到的是 byte 类型的字符
}
fmt.Println()
for _, r := range s {
fmt.Printf("%v ", r) // 这里得到的是 rune 类型的字符
}
}
// 输出结果
13 // len方法显示的是字节长度
72 101 108 108 111 44 32 228 184 150 231 149 140 // 显示的都是 UTF-8 编码的字节序列
72 101 108 108 111 44 32 19990 30028 // 前面一部分显示的是字节序列,后面显示的是Unicode码点,便于我们理解,计算机处理时还是会将其转换为字节序列
可以看到 for range
循环能够正确地处理 Unicode 字符(比如 "世" 和 "界"),而普通的 for
循环则不能正确处理。
总的来说,byte
和 rune
类型都是 Go 语言中处理字符的重要工具,它们之间的区别主要取决于你需要处理的字符的编码范围。