结论先行
在进行 big.Int
类型的简单相互赋值过程中发生了浅拷贝,big.Int
类型数据存储的实体 []uint
并未发生变更,导致出现数据紊乱。
问题引入
在Golang中,标准库提供了big包用来进行大数运算。为了研究它的用法,我编写了下边这个小程序来验证它的特性。程序的逻辑很简单,初始化两个大数变量 a = 1
和 b = 2
,然后使用中间变量法对 a
和 b
进行交换,交换完毕后再对中间变量加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
然鹅。。。从输出结果来看,问题并没有得到解决。
为什么取值操作没有什么卵用呢?我们注意到这里的 a
、b
、tmp
实际上是标准库中的 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
变量的引用,当 a
和 b
交换后 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