Go 语言类型 整数型

基本概念

整数型数据(Integer)是计算机中基本数据类型,用来表示没有小数部分的数字。整型可以存储正数、负数以及零。

整数类型

在 Go 语言中,int 型表示有符号整数,而 uint 型表示无符号整数。共有 10 种精确大小的整数类型:

类型 字节长度 取值范围
int 4 或 8 int32int64 类型
int8 1 -128~127 即 -27~(27-1)
int16 2 -32768~32767 即 -215~(215-1)
int32 4 -231~(231-1)
int64 8 -263~(263-1)
uint 4 或 8 uint32uint64 类型
uint8 1 0~255 即 0~(28-1)
uint16 2 0~65535 即 0~(216-1)
uint32 4 0~(232-1)
uint64 8 0~(264-1)

日常开发中直接使用 int 类型即可。

表示方法

整数型可以用四种数制表示:

  • 十六进制:前缀为 0x0X,后跟十六进制数字 0-9 以及 A-F,不区分大小写。
  • 八进制:前缀为 0,后跟八进制数字 0-7。
  • 十进制:前缀为正负号,后跟数字 0-9。正号很少用,但是完全合法。
  • 二进制:前缀为 0b0B,后跟数字 0 或 1。Go 语言 1.13 版本以上才支持。

下面是各种数字表示法示例:

package main

import "fmt"

func main() {
	var decimal int = +42      // 十进制
	var octal int = 052        // 八进制
	var hexadecimal int = 0x2A // 十六进制
	var binary int = 0b101010  // 二进制

	fmt.Println(decimal, octal, hexadecimal, binary)
}

此外,整型部分支持使用指数形式来表示。例如,十进制 1500 可以表示为 1.5e315e2。只要科学计数法表示的值可以精确地转换为整数,类似于把 1.0 赋值给整数,编译时会自动识别为整数字面量:

package main

import "fmt"

func main() {
	// 不指定类型时为浮点数
	var a = 2e3
	fmt.Printf("%T: %v\n", a, a)

	// 编译成功,把 1.5e3 等于 1500,是个整数。
	var b int = 1.5e3
	fmt.Printf("%T: %v\n", b, b)
}

整数符号

除了十进制可以直接在数字前使用正负号来表示正负,其他数制在语法上仅能定义正数。先跳过二进制补码概念,看用其他数制来表示负数:

package main

import "fmt"

func main() {
	// 先将正数 42 赋给变量
	var hexadecimal int = 0x2A // 十六进制正数

	fmt.Println("Hexadecimal:", hexadecimal) // 输出为:42
	// 对变量使用负号,就得到了 -42
	fmt.Printf("Negative Hexadecimal: %x", -hexadecimal) // 输出为:-2a
}

可以看到在 Go 语言中,用十六进制表示负数,只用将对应正数赋值给变量,然后对变量使用负号来动态地产生负数形式。这个处理方法在编程中非常普遍,不管数值是以哪种数制表示,底层都以二进制形式存储,负数通过补码处理,所有整型都可以使用相同运算符和函数进行操作。

二进制补码用于统一二进制加减运算,而且还能正确表示零。补码计算规则只有两条:

  • 正数补码:是其本身二进制表示。
  • 负数补码:是其对应正数二进制表示的反码(每个二进制位取反)加一。

下面用一个例子展示推导过程:

  • 有符号整型使用二进制的最高位来表示正负。例如 int8 类型,整数表示范围从 -128+127,二进制表示范围从 0000000011111111。最高位为 0 和最高位为 1 的数都有 128 个,但 000000000 占据了,所以最高位 0 代表的正数比最高位 1 代表的负数少 1 个。
  • 0000000101111111 代表正数 1 到 127。
  • 10000000 代表 -128,也是 127 的反码。
  • 10000000 加 1 得到 10000001,代表 -127,符合负数补码规则。
  • 1000000011111111 代表负数 -128 到 -1。
  • 计算公式 127-2 转为加法表示等于 127+(-2),用二进制表示等于 01111111+11111110,结果为 101111101。由于 int8 类型只能取最低 8 位,结果是 01111101 代表 125,刚好比 01111111 代表的 127 少 2,结果正确。
  • 对 0 应用负数补码规则还是 0,不存在 -0 问题。

