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 變量的類型和值都未被設(shè)置時,它才真正等于 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é)構(gòu)體指針變量,而不再是一個interface 類型變量,類型為 *CustomizedError ,且值為 nil,所以做 err ≠ nil 的比較時結(jié)果就是將是 false。
問題到這里似乎就告一段落了,但,再仔細想想,就會發(fā)現(xiàn)這其中似乎還是漏掉了一環(huán)。
如果給一個 interface 類型的變量賦值時,會同時改變它的類型 T 和值 V,那跟 nil 比較時為什么不是跟它的新類型對應的 nil 比較呢?
事實上,interface 變量跟普通變量確實有一定區(qū)別,一個非空接口 interface (即接口中存在函數(shù)方法)初始化的底層數(shù)據(jù)結(jié)構(gòu)是 iface,一個空接口變量對應的底層結(jié)構(gòu)體為 eface。
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}tab 中存放的是類型、方法等信息。data 指針指向的 iface 綁定對象的原始數(shù)據(jù)的副本。
再來看一下 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)存對齊
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é)構(gòu):
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é)構(gòu),可以把 iface 相關(guān)的結(jié)構(gòu)體都復制到同一個包下,然后通過 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é)構(gòu)是
iface,空接口類型類型變量對應的底層結(jié)構(gòu)是eface。 iface中有兩個跟類型相關(guān)的字段,一個表示的是接口的類型inter,一個表示的是變量實際類型_type。- 只有當接口變量的
itab._type與 data 都為nil時,也就是實際類型和值都未被賦值前,才真正等于nil。
以上就是Go疑難雜癥講解之為什么nil不等于nil的詳細內(nèi)容,更多關(guān)于Go nil不等于nil的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go Sentinel 動態(tài)數(shù)據(jù)源配置指南(示例詳解)
本文介紹了如何使用Go語言配置Sentinel的動態(tài)數(shù)據(jù)源,并通過本地文件和Nacos兩種方式實現(xiàn)動態(tài)配置,通過這種方式,可以靈活地管理和更新限流規(guī)則,提升系統(tǒng)的穩(wěn)定性和響應速度,感興趣的朋友跟隨小編一起看看吧2025-01-01
詳解如何使用Golang實現(xiàn)自定義規(guī)則引擎
規(guī)則引擎的功能可以簡化為當滿足一些條件時觸發(fā)一些操作,通常使用 DSL 自定義語法來表述,本文給大家介紹了如何使用Golang實現(xiàn)自定義規(guī)則引擎,文中有相關(guān)的代碼示例供大家參考,需要的朋友可以參考下2024-05-05

