【Go语言圣经3.1】
目标
- 复习二进制基础:包括如何进行二进制、八进制、十六进制转换,理解计算机如何存储数据。
- 了解两的补码:这是理解有符号数表示及溢出的关键。
- 掌握类型系统与类型转换:明确不同类型间不能直接运算、需要显式转换的原因,以及可能引发的数据截断问题。
- 多写一些简单的位运算代码:通过实践掌握位掩码、移位操作和按位逻辑运算在解决实际问题(如集合操作、数据压缩、加密等)中的应用。
- 关注格式化输出:学会使用
fmt.Printf
的各种格式化动词,帮助你更直观地看到数据的二进制和其他进制表示形式。
概念
-
明确大小的整数类型
- 有符号整数:
int8
:8 位,范围 -128 到 127int16
:16 位,范围 -32768 到 32767int32
:32 位,范围 -2³¹ 到 2³¹-1int64
:64 位,范围 -2⁶³ 到 2⁶³-1
- 无符号整数:
uint8
:8 位,范围 0 到 255uint16
:16 位,范围 0 到 65535uint32
:32 位,范围 0 到 2³²-1uint64
:64 位,范围 0 到 2⁶⁴-1
- 有符号整数:
-
平台相关的整数类型
- int 与 uint:
这两种类型的大小由编译器根据目标平台确定,可能是 32 位或 64 位。由于其大小不固定,所以在跨平台代码中要特别小心,不应假设其具体大小。
- int 与 uint:
-
特殊类型:rune、byte、uintptr
-
rune:
实际上是
int32
的别名,主要用于表示 Unicode 字符。
使用rune
可以清晰地表明变量的用途是存储一个字符,而非一个简单的数字。- Unicode 是一个国际标准,旨在为全球所有语言中的字符分配唯一的数字编号,从而实现跨语言、跨平台的文本处理。
- 每个字符在 Unicode 标准中都有一个唯一的码点,通常用
U+
后跟十六进制数字表示。例如,大写字母 A 的码点是U+0041
,中文字符“国”的码点是U+56FD
。 - UTF-32:每个码点固定使用 4 个字节,直接对应 Go 中的
rune
类型(即int32
)。这种方式虽然内存使用较多,但每个字符的长度一致,处理起来比较简单
-
byte:
是
uint8
的别名,用于强调该数值代表一个原始字节,而不是一个小整数。 -
uintptr:
是一种无符号整数类型,足够大到可以存放指针值,主要用于底层编程(如与 C 语言库、操作系统接口交互)。
-
-
数值的二进制表示:计算机内部一切以二进制存储,一个整数由若干 bit 组成。
-
两的补码表示
-
有符号数:
在大多数编程语言中,包括 Go,负数采用两的补码表示。对于一个 n-bit 的数,其值域是:
- 最小值:-2^(n-1)
- 最大值:2^(n-1)-1
例如,int8
的范围是 -128 到 127。
-
无符号数:
所有位都用于表示数值,范围为 0 到 2^n - 1。例如
uint8
的范围为 0 到 255。
-
-
Go 语言中提供的运算符包括:
- 算术运算符:
+
、、、/
、%
- 逻辑运算符:
&&
、||
- 比较运算符:
==
、!=
、<
、<=
、>
、>=
- 位运算符:
&
(与)、|
(或)、^
(异或,亦可用作一元按位取反)、&^
(按位清零)、<<
(左移)、>>
(右移)
- 算术运算符:
-
运算符结合赋值
- 对于算术运算符,Go 还提供复合赋值形式,如
+=
、-=
等,这使得代码更加简洁。
- 对于算术运算符,Go 还提供复合赋值形式,如
-
整数除法与模运算
-
整数除法:
当两个整数相除时,结果向零截断(丢弃小数部分),例如
5/4
得到1
。 -
模运算(%):
仅适用于整数,且在 Go 中模运算的结果的符号与被模数相同,即使除数为负数,
-5 % 3
与-5 % -3
均得-2
。
-
-
基本位运算
&
:按位与|
:按位或^
:按位异或(作为一元运算符时表示取反)&^
:按位清零(如果 y 对应位为 1,则结果为 0,否则为 x 对应位的值)
-
移位操作
- 左移(<<):
相当于乘以 2 的 n 次方,左边的空位用 0 填充。 - 右移(>>):
对无符号数右移同样用 0 填充;而有符号数右移时会根据符号位来决定是否填充 0 或 1(这称为符号扩展)。
- 左移(<<):
-
字符表示
- 字符面值用单引号表示,例如
'a'
或'国'
。 - 字符可以使用
%c
进行打印,或者%q
打印成带单引号的格式。
- 字符面值用单引号表示,例如
要点
溢出 wrap-around
如果运算结果超出变量能表示的范围,多出的高位会被截断。
例如,对于 uint8
类型
var u uint8 = 255
fmt.Println(u, u+1, u*u) // 输出 "255 0 1"
类型转换与类型安全
Go 语言强调类型安全,不同的整数类型不能直接进行运算,即使它们底层可能具有相同的位数。例如:
var apples int32 = 1
var oranges int16 = 2
// var compote int = apples + oranges // 编译错误,类型不匹配
var compote = int(apples) + int(oranges) // 显式转换后才能相加
- 静态类型语言要求显式转换
- 在 Go 语言中,转换语法为
T(x)
,其中T
是目标类型,x
是要转换的值。 - 隐式转换可能会在不经意间导致数据丢失或逻辑错误。例如,将一个较大范围的数据隐式转换成较小范围的类型,可能截断高位数据。显式转换要求开发者明确表明“我知道转换可能带来风险”,从而降低错误风险。
- 静态类型语言(如 Go)的编译器在编译时就会检查类型一致性。如果允许隐式转换,编译器可能无法捕捉到所有潜在的类型错误。
- 在 Go 语言中,转换语法为
- 从大类型转换到小类型时,可能会丢失数据或精度。
- 浮点数转换为整数时,会截断小数部分并向零截断,例如
int(3.141)
得3
。
数值的进制格式
使用 fmt.Printf
时,可以用不同格式化动词控制输出格式:
%d
:十进制%o
:八进制%x
/%X
:十六进制%08b
:二进制,至少 8 位,不足补 0
例如:
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // 输出 "438 666 0666"
这里 %[1]
表示重复使用第一个操作数,#
前缀表示在八进制和十六进制输出时添加前缀(如 0 或 0x)。
语言特性
总结与补充
两的补码
你有一个数字钟表,比如钟表上标有数字0到11。当你往前走时,比如从10走2步,就会变成0。两的补码就利用了这种“绕圈”的特性,把数字“绕回来”来表示负数。
“两的补码”这个名字来源于它表示负数的方法:在二进制中,用一种方法把一个正数变成它的负数,而这个方法实际上就是用“2的幂”来做补数。因为2^n的基础是2,所以这种方法叫做两的补码。
假设我们用8位二进制来表示数字,负数的表示其实就是“2^8减去正数”。比如-5此时是256-5
用反转加1的方法,实际上就是找出一个数,它和正数相加正好等于2^n,这个数我们就称为正数的“补数”。
设我们用 n 位二进制数表示一个整数 x,那么所有 n 位全为 1 的数就是 2ⁿ–1。在这种表示下,对 x 取反(即每一位 0 与 1 互换)得到的数,就是:
~x = (2ⁿ – 1) – x
接下来,我们对 ~x 加 1:
~x + 1 = (2ⁿ – 1 – x) + 1 = 2ⁿ – x
因此,有:
x + (~x + 1) = x + (2ⁿ – x) = 2ⁿ
想象你有一盒糖果,一共有256颗糖(这就像2^8)。如果你有5颗糖,那么剩下的糖数(256 - 5)就是你没有的糖。两的补码就像告诉你,“为了表示-5,我们实际上在表示‘从256里拿走5颗糖’的结果。
用二进制表示集合/构造集合(能够去重)
-
集合与位的对应关系
想象有一个小盒子,每个盒子中的位置(位)对应一个元素。如果某个位置为1,就表示集合中包含对应的元素;如果为0,则不包含。
- 例如,数字
00100010
表示第1位和第5位为1,可以看作集合 {1, 5}。
- 例如,数字
-
构造集合
使用位运算“或”(
|
)可以将多个单一元素“合并”为一个集合:var x uint8 = 1<<1 | 1<<5 // 1<<1 产生 00000010,1<<5 产生 00100000,或运算后得 00100010
同理,
var y uint8 = 1<<1 | 1<<2 // 结果为 00000110,表示集合 {1, 2}
测试集合成员资格
-
位与运算
用“与”(
&
)运算符可以检测两个集合的交集,或者检测一个特定元素是否在集合中。- 在示例中,
x & y
对应的运算是:x
为00100010
y
为00000110
- 逐位与运算后,只有第二位(从右数,位1)同时为1,所以结果为
00000010
,代表集合交集 {1}。
- 在示例中,
-
测试某个元素是否存在
如果要检测集合 x 是否包含元素 n,可以这样写:
if x & (1 << n) != 0 { // 表示元素 n 存在于集合 x 中 }
这里
(1 << n)
就构造了只有第 n 位为1的掩码,和 x 进行与运算后,如果结果不为0,则说明该位在 x 中也为1,即元素 n 存在。