在日常開發中,可能一不小心就會掉進 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
}
這是一個簡化過了的例子,在上述程式碼中,我們建立了一個事務,然後做了一些更新,在更新過程中如果發生了錯誤,希望返回對應的錯誤碼和提示資訊。
如果感興趣的話,可以在這個地址線上執行這份程式碼:
Go Playground - The Go Programming Language
看起來每個方法都會返回 nil
,應該能順利走到最後一行,輸出 success
才對,但實際上,輸出的卻是:
err updating: <nil>
為什麼明明返回的是 nil
,卻被判定為 err ≠ nil
呢?難道這個 nil
也有什麼奇妙之處?
這就需要我們來更深入一點了解 error
本身了。在 Go 語言中, error
是一個 interface
,內部含有一個 Error()
函數,返回一個字串,介面的描述如下:
// 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
比較,結果就會是不相等,因為只有當這個 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
函數返回的是一個 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
}
而在呼叫 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
已經不是 nil
,前面已經說過,只有當一個介面變數的 T
和 V
同時為 nil
時,這個變數才會被判定為 nil
,所以該不等式會判定為 true
。
要修復這個問題,其實最簡單的方法便是在呼叫 doUpdate
方法時給 err
進行重新宣告:
if err := txn.doUpdate(); err != nil {
log.Fatalf("err updating: %v", err)
}
此時,err
其實成了一個新的結構體指標變數,而不再是一個interface
型別變數,型別為 *CustomizedError
,且值為 nil
,所以做 err ≠ nil
的比較時結果就是將是 false
。
問題到這裡似乎就告一段落了,但,再仔細想想,就會發現這其中似乎還是漏掉了一環。
如果給一個 interface
型別的變數賦值時,會同時改變它的型別 T
和值 V
,那跟 nil
比較時為什麼不是跟它的新型別對應的 nil
比較呢?
事實上,interface
變數跟普通變數確實有一定區別,一個非空介面 interface
(即介面中存在函數方法)初始化的底層資料結構是 iface
,一個空介面變數對應的底層結構體為 eface
。
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
tab
中存放的是型別、方法等資訊。data
指標指向的 iface
繫結物件的原始資料的副本。
再來看一下 itab
的結構:
// 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 // 用於記憶體對齊
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
itab
中一共包含 5 個欄位,inner
欄位存的是初始化 interface
時的靜態型別。_type
存的是 interface
對應具體物件的型別,當 interface
變數被賦值後,這個欄位便會變成被賦值的物件的型別。
itab
中的 _type
和 iface
中的 data
便分別對應 interface
變數的 T
和 V
,_type
是這個變數對應的型別,data
是這個變數的值。在之前的賦值測試中,通過 reflect.TypeOf
與 reflect.ValueOf
方法獲取到的資訊也分別來自這兩個欄位。
這裡的 hash
欄位和 _type
中存的 hash
欄位是完全一致的,這麼做的目的是為了型別斷言。
fun
是一個函數指標,它指向的是具體型別的函數方法,在這個指標對應記憶體地址的後面依次儲存了多個方法,利用指標偏移便可以找到它們。
再來看看 interfacetype
的結構:
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
結構,可以把 iface
相關的結構體都複製到同一個包下,然後通過 unsafe.Pointer
進行型別強轉,就可以通過打斷點的方式來檢視了。
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
。
iface
,空介面型別型別變數對應的底層結構是 eface
。iface
中有兩個跟型別相關的欄位,一個表示的是介面的型別 i
nter,一個表示的是變數實際型別 _type
。itab._type
與 data 都為 nil
時,也就是實際型別和值都未被賦值前,才真正等於 nil
。到此,一個有趣的探索之旅就結束了,但長路漫漫,前方還有無數的問題等待我們去探索和發現,這便是學習的樂趣,希望能與君共勉。