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

Golang中由零值和gob庫(kù)特性引起B(yǎng)UG解析

 更新時(shí)間:2023年04月27日 09:47:52   作者:杜秉軒  
這篇文章主要為大家介紹了Golang中由零值和gob庫(kù)特性引起B(yǎng)UG解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

起源

就在今年9月份,我負(fù)責(zé)的部門平臺(tái)項(xiàng)目發(fā)布了一個(gè)新版本,該版本同時(shí)上線了一個(gè)新功能,簡(jiǎn)單說(shuō)有點(diǎn)類似定時(shí)任務(wù)。頭一天一切正常,但第二天出現(xiàn)了極少數(shù)任務(wù)沒(méi)有正常執(zhí)行(已經(jīng)暫停的任務(wù)繼續(xù)執(zhí)行,正常的任務(wù)反而沒(méi)有執(zhí)行)的情況。

問(wèn)題的現(xiàn)象讓我和另一個(gè)同事的第一反應(yīng)是定時(shí)任務(wù)執(zhí)行的邏輯出現(xiàn)了問(wèn)題。但在我們耗費(fèi)了大量的時(shí)間去DEBUG、測(cè)試后,發(fā)現(xiàn)問(wèn)題的根本并不在功能邏輯,而是一段已經(jīng)上線了一年并且沒(méi)有動(dòng)過(guò)的底層公共代碼。這段代碼的核心就是本篇文章的主人公gob,引發(fā)問(wèn)題的根源則是go語(yǔ)言的一個(gè)特性:零值

后文中我會(huì)用一個(gè)更簡(jiǎn)化的例子描述這個(gè) BUG。

1 gob 與零值

先簡(jiǎn)單介紹一下gob和零值。

1.1 零值

零值是 Go 語(yǔ)言中的一個(gè)特性,簡(jiǎn)單說(shuō)就是:Go 語(yǔ)言會(huì)給一些沒(méi)有被賦值的變量提供一個(gè)默認(rèn)值。譬如下面這段代碼:

package main
import (
    "fmt"
)
type person struct {
    name   string
    gender int
    age    int
}
func main() {
    p := person{}
    var list []byte
    var f float32
    var s string
    var m map[string]int
    fmt.Println(list, f, s, m)
    fmt.Printf("%+v", p)
}
/* 結(jié)果輸出
[] 0  map[]
{name: gender:0 age:0}
*/

零值在很多時(shí)候確實(shí)為開(kāi)發(fā)者帶來(lái)了方便,但也有許多不喜歡它的人認(rèn)為零值的存在使得代碼從語(yǔ)法層面上不嚴(yán)謹(jǐn),帶來(lái)了一些不確定性。譬如我即將在后文中詳細(xì)描述的問(wèn)題。

1.2 gob

gob是 Go 語(yǔ)言自帶的標(biāo)準(zhǔn)庫(kù),在encoding/gob中。gob其實(shí)是go binary的簡(jiǎn)寫,因此從它的名稱我們也可以猜到,gob應(yīng)當(dāng)與二進(jìn)制相關(guān)。

實(shí)際上gobGo 語(yǔ)言獨(dú)有的以二進(jìn)制形式序列化和反序列化程序數(shù)據(jù)的格式,類似 Python 中的 pickle。它最常見(jiàn)的用法是將一個(gè)對(duì)象(結(jié)構(gòu)體)序列化后存儲(chǔ)到磁盤文件,在需要使用的時(shí)候再讀取文件并反序列化出來(lái),從而達(dá)到對(duì)象持久化的效果。

例子我就不舉了,本篇也不是gob的使用專題。這是它的**官方文檔**,對(duì)gob用法不熟悉的朋友們可以看一下文檔中的Example部分,或者直接看我后文中描述問(wèn)題用到的例子。

2 問(wèn)題

2.1 需求

在本文的開(kāi)頭,我簡(jiǎn)單敘述了問(wèn)題的起源,這里我用一個(gè)更簡(jiǎn)單的模型來(lái)展開(kāi)描述。

首先我們定義一個(gè)名為person的結(jié)構(gòu)體:

type person struct {
    // 和 json 庫(kù)一樣,字段首字母必須大寫(公有)才能序列化
    ID     int
    Name   string // 姓名
    Gender int    // 性別:男 1,女 0
    Age    int    // 年齡
}

