Go語言第一深坑 - interface 與 nil 的比較

interface簡介

Go語言以簡單易上手而著稱,它的語法很是簡單,熟悉C++,Java的開發者只須要很短的時間就能夠掌握Go語言的基本用法。git

interface是Go語言裏所提供的很是重要的特性。一個interface裏能夠定義一個或者多個函數,例如系統自帶的io.ReadWriter的定義以下所示:github

type ReadWriter interface { Read(b []byte) (n int, err error) Write(b []byte) (n int, err error) }

任何類型只要它提供了Read和Write的綁定函數實現,Go就認爲這個類型實現了這個interface(duck-type),而不像Java須要開發者使用implements標明。redis

然而Go語言的interface在使用過程當中卻有一個特別坑的特性,當你比較一個interface類型的值是不是nil的時候,這是須要特別注意避免的問題。數據庫

一次真實的踩坑

這是咱們在GoWorld分佈式遊戲服務器的開發中,碰到的一個實際的bug。因爲GoWorld支持多種不一樣的數據庫(包括MongoDB,Redis等)來保存服務端對象,所以GoWorld在上層提供了一個統一的對象存儲接口定義,而不一樣的對象數據庫實現只須要實現EntityStorage接口所提供的函數便可。服務器

// EntityStorage defines the interface of entity storage backends type EntityStorage interface { List(typeName string) ([]common.EntityID, error) Write(typeName string, entityID common.EntityID, data interface{}) error Read(typeName string, entityID common.EntityID) (interface{}, error) Exists(typeName string, entityID common.EntityID) (bool, error) Close() IsEOF(err error) bool }

以一個使用Redis做爲對象數據庫的實現爲例,函數OpenRedis鏈接Redis數據庫並最終返回一個redisEntityStorage對象的指針。分佈式

// OpenRedis opens redis as entity storage func OpenRedis(url string, dbindex int) *redisEntityStorage { c, err := redis.DialURL(url) if err != nil { return nil } if dbindex >= 0 { if _, err := c.Do("SELECT", dbindex); err != nil { return nil } } es := &redisEntityStorage{ c: c, } return es }

在上層邏輯中,咱們使用OpenRedis函數鏈接Redis數據庫,並將返回的redisEntityStorage指針賦值個一個EntityStorage接口變量,由於redisEntityStorage對象實現了EntityStorage接口所定義的全部函數。函數

var storageEngine StorageEngine // 這是一個全局變量 storageEngine = OpenRedis(cfg.Url, dbindex) if storageEngine != nil { // 鏈接成功 ... } else { // 鏈接失敗 ... }

上面的代碼看起來都很正常,OpenRedis在鏈接Redis數據庫失敗的時候會返回nil,而後調用者將返回值和nil進行比較,來判斷是否鏈接成功。這個就是Go語言少有的幾個深坑之一,由於無論OpenRedis函數是否鏈接Redis成功,都會運行鏈接成功的邏輯。學習

尋找問題所在

想要理解這個問題,首先須要理解interface{}變量的本質。在Go語言中,一個interface{}類型的變量包含了2個指針,一個指針指向值的類型,另一個指針指向實際的值。 咱們能夠用以下的測試代碼進行驗證。測試

// InterfaceStructure 定義了一個interface{}的內部結構 type InterfaceStructure struct { pt uintptr // 到值類型的指針 pv uintptr // 到值內容的指針 } // asInterfaceStructure 將一個interface{}轉換爲InterfaceStructure func asInterfaceStructure (i interface{}) InterfaceStructure { return *(*InterfaceStructure)(unsafe.Pointer(&i)) } func TestInterfaceStructure(t *testing.T) { var i1, i2 interface{} var v1 int = 0x0AAAAAAAAAAAAAAA var v2 int = 0x0BBBBBBBBBBBBBBB i1 = v1 i2 = v2 fmt.Printf("sizeof interface{} = %d\n", unsafe.Sizeof(i1)) fmt.Printf("i1 %x %+v\n", i1, asInterfaceStructure(i1)) fmt.Printf("i2 %x %+v\n", i2, asInterfaceStructure(i2)) var nilInterface interface{} fmt.Printf("nil interface = %+v\n", asInterfaceStructure(nilInterface)) }

這段代碼的輸出以下:ui

sizeof interface{} = 16 i1 aaaaaaaaaaaaaaa {pt:5328736 pv:825741282816} i2 bbbbbbbbbbbbbbb {pt:5328736 pv:825741282824} nil interface = {pt:0 pv:0}

因此對於一個interface{}類型的nil變量來講,它的兩個指針都是0。這是符合Go語言對nil的標準定義的。在Go語言中,nil是零值(Zero Value),而在Java之類的語言裏,null其實是空指針。關於零值和空指針有什麼區別,這裏就再也不展開了。

當咱們將一個具體類型的值賦值給一個interface類型的變量的時候,就同時把類型和值都賦值給了interface裏的兩個指針。若是這個具體類型的值是nil的話,interface變量依然會存儲對應的類型指針和值指針。

func TestAssignInterfaceNil(t *testing.T) { var p *int = nil var i interface{} = p fmt.Printf("%v %+v is nil %v\n", i, asInterfaceStructure(i), i == nil) }

輸入以下:

<nil> {pt:5300576 pv:0} is nil false

可見,在這種狀況下,雖然咱們把一個nil值賦值給interface{},可是實際上interface裏依然存了指向類型的指針,因此拿這個interface變量去和nil常量進行比較的話就會返回false

如何解決這個問題

想要避開這個Go語言的坑,咱們要作的就是避免將一個有可能爲nil的具體類型的值賦值給interface變量。以上述的OpenRedis爲例,一種方法是先對OpenRedis返回的結果進行非-nil檢查,而後再賦值給interface變量,以下所示。

var storageEngine StorageEngine // 這是一個全局變量 redis := OpenRedis(cfg.Url, dbindex) if redis != nil { // 鏈接成功 storageEngine = redis // 肯定redis不是nil以後再賦值給interface變量 } else { // 鏈接失敗 ... }

另一種方法是讓OpenRedis函數直接返回EntityStorage接口類型的值,這樣就能夠把OpenRedis的返回值直接正確賦值給EntityStorage接口變量。

// OpenRedis opens redis as entity storage func OpenRedis(url string, dbindex int) EntityStorage { c, err := redis.DialURL(url) if err != nil { return nil } if dbindex >= 0 { if _, err := c.Do("SELECT", dbindex); err != nil { return nil } } es := &redisEntityStorage{ c: c, } return es }

至於那種方法更好,就見仁見智了。但願你們在實際項目中不要踩坑,即便踩了也能快速跳出來!

開源分佈式遊戲服務器引擎:https://github.com/xiaonanln/goworld,歡迎賞星,共同窗習

對Go語言服務端開發感興趣的朋友歡迎加入QQ討論羣:662182346

相關文章
相關標籤/搜索