欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

揭秘Go Json.Unmarshal精度丟失之謎

 更新時間:2023年08月08日 14:17:13   作者:后端研究所  
我們知道在json反序列化時是沒有整型和浮點型的區(qū)別,數(shù)字都使用同一種類型,在go語言的類型中這種共同類型就是float64,下面我們就來探討一下Json.Unmarshal精度丟失之謎吧

緣起

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

需求大概是這樣的:

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

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

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

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

  • 下游服務收到的json字符串中的唯一ID是沒問題的,和上游一致
  • 下游服務經過json.unmarshal反序列化之后唯一ID發(fā)生了變化,和上游不一致

究竟發(fā)生了什么?

難道我被智子給監(jiān)控了嗎?

我不理解 我不明白......

任何不合理現(xiàn)象背后一定有個合理的解釋,千萬不要像我這樣被玄學占領了高地。

分析

我決定看看究竟是誰在搞鬼,現(xiàn)在的矛頭指向了json.unmarshal這個反序列化的動作,于是我寫了個小demo復現(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))
}

跑一下看看結果如下:

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

果然復現(xiàn)了:

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

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

于是我谷歌了一波,原來是這樣的:

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

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

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

解決方案有兩種:

  • 上游將id改為string傳給下游
  • 下游使用json.number類型來避免對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))
}

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

為什么json.unmarshal使用float64來處理就可能出現(xiàn)精度缺失呢?

缺失的程度是怎樣的?

什么時候出現(xiàn)精度缺失?

里面有什么規(guī)律嗎?

反序列化時decoder和unmarshal如何選擇呢?

雖然問題解決了,但是沒搞清楚上面這些問題,相當于并沒有什么收獲,于是我決定探究一番。

探究

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

二進制的科學計數(shù)法

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

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

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

二進制1010010.110表示為1.010010110 × (2 ^ 6),我們后面要說的IEEE754標準本質上就是二進制科學計數(shù)法的工程標準定義。

IEEE754標準的誕生

在20世紀六七十年代,各家電腦公司的各個型號的電腦,有著千差萬別的浮點數(shù)表示,卻沒有一個業(yè)界通用的標準。

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

IEEE754(ANSI/IEEE Std 754-1985)是20世紀80年代以來最廣泛使用的浮點數(shù)運算標準,為許多CPU與浮點運算器所采用,標準規(guī)定了四種表示浮點數(shù)值的方式:單精確度(32位)、雙精確度(64位)、延伸單精確度(43位以上很少使用)與延伸雙精確度(79位以上)。

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

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

IEEE754的基本原理

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

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

IEEE754標準將64位分為三部分:

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

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

IEEE754標準可以認為是二進制的科學計數(shù)法,該標準認為任何一個數(shù)字都可以表示為:

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

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

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

2.指數(shù)E的約束

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

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

沒錯,當指數(shù)部分E全部為1時,需要看M的情況,如果有效數(shù)字M全為0,表示±無窮大,如果有效數(shù)字M不全為0,表示為NaN。

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

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

前面了解了IEEE754的基本原理,接下來就是實際應用了。

一般來說10進制場景下存在三種情況轉換為浮點型:

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

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

偷個懶從菜鳥教程網站上copy個例子,將10進制173.8625轉換為2進制的做法:

十進制整數(shù)轉換為二進制整數(shù)采用"除2取余,逆序排列"法

十進制小數(shù)轉換成二進制小數(shù)采用"乘2取整,順序排列"法

合并兩部分

(173.8125)10=(10101101.1101)2

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

安全整數(shù)范圍

我們之前有疑問:任何整數(shù)經過float64處理后都有問題嗎?還是說有個安全轉換的數(shù)值范圍呢?

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

尾數(shù)部分全部為1時就已經拉滿了,再多1位尾數(shù)就要向指數(shù)發(fā)生進位,此時就會出現(xiàn)精度缺失,因此對于float64來說:

  • 最大的安全整數(shù)是52位尾數(shù)全為1且指數(shù)部分為最小 0x001F FFFF FFFF FFFF
  • float64可以存儲的最大整數(shù)是52位尾數(shù)全位1且指數(shù)部分為最大 0x07FEF FFFF FFFF FFFF