圍繞這個(gè)結(jié)構(gòu)體,我們會(huì)錄入若干個(gè)人員信息,每一個(gè)人員都是一個(gè)person對(duì)象。但出于一些原因,我們必須使用gob將這些人員信息持久化到本地磁盤,而不是使用 MySQL 之類的數(shù)據(jù)庫(kù)。

接著,我們有這樣一個(gè)需求:

遍歷并反序列化本地存儲(chǔ)的gob文件,然后判斷男女性別的數(shù)量,并統(tǒng)計(jì)。

2.2 代碼

根據(jù)上面的需求和背景,代碼如下(為了節(jié)省篇幅,這里省略了 package, import, init() 等代碼):

  • defines.go
// .gob 文件所在目錄
const DIR = "./persons"
type person struct {
    // 和 json 庫(kù)一樣,字段首字母必須大寫(公有)才能序列化
    ID     int
    Name   string // 姓名
    Gender int    // 性別:男 1,女 0
    Age    int    // 年齡
}
// 需要持久化的對(duì)象們
var persons = []person{
    {0, "Mia", 0, 21},
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
}
  • serializer.go
// serialize 將 person 對(duì)象序列化后存儲(chǔ)到文件,
// 文件名為 ./persons/${p.id}.gob
func serialize(p person) {
    filename := filepath.Join(DIR, fmt.Sprintf("%d.gob", p.ID))
    buffer := new(bytes.Buffer)
    encoder := gob.NewEncoder(buffer)
    _ = encoder.Encode(p)
    _ = ioutil.WriteFile(filename, buffer.Bytes(), 0644)
}
// unserialize 將 .gob 文件反序列化后存入指針參數(shù)
func unserialize(path string, p *person) {
    raw, _ := ioutil.ReadFile(path)
    buffer := bytes.NewBuffer(raw)
    decoder := gob.NewDecoder(buffer)
    _ = decoder.Decode(p)
}
  • main.go
func main() {
    storePersons()
    countGender()
}
func storePersons() {
    for _, p := range persons {
        serialize(p)
    }
}
func countGender() {
    counter := make(map[int]int)
    // 用一個(gè)臨時(shí)指針去作為文件中對(duì)象的載體,以節(jié)省新建對(duì)象的開(kāi)銷。
    tmpP := &person{}
    for _, p := range persons {
        // 方便起見(jiàn),這里直接遍歷 persons ,但只取 ID 用于讀文件
        id := p.ID
        filename := filepath.Join(DIR, fmt.Sprintf("%d.gob", id))
        // 反序列化對(duì)象到 tmpP 中
        unserialize(filename, tmpP)
        // 統(tǒng)計(jì)性別
        counter[tmpP.Gender]++
    }
    fmt.Printf("Female: %+v, Male: %+v\n", counter[0], counter[1])
}

執(zhí)行代碼后,我們得到了這樣的結(jié)果:

// 對(duì)象們
var persons = []person{
    {0, "Mia", 0, 21},
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
}
// 結(jié)果輸出
Female: 1, Male: 4

嗯?1 個(gè)女性,4 個(gè)男性?BUG出現(xiàn)了,這樣的結(jié)果顯然與我們的預(yù)設(shè)數(shù)據(jù)不符。是哪里出了問(wèn)題?

2.3 定位

我們?cè)?code>countGender()函數(shù)中的for循環(huán)里添加一行打印語(yǔ)句,將每次讀取到的person對(duì)象讀出來(lái),然后得到了這樣的結(jié)果:

// 添加行
fmt.Printf("%+v\n", tmpP)
// 結(jié)果輸出
&{ID:0 Name:Mia Gender:0 Age:21}
&{ID:1 Name:Jim Gender:1 Age:18}
&{ID:2 Name:Bob Gender:1 Age:25}
&{ID:3 Name:Jenny Gender:1 Age:16}
&{ID:4 Name:Marry Gender:1 Age:30}

好家伙,Jenny 和 Marry 都給變成男人了!但神奇的是,除了 Gender 這一項(xiàng)外,其他所有的數(shù)據(jù)都正常!看到這一結(jié)果,如果大家和我一樣,平時(shí)經(jīng)常和 JSON、Yml 之類的配置文件打交道,很可能會(huì)想當(dāng)然地認(rèn)為:上面的 gob 文件讀取正常,應(yīng)當(dāng)是存儲(chǔ)出了問(wèn)題。

