[Go疑難雜症]為什麼nil不等於nil

2022-10-27 15:01:36

現象

在日常開發中,可能一不小心就會掉進 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 變數賦值前,TV 都是 nil,但給它賦值後,不僅會改變它的值,還會改變它的型別。

當把一個值為 nil 的字串指標賦值給它後,雖然它的值是 V=nil,但它的型別 T 卻變成了 *string

此時如果拿它來跟 nil 比較,結果就會是不相等,因為只有當這個 interface 變數的型別和值都未被設定時,它才真正等於 nil

再來看看之前的例子中,err 變數的 TV 是如何變化的:

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,前面已經說過,只有當一個介面變數的 TV 同時為 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 中的 _typeiface 中的 data 便分別對應 interface 變數的 TV_type 是這個變數對應的型別,data 是這個變數的值。在之前的賦值測試中,通過 reflect.TypeOfreflect.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 進行重新賦值時,erritab._type 欄位會被賦值成 *CustomizedError ,所以此時,err 變數實際上是一個 itab.inter.typerror ,但實際型別為 *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

總結

  1. 介面型別變數跟普通變數是有差異的,非空介面型別變數對應的底層結構是 iface ,空介面型別型別變數對應的底層結構是 eface
  2. iface 中有兩個跟型別相關的欄位,一個表示的是介面的型別 inter,一個表示的是變數實際型別 _type
  3. 只有當介面變數的 itab._type 與 data 都為 nil 時,也就是實際型別和值都未被賦值前,才真正等於 nil

到此,一個有趣的探索之旅就結束了,但長路漫漫,前方還有無數的問題等待我們去探索和發現,這便是學習的樂趣,希望能與君共勉。