本章将讨论十六进制和八进制。我们还将讨论 ASCII 和 UTF-8。
要表示一个二进制数,你需要很多零和一来组合。这个表示很很长。为了表示十进制数 1324,我们需要使用 11 个二进制字符。因此我们需要更简洁的表示方法。
In [1]: bin(1324) Out[1]: '0b10100101100'
十六进制(Hexadecimal)也是一种位置数字系统,它使用 16 个字符来表示一个数字。
十六进制的字符是数字和字母。我们使用从 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
差点忘记介绍八进制了。它使用基数 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
bit 实际是 Binary digit 的缩写,它只有一位,用 0 和 1 来表示,可以表示2种数据,可通过组合在一起来表示更多的数据内容。比如:10100101100 由 11 位 bit 组成。这种组合方式有很多种:
使用 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
如果你想存储数字以外的东西怎么办?例如,我们如何存储 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”。
我们需要定义一个通用词汇来理解字符编码:
有一种字符集你必须得知道:Unicode。这是一个标准,列出了当今计算机上使用的生活语言中的绝大多数字符。
在它的版本 11.0 中由 137,374 个字符组成。 Unicode 就像一个巨大的表格,将一个字符映射到一个代码点。例如,字符“A”被映射到代码点“0041”。
有了 Unicode,我们有了基础的字符表,现在下一个问题是找到一种方法来对这些字符进行编码,将这些代码点放入数据字节中。这正是 ASCII 和 UTF-8 所做的事情。
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 行)
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 代码点的字符进行编码。那代表多少个字符
你可能会问为什么我们要给计数加一……那是因为字符是从代码点 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 |
字符串是“一串字符序列”。例如,“Test”是一个由 4 个不同字符组成的字符串:T、e、s 和 t。字符串很流行;我们使用它们在我们的程序中存储原始文本。它们通常是人类可读的,例如,应用程序用户的名字和姓氏是两个字符串。
字符可以来自不同的字符集。如果使用字符集 ASCII,则只能从 128 个可用字符中进行选择。
每个字符在字符集中都有一个对应的代码点。正如我们之前看到的,代码点是一个任意选择的无符号整数。字符串使用字节存储。让我们以仅由 ASCII 字符组成的字符串为例:
Hello
单个字节可以存储每个字符。该字符串可以由下面的比特存储:
01001000 01100101 01101100 01101100 01101111
顺便提一下,在 Go 中,字符串是不可变的,这意味着它们一旦创建就无法修改。
字符串文字有两类:
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 隐式编码的。
字符串是字节的集合。我们可以使用 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) }