(0x001F FFFF FFFF FFFF)16 = (9007199254740991)10
(0x07EF FFFF FFFF FFFF)16 = (9218868437227405311)10

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

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

decoder和unmarshal

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

但是float64存在精度缺失的問題,因此go單獨對此給出了一個解決方案:

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

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

我們來看看Number類型的源碼實現(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)
}

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

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

其中的高贊答案給出了一些觀點:

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

小結

到這里大部分問題已經搞清楚,但是仍然一些疑問沒有搞清楚:

  • 為什么json.unmarshal沒有直接只用類似于decode方案中的Number類型來避免float64帶來的精度損失?
  • json.unmarshal反序列化過程的詳細原理是怎樣的?

以上就是揭秘Go Json.Unmarshal精度丟失之謎的詳細內容,更多關于go json.Unmarshal的資料請關注腳本之家其它相關文章!

相關文章

  • Go語言實現(xiàn)對稱加密和非對稱加密的示例代碼

    Go語言實現(xiàn)對稱加密和非對稱加密的示例代碼

    本文主要介紹了Go語言實現(xiàn)對稱加密和非對稱加密的示例代碼,通過實際代碼示例展示了如何在Go中實現(xiàn)這兩種加密方式,具有一定的參考價值,感興趣的可以了解一下
    2024-01-01
  • GO語言實現(xiàn)的端口掃描器分享

    GO語言實現(xiàn)的端口掃描器分享

    這篇文章主要介紹了GO語言實現(xiàn)的端口掃描器分享,本文直接給出實現(xiàn)代碼,代碼中包含大量注釋,需要的朋友可以參考下
    2014-10-10
  • PHP和GO對接ChatGPT實現(xiàn)聊天機器人效果實例

    PHP和GO對接ChatGPT實現(xiàn)聊天機器人效果實例

    這篇文章主要為大家介紹了PHP和GO對接ChatGPT實現(xiàn)聊天機器人效果實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2024-01-01
  • Golang CSP并發(fā)機制及使用模型

    Golang CSP并發(fā)機制及使用模型

    這篇文章主要為大家介紹了Golang CSP并發(fā)機制及使用模型,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-05-05
  • go基礎語法50問及方法詳解

    go基礎語法50問及方法詳解

    這篇文章主要為大家介紹了go基礎語法50問及方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01
  • go get 和 go install 對比介紹

    go get 和 go install 對比介紹

    go install和go get都是Go語言的工具命令,但它們之間有一些區(qū)別。go get:用于從遠程代碼存儲庫(如 GitHub)中下載或更新Go代碼包。go install:用于編譯并安裝 Go 代碼包,本文go get和go install對比介紹的非常詳細,需要的朋友可以參考一下
    2023-04-04
  • Go語言非main包編譯為靜態(tài)庫并使用的示例代碼

    Go語言非main包編譯為靜態(tài)庫并使用的示例代碼

    本文以Windows為例,介紹一下如何將Go的非main包編譯為靜態(tài)庫,用戶又將如何使用。通過實際項目創(chuàng)建常規(guī)工程,通過示例代碼給大家介紹的非常詳細,需要的朋友參考下吧
    2021-07-07
  • go語言中反射機制的三種使用場景

    go語言中反射機制的三種使用場景

    本文主要介紹了go語言中反射機制的三種使用場景,包括JSON解析、ORM框架和接口適配,具有一定的參考價值,感興趣的可以了解一下
    2025-02-02
  • Golang接入釘釘通知的示例代碼

    Golang接入釘釘通知的示例代碼

    本文主要介紹了Golang接入釘釘通知的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-08-08
  • GoFrame框架ORM原生方法對象操作開箱體驗

    GoFrame框架ORM原生方法對象操作開箱體驗

    這篇文章主要為大家介紹了GoFrame框架ORM原生方法對象操作的開箱體驗,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-06-06

最新評論