1 你将在本章中学到什么?
- 什么是 Unicode、ASCII 和 UTF-8?
- 字符是如何存储的。
- 什么是符文类型?
2 涵盖的技术概念
- ASCII
- UTF8
- Hexadecimal 十六进制
- Octal 八进制
- Rune
- Code Point
3 介绍
本章将讨论十六进制和八进制。我们还将讨论 ASCII 和 UTF-8。
4 Base 16:十六进制表示
要表示一个二进制数,你需要很多零和一来组合。这个表示很很长。为了表示十进制数 1324,我们需要使用 11 个二进制字符。因此我们需要更简洁的表示方法。
In [1]: bin(1324)
Out[1]: '0b10100101100'
十六进制(Hexadecimal)也是一种位置数字系统,它使用 16 个字符来表示一个数字。
- 前缀 Hexa 在拉丁语中的意思是 6
- Decimal 来自拉丁词 Decem,意思是 10
十六进制的字符是数字和字母。我们使用从 0 到 9(10 个字符)的数字和从 A 到 F(6 个字符)的字母。
上图是 16 进制 与 10 进制之间的关系表,0 到 9 的数字对应十进制系统中的相同值,字母 A 对应于 10,字母 B 对应于 11 ...等。这是十六进制数字系统的特点;我们使用字母来表示数值(节省表示的空间)。
这也许会给我们带来一些疑惑,我们必须尝试去接受它,我们需要更多的字符所以我们拿了字母...
你可以看到我们在这个符号中引入了字母。那是因为从 0 到 9,你有十个字符、十个数字,但是对于基数为 16 的编号系统,我们还需要六个字符。这就是为什么我们采用了字母表的前六个字母。这是历史的选择;其他字符可以替换字母,系统将仍然相同。
首先明确,无论是10 进制的 1324 还是 16 进制的 52C,它们表示的含义都是同一个数量。
如果我们将一个 16 进制数转换成 一个 10进制数,计算方式是:从 16 进制数的低位(右边)开始,逐步向高位(左边),取每位的字符对应 10 进制数值,与 16 的 n
次幂相乘(这里的 n 不是固定的,它等于该字符所在的位序 - 1,位序指从低位到高位的排序),最后将每位的计算结果相加就是 10 进制的值了。
举个例子:10 进制的 1324 相当于 16 进制的 52C
# 十六进制 5 2 C
1. 计算 'C':
因为 'C' 对应的10进制值是12,它所在的位序是1,即它是右边第1位,所以它的值为:12*(16^(1-1)) = 12
2. 计算'2':
因为 '2' 对应的10进制值是2,它所在的位序是2,即它是右边第2位,所以它的值为:2*(16^(2-1)) = 32
3. 计算'5':
因为 '5' 对应的10进制值是5,它所在的位序是5,即它是右边第3位,所以它的值为:5*(16^(3-1)) = 1280
4. 将每位结果相加:
12 + 32 + 1280 = 1224
在 Go 中,如果你想打印数据的 16 进制表示,可以使用fmt
函数:
package main
import "fmt"
func main() {
n := 2548
fmt.Printf("%x", n)
}
这个程序的输出是9f4
(即十进制数字 2458 对应的 16 进制表示)。"%x" 是十六进制的格式化动词,它会用小写方式展示。
如果你将格式化动词改成"%X",就可以打印出大写的十六进制:9F4
。
注意代码片段中的n
是十进制表示,即代码中数值默认都是十进制表示。如果你想在代码中表示十六进制数值,需要在数值前面加上0x
的标识:
package main
import "fmt"
func main() {
n := 2548
n2 := 0x9F4
fmt.Printf("%X\n", n)
fmt.Printf("%x\n", n2)
}
输出:
9F4
9f4
如何你想让0x9F4
以十进制的方式打印出来,你可以是格式化动词"%d":
package main
import "fmt"
func main() {
n2 := 0x9F4
fmt.Printf("Decimal : %d\n", n2)
}
输出:
Decimal: 2548
5 Base8:八进制表示
差点忘记介绍八进制了。它使用基数 8,这意味着八个不同的字符。选择了从 0 到 7 的数字。十进制到八进制的转换和我之前介绍的方法类似。让我们举个例子:
我们从最右边的字符开始,将它乘以 0 的 8 次方,即 1。然后我们取下一个字符:5 将其乘以 8 的 1 次方,即 8……
要知道,Unix 操作系统中的文件权限就是通过八进制表示的。
在 Go 中,通过增加前缀0
或者0o
来表示数值是八进制。和十六进制一样,fmt 包也为八进制提供两种格式化动词:
package main
import "fmt"
func main() {
n2 := 0x9F4
fmt.Printf("Decimal : %d\n", n2)
// n3 is represented using the octal numeral system
n3 := 02454
// alternative : n3 := 0o2454
// convert in decimal
fmt.Printf("decimal: %d\n", n3)
// n4 is represented using the decimal numeral system
n4 := 1324
// output n4 (decimal) in octal
fmt.Printf("octal: %o\n", n4)
// output n4 (decimal) in octal (with a 0o prefix)
fmt.Printf("octal with prefix : %O\n", n4)
}
输出:
Decimal : 2548
decimal: 1324
octal: 2454
octal with prefix : 0o2454
6 数据的表示方式:bit、nibble、bytes 和 word
bit 实际是 Binary digit 的缩写,它只有一位,用 0 和 1 来表示,可以表示2种数据,可通过组合在一起来表示更多的数据内容。比如:10100101100 由 11 位 bit 组成。这种组合方式有很多种:
- 一个 nilbble 表示由 4 个比特构成
- 一个 byte 由 8 个比特构成
- 一个 word 由 16 个比特构成
- 一个 doubleword 由 32 个比特构成
- 一个 quadword 由 64 个比特构成(等价于 4 个 word)
使用 Go,你可以创建一个字节切片。许多常见的标准包函数和方法都将字节片作为参数。让我们看看如何创建字节切片:
package main
import "fmt"
func main() {
b := make([]byte, 0)
b = append(b, 255)
b = append(b, 10)
fmt.Println(b)
}
在上面的代码片段中,我们创建了一个字节切片(使用内置 make),然后我们将两个数字附加到切片中。
Golang 字节类型是 uint8 的别名。 Uint8 意味着我们可以在 8 位(一个字节)的数据上存储无符号(没有任何符号,所以没有负数)整数。uint8 的最小值是 0,最大值是 255(即 8 位都是1)。
这就是为什么我们只能将 0 到 255 之间的数字追加到一个字节片中。如果你尝试追加一个大于 255 的数字,你将收到以下错误:
./prog.go:7:15: constant 256 overflows byte
如果你想以二进制来打印数值,可以用"%b"格式化动词:
package main
import "fmt"
func main() {
n2 := 0x9F4
fmt.Printf("Decimal : %d\n", n2)
fmt.Printf("Binary : %b\n", n2)
}
输出:
Decimal : 2548
Binary : 100111110100
7 其他字符呢?
如果你想存储数字以外的东西怎么办?例如,我们如何存储 Masaoki Shiki 的这个俳句:
spring rain:
browsing under an umbrella
at the picture-book store
字节类型是否合适?一个字节只不过是一个存储在 8 位上的无符号整数。这个俳句由字母和特殊字符组成。我们有一个“:”和一个“-”,我们还有换行符……我们如何存储这些字符?之前提到的十六进制、八进制或者二进制,如何来表示这个俳句?
我们必须想办法给每个字母甚至特殊字符一个唯一的代码。你可能听说过 UTF-8、ASCII、Unicode?本节将解释它们是什么以及它们是如何工作的。我开始编程时(那不是在 Go 中),字符编码是一种晦涩的东西,我觉得它并不有趣,但字符编码可能是必不可少的,因为我在工作上曾经花了几个晚上的时间来解决可以来解决关于字符的问题。
字符编码的历史非常悠久。随着电报的发展,我们需要一种可以在电线上传输的方式来编码消息。最早的尝试之一是摩尔斯电码。它由四个符号组成:短信号、长信号、短空格、长空格(来自*)。字母表中的每个字母都可以用莫尔斯编码。例如,A 被编码为一个短信号,然后是一个长信号。加号“+”被编码为“short long short long short”。
8 名词
我们需要定义一个通用词汇来理解字符编码:
- Character 字符 这可以由我们手写。它传达了一种意义。例如,符号“+”是一个字符。这意味着在其他东西上添加一些东西。字符可以是字母、符号或表意文字。
- Character set 字符集:这是不同字符的集合。你经常会看到或听到缩写“charset”。
- Code point 码点:字符集中的每个字符作为唯一标识该字符的等效数值。这个数值是一个码点。
9 字符集与编码
有一种字符集你必须得知道:Unicode。这是一个标准,列出了当今计算机上使用的生活语言中的绝大多数字符。
在它的版本 11.0 中由 137,374 个字符组成。 Unicode 就像一个巨大的表格,将一个字符映射到一个代码点。例如,字符“A”被映射到代码点“0041”。
有了 Unicode,我们有了基础的字符表,现在下一个问题是找到一种方法来对这些字符进行编码,将这些代码点放入数据字节中。这正是 ASCII 和 UTF-8 所做的事情。
10 ASCII 如何工作?
ASCII 表示美国信息交换标准代码。它是在六十年代发展起来的。目标是找到一种方法来对用于传输消息的字符进行编码。
ASCII 使用七个二进制数字上编码字符,另一个二进制数字是奇偶校验位。奇偶校验位用于检测传输错误。加在前7位之后,值为0。如果1的个数为奇数,则奇偶校验位为1;如果个数是偶数,则设置为 0。
一个字节的数据正好可以存储每个字符。使用7 个bit能表示多少个整数呢?
用一个比特,我们可以编码两个值,0 和 1,用 2 个比特,我们可以编码四个不同的值。当你添加一点时,你可以将可以编码的值的数量乘以 2。使用 7 位,你可以编码 128 个整数。一般来说,可以用 n 个二进制数字编码的无符号整数的数量是 n 次幂的 2。
Number of bits Number of values
1 2
2 4
3 8
4 16
5 32
6 64
7 128
所以,ASCII 允许你对 128 个不同的字符进行编码。对于每个字符,我们都有一个特定的代码点。无符号整数值表示代码点。
在上图 中,你可以看到 USASCII 代码图表。此表帮助你将字节转换为字符。例如,字母 B 相当于 1000010(二进制)(第 4 列,第 2 行)
11 UTF-8 如何工作?
UTF-8 表示 Universal Character Set Transformation Format1 8 比特。它是由Rob Pike 和 Ken Thompson发明的(他们两人也是 Go 的创造者!)这种编码的设计非常巧妙。我将尝试简要解释一下:
UTF-8 是一种可变宽度的编码系统。这意味着字符使用一到四个字节进行编码(一个字节代表八个二进制数字)。
从上图,你可以看到 UTF-8 的编码规则。一个字符可以编码为 1 到 4 个字节。
使用一个字节只可以进行编码的码点是从 U+0000 到 U+007F(包括在内)。该范围由 128 个字符组成。(从0到127,一共有128个数字。
但是需要编码更多的字符!毕竟它有 137,374 个。我们需要用两个字节来表示 U+0080 及其之后的码点,甚至更多的字节来表示更大的码点。而且,我们也需要知道当前这个码点用来几个字节来表示,这样我们在解码是就可以尽可能的节省时间了。
这就是为什么 UTF-8 的创建者添加了固定的字节来标识。第一个附加字节用 1 比特开头,值为“0”;那些是固定的。我们现在使用 2 个字节来编码我们的字符时,我们只需添加固定为“110”。它对 UTF-8 解码器说:“小心;我们是2!”。
如果我们使用 2 个字节,我们有 11 位空闲(8 * 2 - 5(固定位)=11)。我们可以对包含从 U+0080 到 U+07FF 的 Unicode 代码点的字符进行编码。那代表多少个字符
- 十六进制 0080 = 十进制 128
- 十六进制 07FF = 十进制 2047
- 从 0080 到 07FF 有 2047-128+1=1920
你可能会问为什么我们要给计数加一……那是因为字符是从代码点 0 开始索引的。
如果使用 3 个字节,则第一个字节将从固定位1110开始。这将向解码器发出信号,该字符是使用 3 个字节编码的。换句话说,下一个字符将在第三个字节之后开始。附加的两个字节以10开头。使用三个编码字节,你有 16 位空闲(8 * 3 - 8(固定位)=16)。您可以将字符从 U+0800 编码到 U+FFFF。
如果你已经了解了 3 个字节是如何工作的,那么了解系统如何使用 4 个字节应该没有问题。在我们的第一个字节中,我们固定了前五个位 (11110)。然后我们有三个额外的字节。如果我们从总位数中减去固定位,我们就有 21 位可用。这意味着我们可以将代码点从 U+10000 编码到 U+10FFFF。
字节数 | 编码范围 |
---|---|
1 | U+0000~U+007F |
2 | U+0080~U+07FF |
3 | U+0800~U+FFFF |
4 | U+10000~U+10FFFF |
12 字符串
字符串是“一串字符序列”。例如,“Test”是一个由 4 个不同字符组成的字符串:T、e、s 和 t。字符串很流行;我们使用它们在我们的程序中存储原始文本。它们通常是人类可读的,例如,应用程序用户的名字和姓氏是两个字符串。
字符可以来自不同的字符集。如果使用字符集 ASCII,则只能从 128 个可用字符中进行选择。
每个字符在字符集中都有一个对应的代码点。正如我们之前看到的,代码点是一个任意选择的无符号整数。字符串使用字节存储。让我们以仅由 ASCII 字符组成的字符串为例:
Hello
单个字节可以存储每个字符。该字符串可以由下面的比特存储:
01001000 01100101 01101100 01101100 01101111
顺便提一下,在 Go 中,字符串是不可变的,这意味着它们一旦创建就无法修改。
13 String literals
字符串文字有两类:
-
原生字符串文字。它们被定义在反引号之间。
- 禁止字符是 反引号
- 丢弃的字符是 回车符 (\r)
-
解释型字符串文字。它们被定义在双引号之间。
- 禁止字符是 换行、未转义的双引号
package main
import "fmt"
func main() {
raw := `spring rain:
browsing under an umbrella
at the picture-book store`
fmt.Println(raw)
interpreted := "i love spring"
fmt.Println(interpreted)
}
你可能注意到在这段代码中,我们没有告诉 Go 我们使用哪个字符集。这是因为字符串文字是使用 UTF-8 隐式编码的。
14 Runes
字符串是字节的集合。我们可以使用 for 循环遍历字符串的字节:
package main
import "fmt"
func main() {
s := "我爱 Golang"
for _, v := range s {
fmt.Printf("Unicode code point: %U - character '%c' - binary %b - hex %X - Decimal %d\n", v, v, v, v, v)
}
}
输出:
Unicode code point: U+6211 - character '我' - binary 110001000010001 - hex 6211 - Decimal 25105
Unicode code point: U+7231 - character '爱' - binary 111001000110001 - hex 7231 - Decimal 29233
Unicode code point: U+0020 - character ' ' - binary 100000 - hex 20 - Decimal 32
Unicode code point: U+0047 - character 'G' - binary 1000111 - hex 47 - Decimal 71
Unicode code point: U+006F - character 'o' - binary 1101111 - hex 6F - Decimal 111
Unicode code point: U+006C - character 'l' - binary 1101100 - hex 6C - Decimal 108
Unicode code point: U+0061 - character 'a' - binary 1100001 - hex 61 - Decimal 97
Unicode code point: U+006E - character 'n' - binary 1101110 - hex 6E - Decimal 110
Unicode code point: U+0067 - character 'g' - binary 1100111 - hex 67 - Decimal 103
上图中的消息是“我爱 Golang”,前两个字符是中文。
程序将遍历字符串的每个字符。在 for 循环中 v 的类型是rune
。rune
是一个内置类型,定义如下:
// 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
一个Rune
代表一个 Unicode 码点。
- Unicode 代码点是数值。
- 按照惯例,它们总是用以下格式表示:“U+X”,其中 X 是代码点的十六进制表示。 X 应该有四个字符。
- 如果 X 少于四个字符,我们添加零。
- 例如:字符“o”的代码点等于 111(十进制)。十六进制的 111 写成 6F。十进制码点为 U+006F
要以常规格式打印代码点,你可以使用格式动词“%U”。
你也可以使用单引号来创建一个rune
:
package main
import "fmt"
func main(){
var aRune rune = 'Z'
fmt.Printf("Unicode Code point of '%c': %U\n", aRune, aRune)
}
15 随堂测试
15.1 问题
- 判断对错:“785G”是一个十六进制数字
- 判断对错:“785f”和“785F”代表相同的数量
- 表示十六进制数(带有大写字母)的格式化动词是什么?
- 用十进制表示数字的格式化动词是什么?
- 什么是码点?
- 填空白。 ______ 是一个字符集,______ 是一个编码标准。
- 判断对错:UTF-8 允许你编码比 ASCII 更少的字符。
- 我可以使用多少字节来使用 UTF-8 编码系统对字符进行编码?
15.2 答案
- 判断对错:“785G”是一个十六进制数字
错误,因为字母 G 不属于十六进制数。字母 A 到 F 可以是十六进制数的一部分。 - 判断对错:“785f”和“785F”代表相同的数量
正确,字母大小写的含义都相同。 - 表示十六进制数(带有大写字母)的格式化动词是什么?
%X - 用十进制表示数字的格式化动词是什么?
%d - 什么是码点?
代码点是一个数字值,用于标识字符集中的一个字符 - 填空白。 ______ 是一个字符集,______ 是一个编码标准。
Unicode 是一个字符集,UTF-8 是一个编码标准。 - 判断对错:UTF-8 允许你编码比 ASCII 更少的字符
错误,UTF-8 最少用一个字节,ASCII 也是用一个字节 - 我可以使用多少字节来使用 UTF-8 编码系统对字符进行编码?
可以使用 1~4 个字节,这取决于字符的码点