gob文件是二進(jìn)制文件,我們難以像 JSON 文件那樣用肉眼去驗(yàn)證。即便在 Linux 下使用xxd之類的工具,也只能得到這樣一種模棱兩可的輸出:

>$ xxd persons/1.gob 
0000000: 37ff 8103 0101 0670 6572 736f 6e01 ff82  7......person...
0000010: 0001 0401 0249 4401 0400 0104 4e61 6d65  .....ID.....Name
0000020: 010c 0001 0647 656e 6465 7201 0400 0103  .....Gender.....
0000030: 4167 6501 0400 0000 0eff 8201 0201 034a  Age............J
0000040: 696d 0102 0124 00                        im...$.
>$ xxd persons/0.gob 
0000000: 37ff 8103 0101 0670 6572 736f 6e01 ff82  7......person...
0000010: 0001 0401 0249 4401 0400 0104 4e61 6d65  .....ID.....Name
0000020: 010c 0001 0647 656e 6465 7201 0400 0103  .....Gender.....
0000030: 4167 6501 0400 0000 0aff 8202 034d 6961  Age..........Mia
0000040: 022a 00                                  .*.

也許我們可以嘗試去硬解析這幾個(gè)二進(jìn)制文件,來(lái)對(duì)比它們之間的差異;或者反序列化兩個(gè)除了 Gender 外一模一樣的對(duì)象到gob文件中,然后對(duì)比。大家如果有興趣的話可以嘗試一下。當(dāng)時(shí)的我們因?yàn)闀r(shí)間緊迫等原因,沒(méi)有嘗試這種做法,而是修改數(shù)據(jù)繼續(xù)測(cè)試。

2.4 規(guī)律

由于上文中出問(wèn)題的兩個(gè)數(shù)據(jù)都是女性,程序員的直覺(jué)告訴我這也許并不是巧合。于是我嘗試修改數(shù)據(jù)的順序,將男女完全分開(kāi),然后進(jìn)行測(cè)試:

// 第一組,先女后男
var persons = []person{
    {0, "Mia", 0, 21},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
}
// 結(jié)果輸出
&{ID:0 Name:Mia Gender:0 Age:21}
&{ID:3 Name:Jenny Gender:0 Age:16}
&{ID:4 Name:Marry Gender:0 Age:30}
&{ID:1 Name:Jim Gender:1 Age:18}
&{ID:2 Name:Bob Gender:1 Age:25}
// 第二組,先男后女
var persons = []person{
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
    {0, "Mia", 0, 21},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
}
// 結(jié)果輸出
&{ID:1 Name:Jim Gender:1 Age:18}
&{ID:2 Name:Bob Gender:1 Age:25}
&{ID:2 Name:Mia Gender:1 Age:21}
&{ID:3 Name:Jenny Gender:1 Age:16}
&{ID:4 Name:Marry Gender:1 Age:30}

吊詭的現(xiàn)象出現(xiàn)了,先女后男時(shí),結(jié)果一切正常;先男后女時(shí),男性正常,女性全都不正常,甚至 Mia 原本為 0 的 ID 這里也變成了 2!

經(jīng)過(guò)反復(fù)地測(cè)試和對(duì)結(jié)果集的觀察,我們得到了這樣一個(gè)有規(guī)律的結(jié)論:所有男性數(shù)據(jù)都正常,出問(wèn)題的全是女性數(shù)據(jù)!

進(jìn)一步公式化描述這個(gè)結(jié)論就是:如果前面的數(shù)據(jù)為非 0 數(shù)字,同時(shí)后面的數(shù)據(jù)數(shù)字為 0 時(shí),則后面的 0 會(huì)被它前面的非 0 所覆蓋。

3 答案

再次審計(jì)程序代碼,我注意到了這一句:

// 用一個(gè)臨時(shí)指針去作為文件中對(duì)象的載體,以節(jié)省新建對(duì)象的開(kāi)銷。
tmpP := &person{}

為了節(jié)省額外的新建對(duì)象的開(kāi)銷,我用了同一個(gè)變量來(lái)循環(huán)加載文件中的數(shù)據(jù),并進(jìn)行性別判定。結(jié)合前面我們發(fā)現(xiàn)的 BUG 規(guī)律,答案似乎近在眼前了:所謂后面的數(shù)據(jù) 0 被前面的非 0 覆蓋,很可能是因?yàn)槭褂昧送粋€(gè)對(duì)象加載文件,導(dǎo)致前面的數(shù)據(jù)殘留。

