Golang 的大数运算 big.Int 一次赋值引发的血案

结论先行

在进行 big.Int 类型的简单相互赋值过程中发生了浅拷贝,big.Int 类型数据存储的实体 []uint 并未发生变更,导致出现数据紊乱。

问题引入

在Golang中,标准库提供了big包用来进行大数运算。为了研究它的用法,我编写了下边这个小程序来验证它的特性。程序的逻辑很简单,初始化两个大数变量 a = 1b = 2,然后使用中间变量法对 ab 进行交换,交换完毕后再对中间变量加100,然后输出交换的结果。

这个程序足够简单,以至于简单到我们可以立马说出它的答案,a = 2, b = 1 但是当控制台中结果输出的那一刻发现事情并不简单。。。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 初始化两个变量: a = 1, b = 2
	a := big.NewInt(1)
	b := big.NewInt(2)

	// 打印交换前的数值
	fmt.Printf("a = %v   b = %v\n", a, b)

	// 使用中间变量法进行交换
	tmp := a
	a = b
	b = tmp

	// 交换完成, 对中间变量加100
	tmp.Add(tmp, big.NewInt(100))

	// 打印交换后的结果
	fmt.Printf("a = %v    b = %v   tmp = %v\n", a, b, tmp)
}

输出:

a = 1   b = 2
a = 2   b = 101   tmp = 101

从结果中可以看出,a和b的内容确实进行了交换,但是对中间变量tmp加100的操作貌似也对变量b生效了。这是为什么呢?

问题探索

在上述程序中,我们使用 big.NewInt() 函数对 a、b 进行了初始化,为此,我们找到该函数的实现:

// NewInt allocates and returns a new Int set to x.
func NewInt(x int64) *Int {
	return new(Int).SetInt64(x)
}

从实现上可以看出,该函数会返回一个 *Int 类型,也就是类型 Int 的指针。回头看我们的程序,对于 tmp := a 这条语句而言,实际上是将 a 所指向的地址存进了 tmp 变量中,后续对于 tmp 变量的一切操作实则就是对 a 变量的操作,所以 tmp.Add(tmp, big.NewInt(100)) 这条语句也对 b(交换前的a) 变量生效。

问题似乎得到了解决,接下来更改程序,验证猜想:

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 初始化两个变量: a = 1, b = 2
	a := big.NewInt(1)
	b := big.NewInt(2)

	// 打印交换前的数值
	fmt.Printf("a = %v   b = %v\n", a, b)

	// 使用中间变量法进行交换
	tmp := *a
	*a = *b
	*b = tmp

	// 交换完成, 对中间变量加100
	tmp.Add(&tmp, big.NewInt(100))

	// 打印交换后的结果
	fmt.Printf("a = %v    b = %v   tmp = %v\n", a, b, tmp)
}

在第二版程序中,对原来交换逻辑做了更改,将指针的赋值操作更改为对指针的指向做取值操作。

输出:

a = 1   b = 2
a = 2   b = 101  tmp = 101

然鹅。。。从输出结果来看,问题并没有得到解决。

为什么取值操作没有什么卵用呢?我们注意到这里的 abtmp 实际上是标准库中的 Int 类型,而非保留字 int 类型,所以是不是 Int 类型的实现导致我们的赋值操作无效呢?

// An Int represents a signed multi-precision integer.
// The zero value for an Int represents the value 0.
type Int struct {
	neg bool // sign
	abs nat  // absolute value of the integer
}

// An unsigned integer x of the form
//
//   x = x[n-1]*_B^(n-1) + x[n-2]*_B^(n-2) + ... + x[1]*_B + x[0]
//
// with 0 <= x[i] < _B and 0 <= i < n is stored in a slice of length n,
// with the digits x[i] as the slice elements.
//
// A number is normalized if the slice contains no leading 0 digits.
// During arithmetic operations, denormalized values may occur but are
// always normalized before returning the final result. The normalized
// representation of 0 is the empty or nil slice (length = 0).
//
type nat []Word

// A Word represents a single digit of a multi-precision unsigned integer.
type Word uint

如上,是标准库中 Int 类型的定义。从定义中可以看出,该struct共有两个成员变量:一个表示当前是否为负数的 neg 变量,一个表示当前数值绝对值的 abs 变量,而 abs 变量的类型是 nas 类型,该类型实际上是一个 []uint 类型,即uint的splice。在golang中,splice是一种引用类型,即对于splice类型进行的参数传递、赋值等操作实际上是对其引用的赋值以及传递。所以,在我们第二版程序中的赋值操作其实是执行了一次浅拷贝,即将成员变量 abs 的引用更改为了 a 变量的引用,当 ab 交换后 tmp 的成员变量 abs 依然执行交换前 a 变量的 abs 地址,所以 tmp 变量的变更其实还是对交换前 a 变量的变更。

知道了原因,解决这个问题的方案就有了,我们只需要使用 big.Int 提供的 Set() 方法,对其进行深拷贝即可。当然,在数据量比较大的时候,深拷贝固然安全,但是其性能消耗也是蛮大的,具体使用哪种方式还是需要读者根据实际使用场景进行决断。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 初始化两个变量: a = 1, b = 2
	a := big.NewInt(1)
	b := big.NewInt(2)

	// 打印交换前的数值
	fmt.Printf("a = %v    b = %v\n", a, b)

	// 使用中间变量法进行交换
	tmp := big.NewInt(0)
	tmp.Set(a)
	a.Set(b)
	b.Set(tmp)

	// 交换完成, 对中间变量加100
	tmp.Add(tmp, big.NewInt(100))

	// 打印交换后的结果
	fmt.Printf("a = %v    b = %v   tmp = %v\n", a, b, tmp)
}

输出:

a = 1    b = 2
a = 2    b = 1   tmp = 101
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值