昨天线上一个golang写的模块遇到了下面这个问题:
strconv.ParseUint: parsing "%!d(float64=1.46949287e+08)": invalid syntax
这个模块会从MCQ中读取json格式的消息并解析处理,显然上面这个问题是解析数字出错了~
一般使用golang解析json数据时,在不知道具体数据格式时,可以先把数据解析为map[string]interface{}
,然后再从中取出本次处理需要的数据,步骤如下:
msg := make(map[string]interface{})
if err := json.Unmarshal(b, &msg); err != nil {
// 处理错误
return
}
if _, exist := msg["id"]; !exist {
// 没有我们需要的字段
return
}
id, ok := msg["id"].(float64)
if !ok {
// 我们需要的字段不是数字
return
}
这么做的好处是:当修改了json消息的结构时,只要后续处理的逻辑没变,这段代码就不需要修改。
然而,本次问题的出现就在解析数字的地方。golang中的json Unmarshal会把数字直接解析为float64
类型,但如果这个数字是以科学计数法表示的话,那么就需要注意一下自己的使用情况了,如果使用了fmt
中的一些函数,赋值时可能并不会出错,但使用时就会出问题,比如我们的问题就是这样产生的:
// 从 json 中解析出 id,应该为整形的,没判断
id, ok := msg["id"].(float64)
if !ok {
xxxx
}
item.id = fmt.Sprintf("%d", id)
使用Sprintf
把id
转换为字符串没有报错,但后续再使用strconv.ParseUint
函数解析时就出错了。
到这里为止就把问题解决了,但在写简单的测试时发现了个有趣的现象:
package main
import (
"fmt"
"encoding/json"
)
func main() {
msg := make(map[string]int64)
msg["test"] = int64(1234567)
msg["ok"] = int64(123456)
b, err := json.Marshal(msg)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("before: ", string(b))
mm := make(map[string]interface{})
if err := json.Unmarshal(b, &mm); err != nil{
fmt.Println(err)
return
}
bb, err := json.Marshal(mm)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("after: ", string(bb))
}
运行这个程序,会得到如下的结果:
before: {"ok":123456,"test":1234567}
after: {"ok":123456,"test":1.234567e+06}
如前面所说,json的Unmarshal
会把数字解析为float64
格式,那么变成科学计数法的原因应该在于Marshal
编码float64
格式的数字上,进入golang的源码包,发现在encoding/json/encode.go
的第510行(我用的是1.4.2版本)是处理float
类型的代码:
type floatEncoder int // number of bits
func (bits floatEncoder) encode(e *encodeState, v reflect.Value, quoted bool) {
f := v.Float()
if math.IsInf(f, 0) || math.IsNaN(f) {
e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, int(bits))})
}
b := strconv.AppendFloat(e.scratch[:0], f, 'g', -1, int(bits))
if quoted {
e.WriteByte('"')
}
e.Write(b)
if quoted {
e.WriteByte('"')
}
}
可以看到处理float
的是这个函数strconv.AppendFloat(e.scratch[:0], f, 'g', -1, int(bits))
,这个函数在strconv/ftoa.go
的第50行,在注释中,可以看到格式化参数的说明:
// The format fmt is one of
// 'b' (-ddddp±ddd, a binary exponent),
// 'e' (-d.dddde±dd, a decimal exponent),
// 'E' (-d.ddddE±dd, a decimal exponent),
// 'f' (-ddd.dddd, no exponent),
// 'g' ('e' for large exponents, 'f' otherwise), or
// 'G' ('E' for large exponents, 'f' otherwise).
g
对于大的数使用e
即科学计数法表示,其他使用的正常方式表示。
那么,为什么7位就变成科学计数了呢?原来在strconv.AppendFloat(e.scratch[:0], f, 'g', -1, int(bits))
中,有个控制精度的参数被赋值为-1
,AppendFloat
函数包装了genericFtoa
函数,在genericFtoa
函数中,如果精度参数为-1
,表示的只要数据准确,就可以尽可能的短,进而在formatDigits
函数中根据这个结果把位数设置为6。
__EOF__
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
本文链接: https://hackcv.com/posts/golang%E4%B8%ADjson%E7%A7%91%E5%AD%A6%E8%AE%A1%E6%95%B0%E6%B3%95%E7%9A%84%E9%97%AE%E9%A2%98/