golang 服务出现了 panic,根据 panic 打印出的堆栈找到了问题代码,看上去原因是:json 序列化时,遇到了无法序列化的内容
[panic]: json: unsupported value: NaN or Infinite
解释:基本可以判断出:NaN 以及 Inf 是 float64 类型的两种特例,Json 无法表示这类数据,故 panic
查阅 log 看到,这里最原始的 NaN
其实是字符串"NaN"
,明明是字符串,是如何将 "NaN"
转变为 float64 的呢?问题出在使用的 cast 包的 ToFloat64上
可以从 ToFloat64 的源码中看到,当需要转换成 float64 的类型是 string 或者 json.Number 时,调用的都是 strconv.ParseFloat 函数(s.Float64 本质也是调用该函数),继续阅读 strconv.ParseFloat,我们可以在strconv/atof.go文件中看到以下代码:strconv.ParseFloat 会将字符串 NaN 以及 Inf 转换为 float64
类型的 NaN 以及 Inf。 而 json 无法处理这两种数据,会直接 panic
单独判断下即可
func SetValWhenFloatIsNaNOrInf(val float64) float64 {
if math.IsNaN(val) {
return 0.00
}
if math.IsInf(val, 0) {
return 100.00
}
return val
}
NaN 和 Inf 怎么来的呢
在 float64 类型中,我们可以通过 zero/zero 来得到 NaN,也可以用过 除零 操作来得到 Inf,在 Google 并没有得到能解释这两种常量存在的原因,只从二进制浮点数算术标准(IEEE 754)看到有相关的定义
能否把 NaN 以及 Inf 作为 map 的 key?
测试代码
func TestNaNKeyMap() {
m := make(map[float64]struct{}, 0)
for i := 0; i < 10; i++ {
m[math.NaN()] = struct{}{}
fmt.Printf("nan map len:%d\n", len(m))
}
}
func TestInfKeyMap() {
m := make(map[float64]struct{}, 0)
for i := 0; i < 10; i++ {
m[math.Inf(0)] = struct{}{}
fmt.Printf("inf map len:%d\n", len(m))
}
}
结果:可以看待对于 NaN,每次赋值的时候,其实都是给不同的 key 赋值,而 Inf 则不是;所以我们可以得出以下结论:map[float64]struct 这种以 float64 为 key 的 map,存在内存泄漏
的可能
map 的 key 都会经过 hash,然后再确定value 存储的位置,那么问题大概率出在 hash 算法上,在 runtime/alg.go 找到以下函数:
可以看到,算法里判断到 f != f
时,会给hash 值增加一个随机数,并且注释里也说了是为了适配 any kind of NaN
这里 f != f
的判断也同时用在 func IsNaN(f float64) (is bool)
函数中。