
寫過 Go 代碼的人,肯定對下面的代碼不陌生:
if err != nil {
//...
}
Go 項目中這行代碼會大量存在,這里可能隱藏著陷阱。
1. Go 中的 nil
Go 中 nil 代表零值,表示什么都沒有,其他語言中也有類似的設計,比如 Java 中的 null。
也不是所有類型的零值都是 nil,Go 中不同類型的零值如下:

Go 中的數(shù)據(jù)類型可以分為基本類型和復合類型。
基本類型的零值都不同,有的是數(shù)字,有的是空字符串,對于復合類型, 零值都是 nil。
零值在有些情況下會讓程序崩潰,比如指針的零值,因為指針是指向一塊內(nèi)存地址,如果指針為 nil,那么就表示不指向任何地址,那么使用這個指針內(nèi)存操作就會出現(xiàn) panic。
還有一點需要注意,nil 并不是關鍵字,nil 在 Go 中是這樣定義的:
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
nil 的類型只能是指針,channel、函數(shù)、接口和 slice。
也就是說這樣寫代碼是完全合法的:
var nil = make(map[string]string)
但永遠不要這么做,否則程序就完蛋了。
2. nil 中的陷阱
從 nil 的定義可以知道,nil 只能和指針等幾種類型一起使用。
指針的結(jié)構(gòu)相對簡單,就是指向一個內(nèi)存地址,我們可以很安全的使用 nil 來判斷指針是不是為空,有沒有指向內(nèi)存地址。
channel、map、function 和 slice 的本質(zhì)都是在使用指針,所以也可以使用 nil 來判斷這些類型是否初始化、是否能使用。slice 特殊一點,還有 len 和 cap 兩個屬性,但不影響 nil 的判斷:

而 interface 中的 nil,有隱藏的陷阱。
inteface 的接口與上面的類型都不一樣,interface 由兩部分組成,一部分是接口的類型,另一部分是接口的值:

先看下面代碼的輸出:
var in io.Writer
fmt.Printf("%T\n", in) // nil
var inP *io.Writer
fmt.Printf("%T\n", inP) // *io.Writer
%T 表示輸出這個值的類型。
聲明上面的變量之后,此時 in 和 inP 的結(jié)構(gòu)是下面這樣的:

如果再接著寫下面的代碼:
var in io.Writer
if in != nil {
in.Write([]byte("logs"))
}
var inP *io.Writer
if inP != nil {
inP.Write([]byte("logs")) // 這里會發(fā)生 panic
}
inP 的 type 不是 nil,那么 inP 就不等于 nil。在使用接口時要注意,只有接口的類型和值都是 nil 時,這個接口才等于 nil,否則不相等。
錯誤處理是 Go 程序的重要組成部分,但是這里也容易出現(xiàn)陷阱,看如下的代碼:
func main() {
err := Do() // nil
fmt.Printf("result: %+v\n", (err == nil)) // true
}
func Do() *DoError { // nil
return nil
}
type DoError struct {
}
func (d *DoError) Error() string {
return "doError"
}
上面的代碼返回的不是接口,而是 DoError 類型的指針,所以判斷是否為 nil 沒問題。
如果換種形式,看下面的代碼,返回的是 error 的接口類型,但是這個接口的類型是 *DoError,值是 nil,這樣一來,就和預期的結(jié)果不符合。
func main() {
err := Do() // error(*DoError, nil)
fmt.Printf("result: %+v\n", (err == nil)) // false
}
func Do() error { // error(*DoError, nil)
var err *DoError
return err // nil
}
判斷 err 是不是 nil 是非常高頻的使用場景,在處理這些錯誤時,要非常小心。
3. nil 的其他作用
nil 除了作為零值之外,還有其他的用途。
nil 作為方法的接受者是完全合法的,這里 p 是一個 *Person 類型的 nil:
func main() {
var p *Person // nil
p.SayHi()
}
type Person struct {
}
func (p *Person) SayHi() {
fmt.Println("hi")
}
nil 還可以作為默認值,下面的代碼應該也看的不少了,通常情況下,第二個參數(shù)我們都會直接傳入 nil,這里 nil 的含義是使用默認的配置,我們在自己的代碼中也可以這樣使用。
http.HandleFunc("localhost:8080", nil)
4. 小結(jié)
nil 是指針,channel、函數(shù)、接口和 slice 等類型的零值,其中 interface 的零值有點特殊,只有在類型和值都是 nil 的時候,這個接口才是 nil。
nil 除了作為零值使用之外,還有很多其他的用途,比如作為方法的接受者,表示默認值。
文 / Rayjun