驗(yàn)證的方法也很簡(jiǎn)單,只需要將那個(gè)公共對(duì)象放到下面的for循環(huán)里,使每一次循環(huán)都重新創(chuàng)建一個(gè)對(duì)象用于加載文件數(shù)據(jù),以切斷上一個(gè)數(shù)據(jù)的影響。

我們修改一下代碼(省略了多余部分):

for _, p := range persons {
    // ...
    tmpP := &person{}
    // ...
}
// 結(jié)果輸出
&{ID:0 Name:Mia Gender:0 Age:21}
&{ID:1 Name:Jim Gender:1 Age:18}
&{ID:2 Name:Bob Gender:1 Age:25}
&{ID:3 Name:Jenny Gender:0 Age:16}
&{ID:4 Name:Marry Gender:0 Age:30}
Female: 3, Male: 2

對(duì)了!

結(jié)果確實(shí)如我們推想,是數(shù)據(jù)殘留的原因。但這里又有一個(gè)問(wèn)題了:為什么先 0 后非 0 (先女后男)的情況下,老方法讀取的數(shù)據(jù)又一切正常呢?以及,除了 0 會(huì)被影響外,其他的數(shù)字(年齡)又都不會(huì)被影響?

所有的問(wèn)題現(xiàn)在似乎都在指向 0 這個(gè)特殊數(shù)字!

直到此時(shí),零值這個(gè)特性才終于被我們察覺(jué)。于是我趕緊閱讀了gob庫(kù)的**官方文檔**,發(fā)現(xiàn)了這么一句話:

If a field has the zero value for its type (except for arrays; see above), it is omitted from the transmission.

翻譯一下:

如果一個(gè)字段的類型擁有零值(數(shù)組除外),它會(huì)在傳輸中被省略。

這句話的前后文是在說(shuō)struct,因此這里的field指的也是結(jié)構(gòu)體中的字段,符合我們文中的例子。

根據(jù)我們前面得到的結(jié)論,以及官方文檔的說(shuō)明,我們現(xiàn)在終于可以得出一個(gè)完整的結(jié)論了:

gob庫(kù)在操作數(shù)據(jù)時(shí),會(huì)忽略數(shù)組之外的零值。而我們的代碼一開(kāi)始使用一個(gè)公共對(duì)象來(lái)加載文件數(shù)據(jù),由于零值不被傳輸,因此原數(shù)據(jù)中為零值的字段就不會(huì)讀到,我們看到的實(shí)際上是上一個(gè)非零值的對(duì)象數(shù)據(jù)。

解決方法也很簡(jiǎn)單,就是我上面做的,不要使用公共對(duì)象去加載就好了。

4 回顧

文章開(kāi)頭我敘述的項(xiàng)目 BUG 里,我使用了 0 和 1 來(lái)表示一個(gè)定時(shí)任務(wù)的狀態(tài)(暫停、運(yùn)行)。就像上面 person.Gender 一樣,不同任務(wù)之間因?yàn)榱阒祮?wèn)題受到了干擾,從而造成了任務(wù)執(zhí)行異常,而不涉及零值的其他字段則一切正常。盡管是線上生產(chǎn)環(huán)境,但所幸問(wèn)題發(fā)現(xiàn)的早,處理的及時(shí),并沒(méi)有造成任何生產(chǎn)事故。但整個(gè)過(guò)程和最終的答案卻深深印在了我的腦海里。

后來(lái)我和我同事簡(jiǎn)單討論過(guò),為什么gob選擇忽略零值?以我的角度來(lái)看,可能是為了節(jié)省空間。而我們一開(kāi)始編寫的代碼,也是為了節(jié)省空間而創(chuàng)建了一個(gè)公共對(duì)象,結(jié)果兩個(gè)節(jié)省空間的邏輯最終碰撞出了一個(gè)隱蔽的 BUG。

