如果不了解在Go 中如何处理整数溢出问题,可能会导致非常严重的错误。本节将深入探讨这个主题。但是首先,我们需要记住一些与整数有关的概念。
Go 总共提供了 10种整数类型。有4种有符号整数类型和4种无符号整数类型,具体如下表所示。
其他两种整数类型是最常用的:int和uint。这两种类型的大小取决于操作系统:在32位系统中大小为 32位,在64位系统中大小为 64位。
现在我们讨论溢出问题。假设我们将一个int32类型的整数初始化为其最大值,然后将其递增。下面代码的行为应该是什么?
此段代码可编译,并且在运行时不会挂掉。但是,counter++语句会造成整数的溢出:
当算术运算所得到的值超出其字节码所表示的范围时,会发生整数溢出。一个int32类型的整数用 32位表示,以下是一个int32类型的整数的最大值(math.MaxInt32)的二进制表示形式:
由于 int32是有符号整数,所以最左边的位表示整数的符号:0表示正,1表示负。如果我们递增这个整数,就没有空间来表示新值了。因此,这会导致整数溢出。在二进制表示中,以下为它的新值:
正如我们所看到的,在二进制中,符号位等于 1,表示负。这个值可能是一个32位有符号整数的最小值。
注意 最小负值可能并不是 11111111111111111111111111111111。事实上,大多数系统都依赖于补码运算来表示二进制数(反转每一个位并加1)。此操作的主要目的是让 x+(-x)等于0,而不用考虑 x的具体值。
在Go中,在编译时检测到的整数溢出会产生编译错误。例如,
然而,在运行时,整数上溢或下溢都是静默的;这都不会导致应用程序的panic。牢牢记住这种特性是非常重要的,因为它可能会导致潜在的错误(例如,整数增量或正整数相加会导致负结果)。
在深入研究如何使用常见操作检测整数溢出之前,让我们考虑一下什么时候应该关注这个问题。在大多数情况下,像处理请求计数或基本的加法/乘法运算时,如果我们使用正确的整数类型,不必太担心。但在某些情况下,例如使用较小整数类型的内存受限项目、处理较大数字或进行转换操作时,就需要检查可能发生的溢出了。
注意 1996年,Ariane 5发射失败,原因就是将 64位浮点转换为16位有符号整数时发生了溢出(参见链接16)。
如果我们想在基于已定义大小的类型(int8、int16、int32、int64、uint8、uint16、uint32或 uint64)的整数的增量操作期间检测整数溢出,可以使用math常量检查该值。例如,对于 int32类型的整数:
此函数通过检查输入是否等于 math.MaxInt32来判断。在这个例子中,我们就可以判断增量计算是否会导致溢出。
那么对于 int和uint 类型的整数呢?在Go 1.17之前,我们必须手动构建这些常量。但是现在,math 包中已经提供了 math.MaxInt、math.MinInt和math.MaxUint。如果我们要测试 int 类型的溢出情况,可以使用math.MaxInt:
判断 uint 溢出的逻辑也是一样的,可以使用math.MaxUint:
在本节中,我们学习了如何在增量操作后检查整数溢出情况。接下来可以看一下加法操作。
如何在加法操作过程中检测整数溢出呢?答案依然是使用math.MaxInt:
在本例中,a和b 是两个操作数。如果 a 大于 math.MaxInt-b,该操作将导致整数溢出。现在,让我们看一下乘法运算。
乘法操作处理起来有点儿复杂。我们需要使用最小整数 math.MinInt 来执行检查:
对乘法操作检查整数溢出需要多个步骤。首先,需要测试其中一个操作数是否等于 0、1或math.MinInt。然后将乘法结果除以 b。如果结果不等于原始因子(a),则表示发生了整数溢出。
总之,整数溢出(或者下溢)是 Go 中的静默操作。如果我们想检查溢出以避免潜在错误,可以使用本节中描述的实用程序函数。还要记住,Go 提供了一个处理大数字的包:math/big。如果 int 不够用,使用这个包可能会是一个选择。
在下一节中,我们将继续讨论 Go 基本类型中的浮点类型。