运算统一不仅使编程模型更为简单,还使得硬件设计大为简化,不需要为减法单独设计电路。

声明和初始化

整数型变量声明和初始化有下面 4 种方法:

package main

import "fmt"

func main() {
	// 声明但不赋值,默认值为 0,用于零值初始化
	var a int8

	// 显式声明同时赋值
	var b int64 = -13

	// 使用短变量声明并初始化,自动推断类型为 int
	c := 30

	// 通过表达式赋值,用于强调类型
	d := uint32(101)

	fmt.Println(a, b, c, d)
}

整数运算

整数型支持算术运算、比较运算和位运算。算术运算和位运算结果还是整型,结果是浮点数时,小数部分会被截断,不进行四舍五入:

package main

import "fmt"

func main() {
	// 整数算术运算
	fmt.Println("加法结果 addition:", 5+3)
	fmt.Println("减法结果 subtraction:", 5-3)
	fmt.Println("乘法结果 multiplication:", 5*3)
	fmt.Println("除法结果 division:", 5/3) // 输出:1
	fmt.Println("模运算结果 modulus:", 5%3) // 输出:2

	// 位运算
	fmt.Println("按位与结果 and:", 5&3)        // 输出:1
	fmt.Println("按位或结果 or:", 5|3)         // 输出:7
	fmt.Println("按位异或结果 xor:", 5^3)       // 输出:6
	fmt.Println("左移结果 shiftLeft:", 5<<1)  // 输出:10
	fmt.Println("右移结果 shiftRight:", 5>>1) // 输出:2

	// 比较运算
	fmt.Println("5 == 3:", 5 == 3) // 输出:false
	fmt.Println("5 != 3:", 5 != 3) // 输出:true
	fmt.Println("5 > 3:", 5 > 3)   // 输出:true
	fmt.Println("5 < 3:", 5 < 3)   // 输出:false
	fmt.Println("5 >= 3:", 5 >= 3) // 输出:true
	fmt.Println("5 <= 3:", 5 <= 3) // 输出:true
}

除法运算中,除数为 0 会引发编译错误或运行时异常:

package main

import "fmt"

func main() {
	// 运行时报错
	a := 0
	fmt.Println(5 / a)

	// 编译报错
	fmt.Println(5 / 0)
}

类型转换

将浮点型转为整型时,要注意小数部分损失:

package main

import "fmt"

func main() {
	floatValue := 3.9
	integerValue := int(floatValue)
	fmt.Println("浮点数转换为整数,截断小数部分,得到:", integerValue) // 输出:3

	// 编译报错,因为字面量 3.9 类型未定,不能用于转换
	integerValue = int(3.9)
}

虽然 int 类型大小等于 int32int64,但它是独立类型,必须显式转换后才能进行互相运算:

package main

import "fmt"

func main() {
	var x int = 100
	var y int32 = 100

	// 报错:mismatched types int and int32
	fmt.Println("x 直接与 y 比较:", x == y)

	// 将 int 转换为 int32,再进行比较运算
	fmt.Println("x 转换为 int32 后与 y 相等:", int32(x) == y)

	// 将 int32 转换为 int,再进行算术运算
	fmt.Println("y 转换为 int 后与 x 相乘:", x*int(y))
}

uint 类型同理。

数值溢出

当整型数值发生溢出时,编译运行均不会报错:

package main

import "fmt"

func main() {
	var a uint8 = 255  // 8 位无符号整数最大值为 255
	fmt.Println(a + 2) // 没有报错,输出为 1。257 对于 uint8 类型来说溢出,计数从 0 开始,导致输出为 1。
}

