最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

起源

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

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

后文中我會用一個更簡化的例子描述這個 BUG。

1 gob 與零值

先簡單介紹一下gob和零值。

1.1 零值

零值是 Go 語言中的一個特性,簡單說就是:Go 語言會給一些沒有被賦值的變量提供一個默認值。譬如下面這段代碼:

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)
}
/* 結果輸出
[] 0  map[]
{name: gender:0 age:0}
*/

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

1.2 gob

gob是 Go 語言自帶的標準庫,在encoding/gob中。gob其實是go binary的簡寫,因此從它的名稱我們也可以猜到,gob應當與二進制相關。

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

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

2 問題

2.1 需求

在本文的開頭,我簡單敘述了問題的起源,這里我用一個更簡單的模型來展開描述。

首先我們定義一個名為person的結構體:

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

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

接著,我們有這樣一個需求:

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

2.2 代碼

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

  • defines.go
// .gob 文件所在目錄
const DIR = "./persons"
type person struct {
    // 和 json 庫一樣,字段首字母必須大寫(公有)才能序列化
    ID     int
    Name   string // 姓名
    Gender int    // 性別:男 1,女 0
    Age    int    // 年齡
}
// 需要持久化的對象們
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 對象序列化后存儲到文件,
// 文件名為 ./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)
    // 用一個臨時指針去作為文件中對象的載體,以節(jié)省新建對象的開銷。
    tmpP := &person{}
    for _, p := range persons {
        // 方便起見,這里直接遍歷 persons ,但只取 ID 用于讀文件
        id := p.ID
        filename := filepath.Join(DIR, fmt.Sprintf("%d.gob", id))
        // 反序列化對象到 tmpP 中
        unserialize(filename, tmpP)
        // 統(tǒng)計性別
        counter[tmpP.Gender]++
    }
    fmt.Printf("Female: %+v, Male: %+v\n", counter[0], counter[1])
}

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

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

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

2.3 定位

我們在countGender()函數(shù)中的for循環(huán)里添加一行打印語句,將每次讀取到的person對象讀出來,然后得到了這樣的結果:

// 添加行
fmt.Printf("%+v\n", tmpP)
// 結果輸出
&{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 這一項外,其他所有的數(shù)據(jù)都正常!看到這一結果,如果大家和我一樣,平時經常和 JSON、Yml 之類的配置文件打交道,很可能會想當然地認為:上面的 gob 文件讀取正常,應當是存儲出了問題。

gob文件是二進制文件,我們難以像 JSON 文件那樣用肉眼去驗證。即便在 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                                  .*.

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

2.4 規(guī)律

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

// 第一組,先女后男
var persons = []person{
    {0, "Mia", 0, 21},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
}
// 結果輸出
&{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},
}
// 結果輸出
&{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)了,先女后男時,結果一切正常;先男后女時,男性正常,女性全都不正常,甚至 Mia 原本為 0 的 ID 這里也變成了 2!

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

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

3 答案

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

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

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

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

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

for _, p := range persons {
    // ...
    tmpP := &person{}
    // ...
}
// 結果輸出
&{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

對了!

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

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

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

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

翻譯一下:

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

這句話的前后文是在說struct,因此這里的field指的也是結構體中的字段,符合我們文中的例子。

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

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

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

4 回顧

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

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

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

相關文章

  • Golang使用協(xié)程實現(xiàn)批量獲取數(shù)據(jù)

    Golang使用協(xié)程實現(xiàn)批量獲取數(shù)據(jù)

    服務端經常需要返回一個列表,里面包含很多用戶數(shù)據(jù),常規(guī)做法當然是遍歷然后讀緩存。使用Go語言后,可以并發(fā)獲取,極大提升效率,本文就來聊聊具體的實現(xiàn)方法,希望對大家有所幫助
    2023-02-02
  • 深入淺出Go語言:手把手教你高效生成與解析JSON數(shù)據(jù)

    深入淺出Go語言:手把手教你高效生成與解析JSON數(shù)據(jù)

    本文將帶你一步步走進Go語言的世界,教你如何高效生成與解析JSON數(shù)據(jù),無論你是初學者還是經驗豐富的開發(fā)者,都能在本文中找到實用的技巧和靈感,本文內容簡潔明了,示例豐富,讓你在閱讀的過程中輕松掌握Go語言生成與解析JSON數(shù)據(jù)的技巧,需要的朋友可以參考下
    2024-02-02
  • golang中判斷請求是http還是https獲取當前訪問地址

    golang中判斷請求是http還是https獲取當前訪問地址

    這篇文章主要為大家介紹了golang中判斷請求是http還是https獲取當前訪問地址示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-10-10
  • go語言實現(xiàn)一個簡單的http客戶端抓取遠程url的方法

    go語言實現(xiàn)一個簡單的http客戶端抓取遠程url的方法

    這篇文章主要介紹了go語言實現(xiàn)一個簡單的http客戶端抓取遠程url的方法,實例分析了Go語言http操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-03-03
  • 詳解Go程序添加遠程調用tcpdump功能

    詳解Go程序添加遠程調用tcpdump功能

    這篇文章主要介紹了go程序添加遠程調用tcpdump功能,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-05-05
  • 深入解析golang?bufio

    深入解析golang?bufio

    這篇文章主要介紹了golang?bufio解析,golang的bufio庫使用緩存來一次性進行大塊數(shù)據(jù)的讀寫,以此降低IO系統(tǒng)調用,提升性能,需要的朋友可以參考下
    2022-04-04
  • Go Gin中間件的具體使用

    Go Gin中間件的具體使用

    中間件是Gin框架中的一個重要概念,它可以用來處理HTTP請求和響應,或者在處理請求之前和之后執(zhí)行一些操作,本文主要介紹了Go Gin中間件的具體使用,感興趣的可以了解一下
    2023-11-11
  • golang 使用 viper 讀取自定義配置文件

    golang 使用 viper 讀取自定義配置文件

    這篇文章主要介紹了golang 使用 viper 讀取自定義配置文件,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-01-01
  • Go中的 panic / recover 簡介與實踐記錄

    Go中的 panic / recover 簡介與實踐記錄

    這篇文章主要介紹了Go中的 panic / recover 簡介與實踐,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-04-04
  • go使用SQLX操作MySQL數(shù)據(jù)庫的教程詳解

    go使用SQLX操作MySQL數(shù)據(jù)庫的教程詳解

    sqlx 是 Go 語言中一個流行的操作數(shù)據(jù)庫的第三方包,它提供了對 Go 標準庫 database/sql 的擴展,簡化了操作數(shù)據(jù)庫的步驟,下面我們就來學習一下go如何使用SQLX實現(xiàn)MySQL數(shù)據(jù)庫的一些基本操作吧
    2023-11-11

最新評論

武川县| 汽车| 武胜县| 凌源市| 普兰店市| 巫山县| 河津市| 滦平县| 蓝山县| 崇仁县| 大田县| 麻城市| 沛县| 孟连| 红河县| 开远市| 河西区| 同仁县| 石屏县| 马关县| 鹿邑县| 涿州市| 裕民县| 雅江县| 黄浦区| 陈巴尔虎旗| 尼勒克县| 博湖县| 英山县| 平遥县| 霍林郭勒市| 沙坪坝区| 商丘市| 昌图县| 宾川县| 仙居县| 乌什县| 民权县| 阳城县| 闽清县| 旬邑县|