Go語言interface 與 nil 的比較
interface簡介
Go語言以簡單易上手而著稱,它的語法非常簡單,熟悉C++,Java的開發(fā)者只需要很短的時間就可以掌握Go語言的基本用法。
interface是Go語言里所提供的非常重要的特性。一個interface里可以定義一個或者多個函數(shù),例如系統(tǒng)自帶的io.ReadWriter的定義如下所示:
type ReadWriter interface { Read(b []byte) (n int, err error) Write(b []byte) (n int, err error) }
任何類型只要它提供了Read和Write的綁定函數(shù)實現(xiàn),Go就認(rèn)為這個類型實現(xiàn)了這個interface(duck-type),而不像Java需要開發(fā)者使用implements標(biāo)明。
然而Go語言的interface在使用過程中卻有一個特別坑的特性,當(dāng)你比較一個interface類型的值是否是nil的時候,這是需要特別注意避免的問題。
一次真實的踩坑
這是我們在GoWorld分布式游戲服務(wù)器的開發(fā)中,碰到的一個實際的bug。由于GoWorld支持多種不同的數(shù)據(jù)庫(包括MongoDB,Redis等)來保存服務(wù)端對象,因此GoWorld在上層提供了一個統(tǒng)一的對象存儲接口定義,而不同的對象數(shù)據(jù)庫實現(xiàn)只需要實現(xiàn)EntityStorage接口所提供的函數(shù)即可。
// 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作為對象數(shù)據(jù)庫的實現(xiàn)為例,函數(shù)OpenRedis連接Redis數(shù)據(jù)庫并最終返回一個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函數(shù)連接Redis數(shù)據(jù)庫,并將返回的redisEntityStorage指針賦值個一個EntityStorage接口變量,因為redisEntityStorage對象實現(xiàn)了EntityStorage接口所定義的所有函數(shù)。
var storageEngine StorageEngine // 這是一個全局變量 storageEngine = OpenRedis(cfg.Url, dbindex) if storageEngine != nil { // 連接成功 ... } else { // 連接失敗 ... }
上面的代碼看起來都很正常,OpenRedis在連接Redis數(shù)據(jù)庫失敗的時候會返回nil,然后調(diào)用者將返回值和nil進(jìn)行比較,來判斷是否連接成功。這個就是Go語言少有的幾個深坑之一,因為不管OpenRedis函數(shù)是否連接Redis成功,都會運(yùn)行連接成功的邏輯。
尋找問題所在
想要理解這個問題,首先需要理解interface{}變量的本質(zhì)。在Go語言中,一個interface{}類型的變量包含了2個指針,一個指針指向值的類型,另外一個指針指向?qū)嶋H的值。 我們可以用如下的測試代碼進(jìn)行驗證。
// InterfaceStructure 定義了一個interface{}的內(nèi)部結(jié)構(gòu) type InterfaceStructure struct { pt uintptr // 到值類型的指針 pv uintptr // 到值內(nèi)容的指針 } // asInterfaceStructure 將一個interface{}轉(zhuǎn)換為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)) }
這段代碼的輸出如下:
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的標(biāo)準(zhǔn)定義的。在Go語言中,nil是零值(Zero Value),而在Java之類的語言里,null實際上是空指針。關(guān)于零值和空指針有什么區(qū)別,這里就不再展開了。
當(dāng)我們將一個具體類型的值賦值給一個interface類型的變量的時候,就同時把類型和值都賦值給了interface里的兩個指針。如果這個具體類型的值是nil的話,interface變量依然會存儲對應(yīng)的類型指針和值指針。
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常量進(jìn)行比較的話就會返回false。
如何解決這個問題
想要避開這個Go語言的坑,我們要做的就是避免將一個有可能為nil的具體類型的值賦值給interface變量。以上述的OpenRedis為例,一種方法是先對OpenRedis返回的結(jié)果進(jìn)行非-nil檢查,然后再賦值給interface變量,如下所示。
var storageEngine StorageEngine // 這是一個全局變量 redis := OpenRedis(cfg.Url, dbindex) if redis != nil { // 連接成功 storageEngine = redis // 確定redis不是nil之后再賦值給interface變量 } else { // 連接失敗 ... }
另外一種方法是讓OpenRedis函數(shù)直接返回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 }
至于那種方法更好,就見仁見智了。希望大家在實際項目中不要踩坑,即使踩了也能快速跳出來!
相關(guān)文章
更高效的GoLevelDB:shardingdb實現(xiàn)分片和并發(fā)讀寫操作
這篇文章主要介紹了更高效的GoLevelDB:shardingdb實現(xiàn)分片和并發(fā)讀寫操作的相關(guān)資料,需要的朋友可以參考下2023-09-09go從指定的URL下載圖片并保存到本地的代碼實現(xiàn)
這段代碼定義了一個名為 downloadImage 的函數(shù),其目的是從指定的URL下載圖片并保存到本地文件系統(tǒng),本文是對代碼功能的詳細(xì)描述,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-08-08Golang實現(xiàn)不被復(fù)制的結(jié)構(gòu)體的方法
sync包中的許多結(jié)構(gòu)都是不允許拷貝的,因為它們自身存儲了一些狀態(tài)(比如等待者的數(shù)量),如果你嘗試復(fù)制這些結(jié)構(gòu)體,就會在你的?IDE中看到警告,那這是怎么實現(xiàn)的呢,下文就來和大家詳細(xì)講講2023-03-03golang中import cycle not allowed解決的一種思路
這篇文章主要給大家介紹了關(guān)于golang中import cycle not allowed解決的一種思路,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08