Golang 第三章 1.整数基本数据类型

  • By v2ray节点

  • 2024-08-24 10:56:29

  • 评论

Go 的数值数据类型包括几种不同大小的整数、浮点数和复数。每种数值类型决定了其值的大小和有无符号性。让我们从整数开始。

Go 提供了有符号和无符号的整数运算。有四种不同大小的有符号整数——8位、16位、32位和64位,分别由类型 int8int16int32int64 表示;相应的无符号版本则是 uint8uint16uint32uint64

还有两种类型称为 intuint,它们分别是特定平台上有符号和无符号整数的自然大小或最有效的大小;int 是迄今为止使用最广泛的数值类型。这两种类型的大小相同,可能是32位或64位,但不能假设是哪种大小;即使在相同的硬件上,不同的编译器可能会做出不同的选择。

类型 runeint32 的同义词,通常表示一个 Unicode 代码点。两者可以互换使用。类似地,类型 byteuint8 的同义词,强调该值是一段原始数据而不是一个小的数值。

最后,还有一种无符号整数类型 uintptr,其宽度未指定,但足以容纳一个指针值的所有位。uintptr 类型仅用于低级编程,例如在 Go 程序与 C 库或操作系统交互的边界上。当我们在第13章讨论 unsafe 包时,会看到这方面的例子。

无论大小如何,intuintuintptr 都与它们明确指定大小的同类类型不同。因此,即使整数的自然大小是32位,int 也与 int32 类型不同,在需要 int32 的地方使用 int 值,或反之,都需要显式转换。

有符号数以二进制补码形式表示,其中高位用于表示数字的符号,n 位数字的值范围是从 -2^(n-1)2^(n-1)-1。无符号整数使用所有位表示非负值,其范围为 0 到 2^n-1。例如,int8 的范围是 -128127,而 uint8 的范围是 0255

Go 的用于算术、逻辑和比较的二进制运算符按优先级递减顺序列出如下:

二元运算符只有五个优先级级别。同一级别的运算符从左到右结合,因此为了清晰起见,或者为了使运算符按照预期的顺序在表达式中求值,可能需要使用括号,例如 mask & (1 << 28)

上表中前两行的每个运算符,例如 +,都有一个对应的赋值运算符,如 +=,可以用来简化赋值语句。

算术运算符 +-*/ 可以应用于整数、浮点数和复数,但取余运算符 % 仅适用于整数。不同编程语言中,% 对负数的处理方式各不相同。在 Go 中,余数的符号始终与被除数的符号相同,因此 -5%3-5%-3 的结果都是 -2/ 的行为取决于操作数是否为整数,因此 5.0/4.0 的结果是 1.25,但 5/4 的结果是 1,因为整数除法会将结果向零截断。

如果算术运算的结果(无论是有符号还是无符号)具有比结果类型可以表示的更多的位数,这种情况称为溢出。那些无法容纳的高位将被静默丢弃。如果原始数字是有符号类型的,且最高位是1,那么结果可能会是负数,就像下面的 int8 示例一样:

var u uint8 = 255
fmt.Println(u, u+1, u*u) // "255 0 1"
var i int8 = 127
fmt.Println(i, i+1, i*i) // "127 -128 1"

两整数如果是相同类型的,可以使用下面的二元比较运算符进行比较;比较表达式的结果类型是布尔值。

== equal to
!= not equ al to
< less than
<= less than or equ al to
> greater than
>= greater than or equ al to

实际上,所有基本类型的值-布尔值、数字和字符串-都是可比较的,这意味着两个相同类型的值可以使用 == 和 != 运算符进行比较。此外,整数、浮点数和字符串可以通过比较运算符进行排序。许多其他类型的值是不可比较的,且这些类型的值也不具有顺序。在我们遇到每种类型时,将介绍其值的可比性规则。

此外,还有一元加法和减法运算符:

+ unar y positive (no effec t)
- unar y negat ion

对于整数,+x0+x 的简写,-x0-x 的简写;对于浮点数和复数,+x 就是 x 本身,而 -x 则是 x 的取反。

Go 还提供了以下按位二元运算符,前四个将操作数视为位模式,没有算术进位或符号的概念:

& bit w ise AND
| bit w ise OR
^ bit w ise XOR
&^ bit cle ar (AND NOT)
<< lef t shif t
>> right shif t

运算符 ^ 作为二元运算符时是按位异或(XOR),但作为一元前缀运算符时是按位取反或补码,即它返回一个将操作数的每一位都取反的值。&^ 运算符是按位清零(AND NOT):在表达式 z = x &^ y 中,如果 y 的对应位是 1,则 z 的对应位为 0;否则,z 的对应位等于 x 的对应位。

