<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入理解 Go Json.Unmarshal 精度丟失之謎

          共 5879字,需瀏覽 12分鐘

           ·

          2022-01-08 11:27

          緣起

          前幾天寫(xiě)了個(gè)小需求,本來(lái)以為很簡(jiǎn)單,但是上線之后卻發(fā)現(xiàn)出了bug。

          需求大概是這樣的:

          • 上游調(diào)用我的服務(wù)來(lái)獲取全量信息,上游的數(shù)據(jù)包雖然是json但是結(jié)構(gòu)不確定
          • 我的服務(wù)使用Go語(yǔ)言開(kāi)發(fā),所以就使用了原生的json包來(lái)進(jìn)行反序列化
          • 拿到唯一ID從DB拉取數(shù)據(jù),并返回給上游調(diào)用方

          就是這么簡(jiǎn)單的過(guò)程,讓我栽了個(gè)跟頭,bug的現(xiàn)象是這樣的:

          • 上游給的唯一ID一直在數(shù)據(jù)庫(kù)查不到結(jié)果
          • 上游給的唯一ID一定是真實(shí)有效的

          乖乖,這就矛盾了,于是我祭出了日志大法,在測(cè)試環(huán)境跑了一下,發(fā)現(xiàn)了個(gè)神奇的現(xiàn)象:

          • 下游服務(wù)收到的json字符串中的唯一ID是沒(méi)問(wèn)題的,和上游一致
          • 下游服務(wù)經(jīng)過(guò)json.unmarshal反序列化之后唯一ID發(fā)生了變化,和上游不一致

          究竟發(fā)生了什么?
          難道我被智子給監(jiān)控了嗎?
          我不理解 我不明白......

          任何不合理現(xiàn)象背后一定有個(gè)合理的解釋?zhuān)f(wàn)不要像我這樣被玄學(xué)占領(lǐng)了高地。

          分析

          我決定看看究竟是誰(shuí)在搞鬼,現(xiàn)在的矛頭指向了json.unmarshal這個(gè)反序列化的動(dòng)作,于是我寫(xiě)了個(gè)小demo復(fù)現(xiàn)一下:

          package?main

          import?(
          ?"encoding/json"
          ?"fmt"
          ?"reflect"
          )

          func?main()?{
          ?var?request?=?`{"id":7044144249855934983,"name":"demo"}`

          ?var?test?interface{}
          ?err?:=?json.Unmarshal([]byte(request),?&test)
          ?if?err?!=?nil?{
          ??fmt.Println("error:",?err)
          ?}

          ?obj?:=?test.(map[string]interface{})

          ?dealStr,?err?:=?json.Marshal(test)
          ?if?err?!=?nil?{
          ??fmt.Println("error:",?err)
          ?}

          ?id?:=?obj["id"]

          ?//?反序列化之后重新序列化打印
          ?fmt.Println(string(dealStr))
          ?fmt.Printf("%+v\n",?reflect.TypeOf(id).Name())
          ?fmt.Printf("%+v\n",?id.(float64))
          }

          跑一下看看結(jié)果如下:

          {"id":7044144249855935000,"name":"demo"}
          float64
          7.044144249855935e+18

          果然復(fù)現(xiàn)了:

          原始輸入字符串:
          '{"id":7044144249855934983,"name":"demo"}'
          處理后的字符串:
          '{"id":7044144249855935000,"name":"demo"}'

          id從7044144249855934983變成了7044144249855935000,從有效數(shù)字16位之后變?yōu)?00了,所以這個(gè)id無(wú)法從db獲取數(shù)據(jù)。

          于是我谷歌了一波,原來(lái)是這樣的:

          • 在json的規(guī)范中,對(duì)于數(shù)字類(lèi)型是不區(qū)分整形和浮點(diǎn)型的。
          • 在使用json.Unmarshal進(jìn)行json的反序列化的時(shí)候,如果沒(méi)有指定數(shù)據(jù)類(lèi)型,使用interface{}作為接收變量,其默認(rèn)采用的float64作為其數(shù)字的接受類(lèi)型
          • 當(dāng)數(shù)字的精度超過(guò)float能夠表示的精度范圍時(shí)就會(huì)造成精度丟失的問(wèn)題

          到這里,我基本清楚了為什么會(huì)出現(xiàn)bug:

          • 上游的json字符串格式不確定無(wú)法使用struct來(lái)做反序列化,只能借助于interface{}來(lái)接收數(shù)據(jù)
          • 上游的json所傳的id是數(shù)值類(lèi)型,換成字符串類(lèi)型則沒(méi)有這種問(wèn)題
          • 上游的json所傳的id數(shù)值比較大,超過(guò)了float64的安全整數(shù)范圍

          解決方案有兩種:

          • 上游將id改為string傳給下游
          • 下游使用json.number類(lèi)型來(lái)避免對(duì)float64的使用
          package?main

          import?(
          ?"encoding/json"
          ?"fmt"
          ?"strings"
          )

          func?main()?{
          ?var?request?=?`{"id":7044144249855934983}`

          ?var?test?interface{}

          ?decoder?:=?json.NewDecoder(strings.NewReader(request))
          ?decoder.UseNumber()
          ?err?:=?decoder.Decode(&test)
          ?if?err?!=?nil?{
          ??fmt.Println("error:",?err)
          ?}

          ?objStr,?err?:=?json.Marshal(test)
          ?if?err?!=?nil?{
          ??fmt.Println("error:",?err)
          ?}

          ?fmt.Println(string(objStr))
          }

          事情到這里基本已經(jīng)清晰了,改完上線就修復(fù)bug,但是我心中仍然有很多疑惑:

          • 為什么json.unmarshal使用float64來(lái)處理就可能出現(xiàn)精度缺失呢?
            • 缺失的程度是怎樣的?
          • 什么時(shí)候出現(xiàn)精度缺失?
            • 里面有什么規(guī)律嗎?
          • 反序列化時(shí)decoder和unmarshal如何選擇呢?

          雖然問(wèn)題解決了,但是沒(méi)搞清楚上面這些問(wèn)題,相當(dāng)于并沒(méi)有什么收獲,于是我決定探究一番。

          探究

          float64作為雙精度浮點(diǎn)型嚴(yán)格遵循IEEE754的標(biāo)準(zhǔn),因此想要搞清楚為什么float64可能出現(xiàn)精度缺失,就必須要搞清楚二進(jìn)制科學(xué)計(jì)算法和IEEE754標(biāo)準(zhǔn)的基本原理。

          二進(jìn)制的科學(xué)計(jì)數(shù)法

          在聊float64之前,我們先回憶下十進(jìn)制的科學(xué)計(jì)數(shù)法。

          我們?yōu)榱吮阌谟洃浐椭庇^表達(dá),采用科學(xué)記數(shù)法來(lái)編寫(xiě)數(shù)字的方法,它可以容納太大或太小的值,在科學(xué)記數(shù)法中,所有數(shù)字都是這樣編寫(xiě)的:x = y*10^z,此時(shí)的底數(shù)是10。

          比如2000000=2*10^6,確實(shí)更加直觀簡(jiǎn)便,同樣的這種簡(jiǎn)化類(lèi)的需求在二進(jìn)制也存在,于是出現(xiàn)了基于二進(jìn)制的科學(xué)計(jì)數(shù)法。

          二進(jìn)制1010010.110表示為1.010010110 × (2 ^ 6),我們后面要說(shuō)的IEEE754標(biāo)準(zhǔn)本質(zhì)上就是二進(jìn)制科學(xué)計(jì)數(shù)法的工程標(biāo)準(zhǔn)定義

          IEEE754標(biāo)準(zhǔn)的誕生

          在20世紀(jì)六七十年代,各家電腦公司的各個(gè)型號(hào)的電腦,有著千差萬(wàn)別的浮點(diǎn)數(shù)表示,卻沒(méi)有一個(gè)業(yè)界通用的標(biāo)準(zhǔn)。

          在1980年,英特爾公司就推出了單片的8087浮點(diǎn)數(shù)協(xié)處理器,其浮點(diǎn)數(shù)表示法及定義的運(yùn)算具有足夠的合理性、先進(jìn)性,被IEEE采用作為浮點(diǎn)數(shù)的標(biāo)準(zhǔn),于1985年發(fā)布。

          IEEE754(ANSI/IEEE Std 754-1985)是20世紀(jì)80年代以來(lái)最廣泛使用的浮點(diǎn)數(shù)運(yùn)算標(biāo)準(zhǔn),為許多CPU與浮點(diǎn)運(yùn)算器所采用,標(biāo)準(zhǔn)規(guī)定了四種表示浮點(diǎn)數(shù)值的方式:?jiǎn)尉_度(32位)、雙精確度(64位)、延伸單精確度(43位以上很少使用)與延伸雙精確度(79位以上)。

          威廉·墨頓·卡韓(英語(yǔ):William Morton Kahan,1933年6月5日-),生于加拿大安大略多倫多,數(shù)學(xué)家與計(jì)算機(jī)科學(xué)家,專(zhuān)長(zhǎng)于數(shù)值分析,1989年圖靈獎(jiǎng)得主,1994年被提名為ACM院士,現(xiàn)為加州大學(xué)柏克萊分校計(jì)算機(jī)科學(xué)名譽(yù)教授,被稱(chēng)為浮點(diǎn)數(shù)之父。

          老爺子已經(jīng)近90歲了,這是1968年到加州大學(xué)伯克利分校任數(shù)學(xué)與計(jì)算機(jī)科學(xué)教授時(shí)的照片。

          IEEE754的基本原理

          int64是將64bit的數(shù)據(jù)全部用來(lái)存儲(chǔ)數(shù)據(jù),但是float64需要表達(dá)的信息更多,因此float64單純用于數(shù)據(jù)存儲(chǔ)的位數(shù)將小于64bit,這就導(dǎo)致了float64可存儲(chǔ)的最大整數(shù)是小于int64的

          理解這一點(diǎn)非常關(guān)鍵,其實(shí)也比較好理解,64bit每一位都非常重要,但是float64需要拿出其中幾位來(lái)做別的事情,這樣存儲(chǔ)數(shù)據(jù)的range就比int64小了許多。

          IEEE754標(biāo)準(zhǔn)將64位分為三部分:

          • sign,符號(hào)位部分,1個(gè)bit 0為正數(shù),1為負(fù)數(shù)
          • exponent,指數(shù)部分,11個(gè)bit
          • fraction,小數(shù)部分,52個(gè)bit

          32位的單精度也分為上述三個(gè)部分,區(qū)別在于指數(shù)部分是8bit,小數(shù)部分是23bit,同時(shí)指數(shù)部分的偏移值32位是127,64位是1023,其他的部分計(jì)算規(guī)則是一樣的。

          IEEE754標(biāo)準(zhǔn)可以認(rèn)為是二進(jìn)制的科學(xué)計(jì)數(shù)法,該標(biāo)準(zhǔn)認(rèn)為任何一個(gè)數(shù)字都可以表示為:

          特別注意,圖片中的指數(shù)部分E并沒(méi)有包含偏移值,偏移值是IEEE754轉(zhuǎn)換為浮點(diǎn)數(shù)二進(jìn)制序列時(shí)使用的。

          • 有效數(shù)字M的約束

          M的取值為1≤M<2,M可以寫(xiě)成1.xxxxxx的形式,其中xxxxxx表示小數(shù)部分。IEEE 754規(guī)定,在計(jì)算機(jī)內(nèi)部保存M時(shí),默認(rèn)這個(gè)數(shù)的第一位總是1,因此可以被舍去,只保存后面的xxxxxx部分,在恢復(fù)計(jì)算時(shí)加上1即可。

          • 指數(shù)E的約束

          E為一個(gè)無(wú)符號(hào)整數(shù)也就是都是>=0,在32位單精度時(shí)取值范圍為0~255,在64位雙精度時(shí)取值范圍為0~2047。當(dāng)數(shù)字是小數(shù)時(shí)E將是負(fù)數(shù),為此IEEE754規(guī)定使用科學(xué)計(jì)數(shù)法求的真實(shí)E加上偏移值才是最終表示的E值。

          看到這里讀者會(huì)有疑問(wèn):如果真實(shí)E值超過(guò)128,那么加上偏移值豈不是要超過(guò)255發(fā)生越界了?

          沒(méi)錯(cuò),當(dāng)指數(shù)部分E全部為1時(shí),需要看M的情況,如果有效數(shù)字M全為0,表示±無(wú)窮大,如果有效數(shù)字M不全為0,表示為NaN。

          NaN(Not a Number非數(shù))是計(jì)算機(jī)科學(xué)中數(shù)值數(shù)據(jù)類(lèi)型的一類(lèi)值,含義為未定義或不可表示的值。

          數(shù)據(jù)表示規(guī)則

          前面了解了IEEE754的基本原理,接下來(lái)就是實(shí)際應(yīng)用了。

          一般來(lái)說(shuō)10進(jìn)制場(chǎng)景下存在三種情況轉(zhuǎn)換為浮點(diǎn)型:

          • 純整數(shù)轉(zhuǎn)換為浮點(diǎn)數(shù) 比如 10086
          • 混合小數(shù)轉(zhuǎn)換為浮點(diǎn)數(shù) 比如 123.45
          • 純小數(shù)轉(zhuǎn)換為浮點(diǎn)數(shù) 比如 0.12306

          就分為兩種情況將10進(jìn)制全部轉(zhuǎn)換為2進(jìn)制就可以了,比如整數(shù)部分123就輾轉(zhuǎn)除2取余數(shù)再逆向書(shū)寫(xiě)就好,小數(shù)部分則是輾轉(zhuǎn)乘2取整再順序書(shū)寫(xiě)就好。

          偷個(gè)懶從菜鳥(niǎo)教程網(wǎng)站上copy個(gè)例子,將10進(jìn)制173.8625轉(zhuǎn)換為2進(jìn)制的做法:

          • 十進(jìn)制整數(shù)轉(zhuǎn)換為二進(jìn)制整數(shù)采用"除2取余,逆序排列"法
          • 十進(jìn)制小數(shù)轉(zhuǎn)換成二進(jìn)制小數(shù)采用"乘2取整,順序排列"法
          • 合并兩部分
          (173.8125)10=(10101101.1101)2

          特別注意,在某些情況下小數(shù)部分的乘2取整會(huì)出現(xiàn)無(wú)限循環(huán),但是IEEE754中小數(shù)部分的位數(shù)是有限的,這樣就出現(xiàn)了近似值存儲(chǔ),這也是一種精度缺失的現(xiàn)象。

          安全整數(shù)范圍

          我們之前有疑問(wèn):任何整數(shù)經(jīng)過(guò)float64處理后都有問(wèn)題嗎?還是說(shuō)有個(gè)安全轉(zhuǎn)換的數(shù)值范圍呢?

          我們來(lái)分析下float64可以表示的數(shù)據(jù)范圍是怎樣的:

          尾數(shù)部分全部為1時(shí)就已經(jīng)拉滿(mǎn)了,再多1位尾數(shù)就要向指數(shù)發(fā)生進(jìn)位,此時(shí)就會(huì)出現(xiàn)精度缺失,因此對(duì)于float64來(lái)說(shuō):

          • 最大的安全整數(shù)是52位尾數(shù)全為1且指數(shù)部分為最小 0x001F FFFF FFFF FFFF
          • float64可以存儲(chǔ)的最大整數(shù)是52位尾數(shù)全位1且指數(shù)部分為最大 0x07FEF FFFF FFFF FFFF
          (0x001F?FFFF?FFFF?FFFF)16?=?(9007199254740991)10
          (0x07EF?FFFF?FFFF?FFFF)16?=?(9218868437227405311)10

          也就是理論上數(shù)值超過(guò)9007199254740991就可能會(huì)出現(xiàn)精度缺失。

          10進(jìn)制數(shù)值的有效數(shù)字是16位,一旦超過(guò)16位基本上缺失精度是沒(méi)跑了,回過(guò)頭看我處理的id是20位長(zhǎng)度,所以必然出現(xiàn)精度缺失。

          decoder和unmarshal

          我們知道在json反序列化時(shí)是沒(méi)有整型和浮點(diǎn)型的區(qū)別,數(shù)字都使用同一種類(lèi)型,在go語(yǔ)言的類(lèi)型中這種共同類(lèi)型就是float64。

          但是float64存在精度缺失的問(wèn)題,因此go單獨(dú)對(duì)此給出了一個(gè)解決方案:

          • 使用 json.Decoder 來(lái)代替 json.Unmarshal 方法
          • 該方案首先創(chuàng)建了一個(gè) jsonDecoder,然后調(diào)用了 UseNumber 方法
          • 使用 UseNumber 方法后,json 包會(huì)將數(shù)字轉(zhuǎn)換成一個(gè)內(nèi)置的 Number 類(lèi)型(本質(zhì)是string),Number類(lèi)型提供了轉(zhuǎn)換為 int64、float64 等多個(gè)方法

          UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64

          我們來(lái)看看Number類(lèi)型的源碼實(shí)現(xiàn):

          //?A?Number?represents?a?JSON?number?literal.
          type?Number?string

          //?String?returns?the?literal?text?of?the?number.
          func?(n?Number)?String()?string?{?return?string(n)?}

          //?Float64?returns?the?number?as?a?float64.
          func?(n?Number)?Float64()?(float64,?error)?{
          ????return?strconv.ParseFloat(string(n),?64)
          }

          //?Int64?returns?the?number?as?an?int64.
          func?(n?Number)?Int64()?(int64,?error)?{
          ????return?strconv.ParseInt(string(n),?10,?64)
          }

          從上面可以看到j(luò)son包的NewDecoder和unmarshal都可以實(shí)現(xiàn)數(shù)據(jù)的解析,那么二者有何區(qū)別,什么時(shí)候選擇哪種方法呢?

          https://stackoverflow.com/questions/21197239/decoding-json-using-json-unmarshal-vs-json-newdecoder-decode

          其中的高贊答案給出了一些觀點(diǎn):

          • json.NewDecoder是從一個(gè)流里面直接進(jìn)行解碼,代碼更少,可以用于http連接與socket連接的讀取與寫(xiě)入,或者文件讀取
          • json.Unmarshal是從已存在與內(nèi)存中的json進(jìn)行解碼

          小結(jié)

          到這里大部分問(wèn)題已經(jīng)搞清楚,但是仍然一些疑問(wèn)沒(méi)有搞清楚:

          • 為什么json.unmarshal沒(méi)有直接只用類(lèi)似于decode方案中的Number類(lèi)型來(lái)避免float64帶來(lái)的精度損失?
          • json.unmarshal反序列化過(guò)程的詳細(xì)原理是怎樣的?

          這兩個(gè)疑問(wèn)或許存在某些關(guān)聯(lián),等我研究明白再寫(xiě)吧!

          瀏覽 141
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人性生活视预 | 翔田千里无码乱伦 | xxxx无码 | 翔田千里无码流出修正 | 91成长视频蘑菇视频在线观看 |