今天小編給大家分享一下Golang中由零值和gob庫特性引起BUG怎么解決的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
先簡單介紹一下gob
和零值。
零值是 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} */
零值在很多時候確實為開發者帶來了方便,但也有許多不喜歡它的人認為零值的存在使得代碼從語法層面上不嚴謹,帶來了一些不確定性。譬如我即將在后文中詳細描述的問題。
gob
是 Go 語言自帶的標準庫,在encoding/gob
中。gob
其實是go binary
的簡寫,因此從它的名稱我們也可以猜到,gob
應當與二進制相關。
實際上gob
是 Go 語言獨有的以二進制形式序列化和反序列化程序數據的格式,類似 Python 中的 pickle
。它最常見的用法是將一個對象(結構體)序列化后存儲到磁盤文件,在需要使用的時候再讀取文件并反序列化出來,從而達到對象持久化的效果。
例子我就不舉了,本篇也不是gob
的使用專題。這是它的**官方文檔**,對gob
用法不熟悉的朋友們可以看一下文檔中的Example
部分,或者直接看我后文中描述問題用到的例子。
在本文的開頭,我簡單敘述了問題的起源,這里我用一個更簡單的模型來展開描述。
首先我們定義一個名為person
的結構體:
type person struct { // 和 json 庫一樣,字段首字母必須大寫(公有)才能序列化 ID int Name string // 姓名 Gender int // 性別:男 1,女 0 Age int // 年齡 }
圍繞這個結構體,我們會錄入若干個人員信息,每一個人員都是一個person
對象。但出于一些原因,我們必須使用gob
將這些人員信息持久化到本地磁盤,而不是使用 MySQL 之類的數據庫。
接著,我們有這樣一個需求:
遍歷并反序列化本地存儲的gob
文件,然后判斷男女性別的數量,并統計。
根據上面的需求和背景,代碼如下(為了節省篇幅,這里省略了 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 文件反序列化后存入指針參數 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) // 用一個臨時指針去作為文件中對象的載體,以節省新建對象的開銷。 tmpP := &person{} for _, p := range persons { // 方便起見,這里直接遍歷 persons ,但只取 ID 用于讀文件 id := p.ID filename := filepath.Join(DIR, fmt.Sprintf("%d.gob", id)) // 反序列化對象到 tmpP 中 unserialize(filename, tmpP) // 統計性別 counter[tmpP.Gender]++ } fmt.Printf("Female: %+v, Male: %+v\n", counter[0], counter[1]) }
執行代碼后,我們得到了這樣的結果:
// 對象們 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
出現了,這樣的結果顯然與我們的預設數據不符。是哪里出了問題?
我們在countGender()
函數中的for
循環里添加一行打印語句,將每次讀取到的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
這一項外,其他所有的數據都正常!看到這一結果,如果大家和我一樣,平時經常和 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
文件中,然后對比。大家如果有興趣的話可以嘗試一下。當時的我們因為時間緊迫等原因,沒有嘗試這種做法,而是修改數據繼續測試。
由于上文中出問題的兩個數據都是女性,程序員的直覺告訴我這也許并不是巧合。于是我嘗試修改數據的順序,將男女完全分開,然后進行測試:
// 第一組,先女后男 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}
吊詭的現象出現了,先女后男時,結果一切正常;先男后女時,男性正常,女性全都不正常,甚至 Mia 原本為 0 的 ID 這里也變成了 2!
經過反復地測試和對結果集的觀察,我們得到了這樣一個有規律的結論:所有男性數據都正常,出問題的全是女性數據!
進一步公式化描述這個結論就是:如果前面的數據為非 0 數字,同時后面的數據數字為 0 時,則后面的 0 會被它前面的非 0 所覆蓋。
再次審計程序代碼,我注意到了這一句:
// 用一個臨時指針去作為文件中對象的載體,以節省新建對象的開銷。 tmpP := &person{}
為了節省額外的新建對象的開銷,我用了同一個變量來循環加載文件中的數據,并進行性別判定。結合前面我們發現的 BUG 規律,答案似乎近在眼前了:所謂后面的數據 0 被前面的非 0 覆蓋,很可能是因為使用了同一個對象加載文件,導致前面的數據殘留。
驗證的方法也很簡單,只需要將那個公共對象放到下面的for
循環里,使每一次循環都重新創建一個對象用于加載文件數據,以切斷上一個數據的影響。
我們修改一下代碼(省略了多余部分):
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
對了!
結果確實如我們推想,是數據殘留的原因。但這里又有一個問題了:為什么先 0 后非 0 (先女后男)的情況下,老方法讀取的數據又一切正常呢?以及,除了 0 會被影響外,其他的數字(年齡)又都不會被影響?
所有的問題現在似乎都在指向 0 這個特殊數字!
直到此時,零值這個特性才終于被我們察覺。于是我趕緊閱讀了gob
庫的**官方文檔**,發現了這么一句話:
If a field has the zero value for its type (except for arrays; see above), it is omitted from the transmission.
翻譯一下:
如果一個字段的類型擁有零值(數組除外),它會在傳輸中被省略。
這句話的前后文是在說struct
,因此這里的field
指的也是結構體中的字段,符合我們文中的例子。
根據我們前面得到的結論,以及官方文檔的說明,我們現在終于可以得出一個完整的結論了:
gob
庫在操作數據時,會忽略數組之外的零值。而我們的代碼一開始使用一個公共對象來加載文件數據,由于零值不被傳輸,因此原數據中為零值的字段就不會讀到,我們看到的實際上是上一個非零值的對象數據。
解決方法也很簡單,就是我上面做的,不要使用公共對象去加載就好了。
以上就是“Golang中由零值和gob庫特性引起BUG怎么解決”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。