下面的代码展示了如何使用按位操作将 uint8 值解释为一个由 8 个独立位组成的紧凑且高效的集合。它使用 Printf 的 %b 格式动词来打印数字的二进制位;08 修饰 %b(这里作为副词使用!)以用零填充结果,使其正好为 8 位。

	var x uint8 = 1<<1 | 1<<5
	var y uint8 = 1<<1 | 1<<2
	fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
	fmt.Printf("%08b\n", y) // "00000110", the set {1, 2}
	fmt.Printf("%08b\n", x&y) // "00000010", the intersection {1}
	fmt.Printf("%08b\n", x|y) // "00100110", the union {1, 2, 5}
	fmt.Printf("%08b\n", x^y) // "00100100", the symmetric difference {2, 5}
	fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5}
	for i := uint(0); i < 8; i++ {
		if x&(1<<i) != 0 { // membership test
			fmt.Println(i) // "1", "5"
		}
	}
	fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6}
	fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}

(第六章.5节展示了一种整数集合的实现,这种集合可以比字节大得多。)

在位移操作中,x<<n 和 x>>n,n 操作数决定了要位移的位数,并且必须是无符号的;x 操作数可以是有符号的或无符号的。从算术上讲,左移 x<<n 相当于乘以 2^n,而右移 x>>n 相当于除以 2^n 的下取整。

左移操作用零填充被腾出的位,就像无符号数的右移操作一样;但有符号数的右移操作则用符号位的副本填充腾出的位。因此,当你将整数视为位模式时,使用无符号算术非常重要。

尽管 Go 语言提供了无符号数和算术操作,但我们往往即使对于不能为负的量(比如数组的长度),也倾向于使用有符号的 int 形式,尽管 uint 可能是一个更显而易见的选择。实际上,内置的 len 函数返回的是一个有符号的 int,例如在这个反向宣布奖牌的循环中:

medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
    fmt.Println(medals[i]) // "bronze", "silver", "gold"
}

替代方案将是灾难性的。如果 len 返回一个无符号数字,那么 i 也将是 uint,而条件 i >= 0 根据定义总是为真。在第三次迭代中,当 i == 0 时,i-- 语句将导致 i 不变成 −1,而是变成最大 uint 值(例如,2^64−1),此时对 medals[i] 的访问将在运行时失败或发生恐慌(§5.9),因为尝试访问超出切片边界的元素。

因此,无符号数字通常仅在需要其位运算符或特殊算术运算符时使用,例如在实现位集合、解析二进制文件格式或进行哈希和加密时。它们通常不用于仅表示非负量的情况。

通常,必须进行显式转换才能将一个值从一种类型转换为另一种类型,且用于算术和逻辑(二进制操作符,除位移外)的操作数必须是相同的类型。虽然这偶尔会导致表达式更长,但它也消除了一个整体类别的问题,使程序更易于理解。

以其他上下文中熟悉的例子为例,请考虑以下序列:

var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // compile error

尝试编译这三个声明会产生错误消息:

invalid operation: apples + oranges (mismatched types int32 and int16)

这种类型不匹配可以通过几种方式解决,最直接的方法是将所有内容转换为一个共同的类型:

var compote = int(apples) + int(oranges)

如第二章.5节所述,对于每种类型 T,转换操作 T(x) 可以将值 x 转换为类型 T,如果转换是允许的。许多整数到整数的转换不会改变值;它们只是告诉编译器如何解释一个值。但是,将一个大整数转换为一个较小的整数,或将整数转换为浮点数或反之,则可能会改变值或失去精度:

f := 3.141 // a float64
i := int(f)
fmt.Println(f, i) // "3.141 3"
f=1.99
fmt.Println(int(f)) // "1"

浮点数到整数的转换会丢弃任何小数部分,向零舍入。你应避免在操作数超出目标类型范围的情况下进行转换,因为行为依赖于实现:

f := 1e100 // a float64
i := int(f) // result is implementation-dependent

任何大小和类型的整数字面量可以写作普通的十进制数字,或者以 0 开头的八进制数字,如 0666,或者以 0x 或 0X 开头的十六进制数字,如 0xdeadbeef。十六进制数字的字母可以是大写或小写。如今,八进制数字似乎仅用于一个目的——在 POSIX 系统上表示文件权限,而十六进制数字则广泛用于强调数字的位模式而不是其数值。

在使用 fmt 包打印数字时,我们可以通过 %d%o%x 格式说明符来控制进制和格式,如下例所示:

o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

注意使用了两个 fmt 的技巧。通常,一个包含多个 % 格式说明符的 Printf 格式字符串需要相同数量的额外操作数,但 % 后面的「副词」[1] 会告诉 Printf 重复使用第一个操作数。其次,%o%x%X# 副词会告诉 Printf 分别发出 0、0x 或 0X 前缀。

字符(rune)字面量用单引号括起来。最简单的例子是像 'a' 这样的 ASCII 字符,但也可以直接写任何 Unicode 代码点,或者使用数字转义,正如我们将会看到的那样。

字符(rune)可以用 %c 打印,如果需要引号,则使用 %q

ascii := 'a'
unicode := 'D'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 D 'D'"
fmt.Printf("%d %[1]q\n", newline) // "10 '\n'"


v2ray节点购买