Go疑難雜癥講解之為什么nil不等于nil
現(xiàn)象
在日常開(kāi)發(fā)中,可能一不小心就會(huì)掉進(jìn) Go
語(yǔ)言的某些陷阱里,而本文要介紹的 nil ≠ nil
問(wèn)題,便是其中一個(gè),初看起來(lái)會(huì)讓人覺(jué)得很詭異,摸不著頭腦。
先來(lái)看個(gè)例子:
type CustomizedError struct { ErrorCode int Msg string } func (e *CustomizedError) Error() string { return fmt.Sprintf("err code: %d, msg: %s", e.ErrorCode, e.Msg) }
func main() { txn, err := startTx() if err != nil { log.Fatalf("err starting tx: %v", err) } if err = txn.doUpdate(); err != nil { log.Fatalf("err updating: %v", err) } if err = txn.commit(); err != nil { log.Fatalf("err committing: %v", err) } fmt.Println("success!") } type tx struct{} func startTx() (*tx, error) { return &tx{}, nil } func (*tx) doUpdate() *CustomizedError { return nil } func (*tx) commit() error { return nil }
這是一個(gè)簡(jiǎn)化過(guò)了的例子,在上述代碼中,我們創(chuàng)建了一個(gè)事務(wù),然后做了一些更新,在更新過(guò)程中如果發(fā)生了錯(cuò)誤,希望返回對(duì)應(yīng)的錯(cuò)誤碼和提示信息。
如果感興趣的話,可以在這個(gè)地址在線運(yùn)行這份代碼:
Go Playground - The Go Programming Language
看起來(lái)每個(gè)方法都會(huì)返回 nil
,應(yīng)該能順利走到最后一行,輸出 success
才對(duì),但實(shí)際上,輸出的卻是:
err updating: <nil>
尋找原因
為什么明明返回的是 nil
,卻被判定為 err ≠ nil
呢?難道這個(gè) nil
也有什么奇妙之處?
這就需要我們來(lái)更深入一點(diǎn)了解 error
本身了。在 Go 語(yǔ)言中, error
是一個(gè) interface
,內(nèi)部含有一個(gè) Error()
函數(shù),返回一個(gè)字符串,接口的描述如下:
// The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. type error interface { Error() string }
而對(duì)于一個(gè)變量來(lái)說(shuō),它有兩個(gè)要素,一個(gè)是 type T
,一個(gè)是 value V
,如下圖所示:
來(lái)看一個(gè)簡(jiǎn)單的例子:
var it interface{} fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // <nil> <invalid reflect.Value> it = 1 fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // int 1 it = "hello" fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // string hello var s *string it = s fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // *string <nil> ss := "hello" it = &ss fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // *string 0xc000096560
在給一個(gè) interface
變量賦值前,T
和 V
都是 nil
,但給它賦值后,不僅會(huì)改變它的值,還會(huì)改變它的類(lèi)型。
當(dāng)把一個(gè)值為 nil
的字符串指針賦值給它后,雖然它的值是 V=nil
,但它的類(lèi)型 T
卻變成了 *string
。
此時(shí)如果拿它來(lái)跟 nil
比較,結(jié)果就會(huì)是不相等,因?yàn)?strong>只有當(dāng)這個(gè) interface 變量的類(lèi)型和值都未被設(shè)置時(shí),它才真正等于 nil。
再來(lái)看看之前的例子中,err
變量的 T
和 V
是如何變化的:
func main() { txn, err := startTx() fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) if err != nil { log.Fatalf("err starting tx: %v", err) } if err = txn.doUpdate(); err != nil { fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) log.Fatalf("err updating: %v", err) } if err = txn.commit(); err != nil { log.Fatalf("err committing: %v", err) } fmt.Println("success!") }
輸出如下:
<nil> <invalid reflect.Value>
*err.CustomizedError <nil>
在一開(kāi)始,我們給 err
初始化賦值時(shí),startTx
函數(shù)返回的是一個(gè) error
接口類(lèi)型的 nil
。此時(shí)查看其類(lèi)型 T
和值 V
時(shí),都會(huì)是 nil
。
txn, err := startTx() fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // <nil> <invalid reflect.Value> func startTx() (*tx, error) { return &tx{}, nil }
而在調(diào)用 doUpdate
時(shí),會(huì)將一個(gè) *CustomizedError
類(lèi)型的 nil
值賦值給了它,它的類(lèi)型 T 便成了 *CustomizedError
,V 是 nil
。
err = txn.doUpdate() fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // *err.CustomizedError <nil>
所以在做 err ≠ nil
的比較時(shí),err
的類(lèi)型 T
已經(jīng)不是 nil
,前面已經(jīng)說(shuō)過(guò),只有當(dāng)一個(gè)接口變量的 T
和 V
同時(shí)為 nil
時(shí),這個(gè)變量才會(huì)被判定為 nil
,所以該不等式會(huì)判定為 true
。
要修復(fù)這個(gè)問(wèn)題,其實(shí)最簡(jiǎn)單的方法便是在調(diào)用 doUpdate
方法時(shí)給 err
進(jìn)行重新聲明:
if err := txn.doUpdate(); err != nil { log.Fatalf("err updating: %v", err) }
此時(shí),err
其實(shí)成了一個(gè)新的結(jié)構(gòu)體指針變量,而不再是一個(gè)interface
類(lèi)型變量,類(lèi)型為 *CustomizedError
,且值為 nil
,所以做 err ≠ nil
的比較時(shí)結(jié)果就是將是 false
。
問(wèn)題到這里似乎就告一段落了,但,再仔細(xì)想想,就會(huì)發(fā)現(xiàn)這其中似乎還是漏掉了一環(huán)。
如果給一個(gè) interface
類(lèi)型的變量賦值時(shí),會(huì)同時(shí)改變它的類(lèi)型 T
和值 V
,那跟 nil
比較時(shí)為什么不是跟它的新類(lèi)型對(duì)應(yīng)的 nil
比較呢?
事實(shí)上,interface
變量跟普通變量確實(shí)有一定區(qū)別,一個(gè)非空接口 interface
(即接口中存在函數(shù)方法)初始化的底層數(shù)據(jù)結(jié)構(gòu)是 iface
,一個(gè)空接口變量對(duì)應(yīng)的底層結(jié)構(gòu)體為 eface
。
type iface struct { tab *itab data unsafe.Pointer } type eface struct { _type *_type data unsafe.Pointer }
tab
中存放的是類(lèi)型、方法等信息。data
指針指向的 iface
綁定對(duì)象的原始數(shù)據(jù)的副本。
再來(lái)看一下 itab
的結(jié)構(gòu):
// layout of Itab known to compilers // allocated in non-garbage-collected memory // Needs to be in sync with // ../cmd/compile/internal/reflectdata/reflect.go:/^func.WriteTabs. type itab struct { inter *interfacetype _type *_type hash uint32 // copy of _type.hash. Used for type switches. _ [4]byte // 用于內(nèi)存對(duì)齊 fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. }
itab
中一共包含 5 個(gè)字段,inner
字段存的是初始化 interface
時(shí)的靜態(tài)類(lèi)型。_type
存的是 interface
對(duì)應(yīng)具體對(duì)象的類(lèi)型,當(dāng) interface
變量被賦值后,這個(gè)字段便會(huì)變成被賦值的對(duì)象的類(lèi)型。
itab
中的 _type
和 iface
中的 data
便分別對(duì)應(yīng) interface
變量的 T
和 V
,_type
是這個(gè)變量對(duì)應(yīng)的類(lèi)型,data
是這個(gè)變量的值。在之前的賦值測(cè)試中,通過(guò) reflect.TypeOf
與 reflect.ValueOf
方法獲取到的信息也分別來(lái)自這兩個(gè)字段。
這里的 hash
字段和 _type
中存的 hash
字段是完全一致的,這么做的目的是為了類(lèi)型斷言。
fun
是一個(gè)函數(shù)指針,它指向的是具體類(lèi)型的函數(shù)方法,在這個(gè)指針對(duì)應(yīng)內(nèi)存地址的后面依次存儲(chǔ)了多個(gè)方法,利用指針偏移便可以找到它們。
再來(lái)看看 interfacetype
的結(jié)構(gòu):
type interfacetype struct { typ _type pkgpath name mhdr []imethod }
這其中也有一個(gè) _type
字段,來(lái)表示 interface
變量的初始類(lèi)型。
看到這里,之前的疑問(wèn)便開(kāi)始清晰起來(lái),一個(gè) interface
變量實(shí)際上有兩個(gè)類(lèi)型,一個(gè)是初始化時(shí)賦值時(shí)對(duì)應(yīng)的 interface
類(lèi)型,一個(gè)是賦值具體對(duì)象時(shí),對(duì)象的實(shí)際類(lèi)型。
了解了這些之后,我們?cè)賮?lái)看一下之前的例子:
txn, err := startTx()
這里先對(duì) err
進(jìn)行初始化賦值,此時(shí),它的 itab.inter.typ
對(duì)應(yīng)的類(lèi)型信息就是 error
itab._type
仍為 nil
。
err = txn.doUpdate()
當(dāng)對(duì) err
進(jìn)行重新賦值時(shí),err
的 itab._type
字段會(huì)被賦值成 *CustomizedError
,所以此時(shí),err
變量實(shí)際上是一個(gè) itab.inter.typ
為 error
,但實(shí)際類(lèi)型為 *CustomizedError
,值為 nil
的接口變量。
把一個(gè)具體類(lèi)型變量與 nil
比較時(shí),只需要判斷其 value
是否為 nil
即可,而把一個(gè)接口類(lèi)型的變量與 nil
進(jìn)行比較時(shí),還需要判斷其類(lèi)型 itab._type
是否為nil
。
如果想實(shí)際看看被賦值后 err
對(duì)應(yīng)的 iface
結(jié)構(gòu),可以把 iface
相關(guān)的結(jié)構(gòu)體都復(fù)制到同一個(gè)包下,然后通過(guò) unsafe.Pointer
進(jìn)行類(lèi)型強(qiáng)轉(zhuǎn),就可以通過(guò)打斷點(diǎn)的方式來(lái)查看了。
func TestErr(t *testing.T) { txn, err := startTx() fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) if err != nil { log.Fatalf("err starting tx: %v", err) } p := (*iface)(unsafe.Pointer(&err)) fmt.Println(p.data) if err = txn.doUpdate(); err != nil { fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) p := (*iface)(unsafe.Pointer(&err)) fmt.Println(p.data) log.Fatalf("err updating: %v", err) } if err = txn.commit(); err != nil { log.Fatalf("err committing: %v", err) } fmt.Println("success!") }
補(bǔ)充說(shuō)明一下,這里的inter.typ.kind
表示的是變量的基本類(lèi)型,其值對(duì)應(yīng) runtime
包下的枚舉。
const ( kindBool = 1 + iota kindInt kindInt8 kindInt16 kindInt32 kindInt64 kindUint kindUint8 kindUint16 kindUint32 kindUint64 kindUintptr kindFloat32 kindFloat64 kindComplex64 kindComplex128 kindArray kindChan kindFunc kindInterface kindMap kindPtr kindSlice kindString kindStruct kindUnsafePointer kindDirectIface = 1 << 5 kindGCProg = 1 << 6 kindMask = (1 << 5) - 1 )
比如上圖中所示的 kind = 20
對(duì)應(yīng)的類(lèi)型就是 kindInterface
。
總結(jié)
- 接口類(lèi)型變量跟普通變量是有差異的,非空接口類(lèi)型變量對(duì)應(yīng)的底層結(jié)構(gòu)是
iface
,空接口類(lèi)型類(lèi)型變量對(duì)應(yīng)的底層結(jié)構(gòu)是eface
。 iface
中有兩個(gè)跟類(lèi)型相關(guān)的字段,一個(gè)表示的是接口的類(lèi)型i
nter,一個(gè)表示的是變量實(shí)際類(lèi)型_type
。- 只有當(dāng)接口變量的
itab._type
與 data 都為nil
時(shí),也就是實(shí)際類(lèi)型和值都未被賦值前,才真正等于nil
。
以上就是Go疑難雜癥講解之為什么nil不等于nil的詳細(xì)內(nèi)容,更多關(guān)于Go nil不等于nil的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言同步與異步執(zhí)行多個(gè)任務(wù)封裝詳解(Runner和RunnerAsync)
這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言同步與異步執(zhí)行多個(gè)任務(wù)封裝(Runner和RunnerAsync)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01Go語(yǔ)言基礎(chǔ)知識(shí)總結(jié)(語(yǔ)法、變量、數(shù)值類(lèi)型、表達(dá)式、控制結(jié)構(gòu)等)
這篇文章主要介紹了Go語(yǔ)言基礎(chǔ)知識(shí)總結(jié)(語(yǔ)法、變量、數(shù)值類(lèi)型、表達(dá)式、控制結(jié)構(gòu)等),本文匯總了Go語(yǔ)言的入門(mén)知識(shí),需要的朋友可以參考下2014-10-10簡(jiǎn)單聊聊Golang中defer預(yù)計(jì)算參數(shù)
在golang當(dāng)中defer代碼塊會(huì)在函數(shù)調(diào)用鏈表中增加一個(gè)函數(shù)調(diào)用,下面這篇文章主要給大家介紹了關(guān)于Golang中defer預(yù)計(jì)算參數(shù)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03Go實(shí)現(xiàn)整合Logrus實(shí)現(xiàn)日志打印
這篇文章主要介紹了Go實(shí)現(xiàn)整合Logrus實(shí)現(xiàn)日志打印,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-07-07golang通過(guò)反射設(shè)置結(jié)構(gòu)體變量的值
這篇文章主要介紹了golang通過(guò)反射設(shè)置結(jié)構(gòu)體變量的值操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04CentOS7使用yum安裝Golang的超詳細(xì)步驟
CentOS默認(rèn)并沒(méi)有安裝golang運(yùn)行環(huán)境,下面這篇文章主要給大家介紹了關(guān)于CentOS7使用yum安裝Golang的超詳細(xì)步驟,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02