Hacks on Computer Vision

golang中json科学计数法的问题

2016.06.12

昨天线上一个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)

使用Sprintfid转换为字符串没有报错,但后续再使用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))中,有个控制精度的参数被赋值为-1AppendFloat函数包装了genericFtoa函数,在genericFtoa函数中,如果精度参数为-1,表示的只要数据准确,就可以尽可能的短,进而在formatDigits函数中根据这个结果把位数设置为6。

__EOF__

本文作者HackCV
版权声明本博客所有文章除特别声明外,均采用 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/

发表评论