上面 uint8 类型变量 a 最大只能表示到 255255 再加 2 等于 257,发生了数值溢出,计数会从 0 开始,编译器会简单地将超出位数抛弃,得到运算结果 1。这种现象有个专业术语叫做整数回绕(wrap around),指当一个整数超过其类型所能表示的最大值时,会从该类型能表示的最小值开始再次计数。

整数回绕是无符号整型的特性,而不是错误。但在现实世界中,发生数值溢出可能导致严重后果,必须阻止。在 math 包中能找到一些常量,对应不同整型类型最大值和最小值,活用它们来检测数值溢出情况:

package main

import (
	"fmt"
	"math"
)

// safeAdd 对两个 int8 类型整数进行加法运算,并检查是否溢出
func safeAdd(a, b int8) (int8, bool) {
	sum := int(a) + int(b) // 首先将操作数转换为更大的整数类型
	if sum > math.MaxInt8 {
		return 0, true // 最大值溢出
	} else if sum < math.MinInt8 {
		return 0, true // 最小值溢出
	}
	return int8(sum), false // 未溢出
}

func main() {
	a, b := int8(127), int8(1)
	result, overflow := safeAdd(a, b)
	if overflow {
		fmt.Println("发生溢出") // 结果 128,大于 127,溢出发生
	} else {
		fmt.Println("运算结果:", result)
	}
}

常量定义为无类型(untyped)时,在编译时被认为具有任意精度,不受基本数据类型限制。当无类型常量与变量进行运算时,常量类型和精度会根据上下文自动调整,结果类型由表达式中其他操作数类型决定,也能有效地避免数值溢出:

package main

import "fmt"

func main() {
	const distance = 24000000000000000000000000 // 定义一个非常大的无类型常量
	time := distance / 299792 / 3600 / 24 / 365 // 除以光速,计算时间
	fmt.Printf("类型为:%T,结果为光年:%v", time, time)   // 输出 int 类型值:2538543415469,在 int 类型范围内没有溢出
}

除了整型,浮点型也会发生数值溢出。浮点型溢出会导致计算结果变为无穷大或负无穷大,处理方式类似整型。

大数类型

math/big 包中提供大数(big number)类型,专门处理超出整型或浮点型大小的数值,以应对高精度计算场景。例如大整数类型 big.Int

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 创建两个大整数变量
	firstBigInt, secondBigInt := new(big.Int), new(big.Int)
	// 指定为十进制,必须通过字符串来赋值
	firstBigInt.SetString("12345678901234567890", 10)
	secondBigInt.SetString("98765432109876543210", 10)

	// 执行大整数加法运算
	sum := new(big.Int).Add(firstBigInt, secondBigInt)
	// 执行大整数乘法运算
	product := new(big.Int).Mul(firstBigInt, secondBigInt)

	// 输出结果也是大数类型
	fmt.Println("大整数加法结果:", sum)
	fmt.Println("大整数乘法结果:", product)
	fmt.Printf("运算结果类型为:%T\n", product) // 输出类型:*big.Int
}

此外大数类型还有大浮点数 big.Float 和分数(有理数) big.Rat 类型:

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 创建大浮点数,指定精度为 100,但依然有误差,只是变得很小
	f1, _ := new(big.Float).SetPrec(100).SetString("5.01")
	f2, _ := new(big.Float).SetPrec(100).SetString("3.10")

	// 大浮点数运算和整型一样
	resultF := new(big.Float).Sub(f1, f2)
	fmt.Println("大浮点数减法结果:", resultF) // 输出 1.910000000000000000000000000003

	// 创建分数 1/3 和 2/3,需要指定分子和分母
	r1 := new(big.Rat).SetFrac(big.NewInt(1), big.NewInt(3))
	r2 := new(big.Rat).SetFrac(big.NewInt(2), big.NewInt(3))

	// 分数除法
	resultR := new(big.Rat).Quo(r1, r2)
	fmt.Println("Result of addition:", resultR) // 输出 1/2
}