以上就是Golang中由零值和gob庫(kù)特性引起B(yǎng)UG解析的詳細(xì)內(nèi)容,更多關(guān)于Golang零值gob庫(kù)引起B(yǎng)UG的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 一文帶你了解Go語(yǔ)言中鎖特性和實(shí)現(xiàn)

    一文帶你了解Go語(yǔ)言中鎖特性和實(shí)現(xiàn)

    Go語(yǔ)言中的sync包主要提供的對(duì)并發(fā)操作的支持,標(biāo)志性的工具有cond(條件變量)?once?(原子性)?還有?鎖,本文會(huì)主要向大家介紹Go語(yǔ)言中鎖的特性和實(shí)現(xiàn),感興趣的可以了解下
    2024-03-03
  • Go語(yǔ)言學(xué)習(xí)之接口類型(interface)詳解

    Go語(yǔ)言學(xué)習(xí)之接口類型(interface)詳解

    接口是用來(lái)定義行為的類型,定義的行為不由接口直接實(shí)現(xiàn),而由通過(guò)方法由定義的類型實(shí)現(xiàn),本文就來(lái)和大家詳細(xì)講講Go語(yǔ)言中接口的使用吧
    2023-03-03
  • GoLang并發(fā)機(jī)制探究goroutine原理詳細(xì)講解

    GoLang并發(fā)機(jī)制探究goroutine原理詳細(xì)講解

    goroutine是Go語(yǔ)言提供的語(yǔ)言級(jí)別的輕量級(jí)線程,在我們需要使用并發(fā)時(shí),我們只需要通過(guò) go 關(guān)鍵字來(lái)開(kāi)啟 goroutine 即可。這篇文章主要介紹了GoLang并發(fā)機(jī)制goroutine原理,感興趣的可以了解一下
    2022-12-12
  • Go利用反射reflect實(shí)現(xiàn)獲取接口變量信息

    Go利用反射reflect實(shí)現(xiàn)獲取接口變量信息

    反射是通過(guò)實(shí)體對(duì)象獲取反射對(duì)象(Value、Type),然后可以操作相應(yīng)的方法。本文將利用Go語(yǔ)言中的反射reflect實(shí)現(xiàn)獲取接口變量信息,需要的可以參考一下
    2022-05-05
  • go語(yǔ)言編程二維碼生成及識(shí)別

    go語(yǔ)言編程二維碼生成及識(shí)別

    這篇文章主要為大家介紹了go語(yǔ)言編程二維碼的生成及識(shí)別示例演示,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-04-04
  • Go語(yǔ)言interface詳解

    Go語(yǔ)言interface詳解

    這篇文章主要介紹了Go語(yǔ)言interface詳解,本文講解了什么是interface、interface類型、interface值、空interface、interface函數(shù)參數(shù)等內(nèi)容,需要的朋友可以參考下
    2014-10-10
  • 如何讓shell終端和goland控制臺(tái)輸出彩色的文字

    如何讓shell終端和goland控制臺(tái)輸出彩色的文字

    這篇文章主要介紹了如何讓shell終端和goland控制臺(tái)輸出彩色的文字的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-05-05
  • Gotify搭建你的消息推送系統(tǒng)

    Gotify搭建你的消息推送系統(tǒng)

    這篇文章主要介紹了Gotify搭建你的消息推送系統(tǒng),今天要分享的是 gotify,是一個(gè)用 go 編寫的消息服務(wù)端,有需要的朋友可以借鑒參考下,希望能夠有所幫助
    2024-01-01
  • go 實(shí)現(xiàn)簡(jiǎn)易端口掃描的示例

    go 實(shí)現(xiàn)簡(jiǎn)易端口掃描的示例

    該功能實(shí)現(xiàn)原理很簡(jiǎn)單,就是發(fā)送socket連接(IP+端口),如果能連接成功,說(shuō)明目標(biāo)主機(jī)開(kāi)放了某端口。當(dāng)要大量掃描端口時(shí),就需要寫并發(fā)編程了。
    2021-05-05
  • go語(yǔ)言解決并發(fā)問(wèn)題小結(jié)

    go語(yǔ)言解決并發(fā)問(wèn)題小結(jié)

    并發(fā)是GO最基本的功能了,但是在傳統(tǒng)的PHP中是比較困難的,如果不借助其它一些擴(kuò)展的話,是做不到并發(fā)的,這篇文章主要介紹了go語(yǔ)言如何解決并發(fā)問(wèn)題,需要的朋友可以參考下
    2024-05-05

最新評(píng)論