Go疑難雜癥講解之為什么nil不等于nil
現(xiàn)象
在日常開發(fā)中,可能一不小心就會掉進 Go
語言的某些陷阱里,而本文要介紹的 nil ≠ nil
問題,便是其中一個,初看起來會讓人覺得很詭異,摸不著頭腦。
先來看個例子:
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 }
這是一個簡化過了的例子,在上述代碼中,我們創(chuàng)建了一個事務,然后做了一些更新,在更新過程中如果發(fā)生了錯誤,希望返回對應的錯誤碼和提示信息。
如果感興趣的話,可以在這個地址在線運行這份代碼:
Go Playground - The Go Programming Language
看起來每個方法都會返回 nil
,應該能順利走到最后一行,輸出 success
才對,但實際上,輸出的卻是:
err updating: <nil>
尋找原因
為什么明明返回的是 nil
,卻被判定為 err ≠ nil
呢?難道這個 nil
也有什么奇妙之處?
這就需要我們來更深入一點了解 error
本身了。在 Go 語言中, error
是一個 interface
,內(nèi)部含有一個 Error()
函數(shù),返回一個字符串,接口的描述如下:
// 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 }
而對于一個變量來說,它有兩個要素,一個是 type T
,一個是 value V
,如下圖所示:
來看一個簡單的例子:
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
在給一個 interface
變量賦值前,T
和 V
都是 nil
,但給它賦值后,不僅會改變它的值,還會改變它的類型。
當把一個值為 nil
的字符串指針賦值給它后,雖然它的值是 V=nil
,但它的類型 T
卻變成了 *string
。
此時如果拿它來跟 nil
比較,結(jié)果就會是不相等,因為只有當這個 interface 變量的類型和值都未被設置時,它才真正等于 nil。
再來看看之前的例子中,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>
在一開始,我們給 err
初始化賦值時,startTx
函數(shù)返回的是一個 error
接口類型的 nil
。此時查看其類型 T
和值 V
時,都會是 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
時,會將一個 *CustomizedError
類型的 nil
值賦值給了它,它的類型 T 便成了 *CustomizedError
,V 是 nil
。
err = txn.doUpdate() fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // *err.CustomizedError <nil>
所以在做 err ≠ nil
的比較時,err
的類型 T
已經(jīng)不是 nil
,前面已經(jīng)說過,只有當一個接口變量的 T
和 V
同時為 nil
時,這個變量才會被判定為 nil
,所以該不等式會判定為 true
。
要修復這個問題,其實最簡單的方法便是在調(diào)用 doUpdate
方法時給 err
進行重新聲明:
if err := txn.doUpdate(); err != nil { log.Fatalf("err updating: %v", err) }
此時,err
其實成了一個新的結(jié)構體指針變量,而不再是一個interface
類型變量,類型為 *CustomizedError
,且值為 nil
,所以做 err ≠ nil
的比較時結(jié)果就是將是 false
。
問題到這里似乎就告一段落了,但,再仔細想想,就會發(fā)現(xiàn)這其中似乎還是漏掉了一環(huán)。
如果給一個 interface
類型的變量賦值時,會同時改變它的類型 T
和值 V
,那跟 nil
比較時為什么不是跟它的新類型對應的 nil
比較呢?
事實上,interface
變量跟普通變量確實有一定區(qū)別,一個非空接口 interface
(即接口中存在函數(shù)方法)初始化的底層數(shù)據(jù)結(jié)構是 iface
,一個空接口變量對應的底層結(jié)構體為 eface
。
type iface struct { tab *itab data unsafe.Pointer } type eface struct { _type *_type data unsafe.Pointer }
tab
中存放的是類型、方法等信息。data
指針指向的 iface
綁定對象的原始數(shù)據(jù)的副本。
再來看一下 itab
的結(jié)構:
// 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)存對齊 fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. }
itab
中一共包含 5 個字段,inner
字段存的是初始化 interface
時的靜態(tài)類型。_type
存的是 interface
對應具體對象的類型,當 interface
變量被賦值后,這個字段便會變成被賦值的對象的類型。
itab
中的 _type
和 iface
中的 data
便分別對應 interface
變量的 T
和 V
,_type
是這個變量對應的類型,data
是這個變量的值。在之前的賦值測試中,通過 reflect.TypeOf
與 reflect.ValueOf
方法獲取到的信息也分別來自這兩個字段。
這里的 hash
字段和 _type
中存的 hash
字段是完全一致的,這么做的目的是為了類型斷言。
fun
是一個函數(shù)指針,它指向的是具體類型的函數(shù)方法,在這個指針對應內(nèi)存地址的后面依次存儲了多個方法,利用指針偏移便可以找到它們。
再來看看 interfacetype
的結(jié)構:
type interfacetype struct { typ _type pkgpath name mhdr []imethod }
這其中也有一個 _type
字段,來表示 interface
變量的初始類型。
看到這里,之前的疑問便開始清晰起來,一個 interface
變量實際上有兩個類型,一個是初始化時賦值時對應的 interface
類型,一個是賦值具體對象時,對象的實際類型。
了解了這些之后,我們再來看一下之前的例子:
txn, err := startTx()
這里先對 err
進行初始化賦值,此時,它的 itab.inter.typ
對應的類型信息就是 error
itab._type
仍為 nil
。
err = txn.doUpdate()
當對 err
進行重新賦值時,err
的 itab._type
字段會被賦值成 *CustomizedError
,所以此時,err
變量實際上是一個 itab.inter.typ
為 error
,但實際類型為 *CustomizedError
,值為 nil
的接口變量。
把一個具體類型變量與 nil
比較時,只需要判斷其 value
是否為 nil
即可,而把一個接口類型的變量與 nil
進行比較時,還需要判斷其類型 itab._type
是否為nil
。
如果想實際看看被賦值后 err
對應的 iface
結(jié)構,可以把 iface
相關的結(jié)構體都復制到同一個包下,然后通過 unsafe.Pointer
進行類型強轉(zhuǎn),就可以通過打斷點的方式來查看了。
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!") }
補充說明一下,這里的inter.typ.kind
表示的是變量的基本類型,其值對應 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
對應的類型就是 kindInterface
。
總結(jié)
- 接口類型變量跟普通變量是有差異的,非空接口類型變量對應的底層結(jié)構是
iface
,空接口類型類型變量對應的底層結(jié)構是eface
。 iface
中有兩個跟類型相關的字段,一個表示的是接口的類型i
nter,一個表示的是變量實際類型_type
。- 只有當接口變量的
itab._type
與 data 都為nil
時,也就是實際類型和值都未被賦值前,才真正等于nil
。
以上就是Go疑難雜癥講解之為什么nil不等于nil的詳細內(nèi)容,更多關于Go nil不等于nil的資料請關注腳本之家其它相關文章!
相關文章
Go語言同步與異步執(zhí)行多個任務封裝詳解(Runner和RunnerAsync)
這篇文章主要給大家介紹了關于Go語言同步與異步執(zhí)行多個任務封裝(Runner和RunnerAsync)的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2018-01-01Go語言基礎知識總結(jié)(語法、變量、數(shù)值類型、表達式、控制結(jié)構等)
這篇文章主要介紹了Go語言基礎知識總結(jié)(語法、變量、數(shù)值類型、表達式、控制結(jié)構等),本文匯總了Go語言的入門知識,需要的朋友可以參考下2014-10-10Go實現(xiàn)整合Logrus實現(xiàn)日志打印
這篇文章主要介紹了Go實現(xiàn)整合Logrus實現(xiàn)日志打印,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-07-07