当题目给出的整数达到几百位甚至上千位时,int64早已无能为力,Go语言标准库提供的math/big包就成了唯一的救命稻草。不过big.Int的读取方式与普通整型不太一样,如果直接套用fmt.Scan那套思路,很容易碰到值为nil导致的panic,或者在大量输入场景下性能不达标。这篇文章就来系统梳理几种读取big.Int的实用技巧,并分析它们各自的适用场景。

一、准备工作:正确初始化big.Int变量
首先要明确一点,big.Int是一个结构体而不是基础类型,声明之后它的内部切片是nil,直接调用方法会出错。正确的做法是先声明变量,再用指针方式使用:
package main
import (
"fmt"
"math/big"
)
func main() {
var num big.Int // 声明一个big.Int变量
// 或者使用指针方式
p := new(big.Int) // 返回 *big.Int
fmt.Println(num.Cmp(p)) // 此处均为0,说明两个都是零值
}这里有个常见的坑:如果在函数外部声明var x *big.Int,得到的只是一个nil指针,后续调用SetString会直接panic。要么用new(big.Int)初始化,要么用big.NewInt(0)创建。在竞赛刷题时,推荐统一使用new(big.Int),写法简洁且不容易遗漏初始化步骤。
另一个细节是big.Int是可变类型,Add、Mul这些方法会直接修改接收者。在需要保留原始输入值的场景下,记得使用new(big.Int).Set(origin)做一份拷贝,避免原值被运算意外污染。
二、三种主流读取方式对比
big.Int本身实现了fmt.Scanner接口,所以它可以被fmt.Scan、fmt.Fscan等函数直接扫描,这是最省事的方式。同时也支持用SetString从字符串解析。下面分别给出示例:
package main
import (
"bufio"
"fmt"
"math/big"
"os"
"strings"
)
func main() {
// 方式一:fmt.Scan 直接扫描
var a, b big.Int
fmt.Scan(&a, &b)
fmt.Println(a.Add(&a, &b))
// 方式二:SetString 解析字符串,第二个参数指定进制,10表示十进制
s := "123456789012345678901234567890"
n, ok := new(big.Int).SetString(s, 10)
if !ok {
fmt.Println("解析失败")
return
}
fmt.Println(n)
// 方式三:bufio.Scanner 高性能读取
sc := bufio.NewScanner(os.Stdin)
sc.Buffer(make([]byte, 1024*1024), 1024*1024) // 扩大缓冲区,防止超长数字被截断
if sc.Scan() {
val, _ := new(big.Int).SetString(strings.TrimSpace(sc.Text()), 10)
fmt.Println(val)
}
}方式一最直观,但fmt.Scan内部使用反射且锁定了全局状态,在输入量达到百万级别时速度明显偏慢。方式二搭配bufio.Scanner是性能最优的组合,Scanner按行切分数据,SetString负责转换,整个过程没有格式化解析的开销,实测比fmt.Scan快数倍以上。
使用方式三时务必调用sc.Buffer扩大缓冲区。Scanner默认缓冲区只有64KB,而一个上千位的十进制大数占不了多少字节,真正危险的是单行包含多个超大数字的情况,一旦超长会触发bufio.Scanner: token too long错误并停止扫描。另外SetString要求字符串是纯净数字,前面带空格或换行符都会解析失败,所以strings.TrimSpace不能省。
三、常见错误与性能优化建议
第一类高频错误是忽略SetString的返回值。它返回(*big.Int, bool)两个值,布尔值表示解析是否成功。如果输入可能包含非法字符(比如十六进制写法传给了十进制解析),不检查返回值就会拿到一个错误结果继续计算,最终输出错误答案却很难定位。
第二类错误是进制处理不当。SetString的第二个参数为0时支持自动识别前缀:0x开头识别为十六进制,0b开头识别为二进制,0开头识别为八进制。如果输入明确是十进制,务必显式传10,否则类似089这样的输入会因为前导零被当作八进制而解析失败。
package main
import (
"fmt"
"math/big"
)
func main() {
// 自动识别进制
a, _ := new(big.Int).SetString("0x1F", 0)
fmt.Println(a) // 输出 31
// 显式十进制,前导零可以正常解析
_, ok := new(big.Int).SetString("089", 10)
fmt.Println(ok) // 输出 true
}从性能角度看,还有两点值得注意。一是复用big.Int对象:在循环中反复new(big.Int)会产生大量临时对象,可以在循环外创建一次,循环内用SetUint64(0)或SetString重置复用,减轻GC压力。二是SetString对超长数字的解析采用子二次算法,几百位的数字解析开销可以忽略,但如果处理上万位的数字且输入频繁,可以考虑缓存解析结果或改用SetBytes直接从字节流构造。
总结一下:日常简单场景用fmt.Scan足够省心;竞赛或高性能场景请选择bufio.Scanner加SetString的组合,并记得扩大缓冲区、去除空白、检查返回值。掌握这几个细节,big.Int的输入处理基本不会再出问题。