nil
是Go中的一個使用頻率很高的預(yù)聲明標(biāo)識符。 很多種類的類型的零值都用nil
表示。 很多有其它語言編程經(jīng)驗的程序員在初學(xué)Go語言的時候常將nil
看成是其它語言中的null
或者NULL
。 這種看法只是部分上正確的,但是Go中的nil
和其它語言中的null
或者NULL
也是有很大的區(qū)別的。
本文的剩余部分將列出和nil
相關(guān)的各種事實。
我們可以直接使用它。
在Go中,預(yù)聲明的nil
可以表示下列種類(kind)的類型的零值:
Go中其它的預(yù)聲明標(biāo)識符都有各自的默認(rèn)類型,比如
true
和false
的默認(rèn)類型均為內(nèi)置類型bool
。
iota
的默認(rèn)類型為內(nèi)置類型int
。
但是,預(yù)聲明標(biāo)識符nil
沒有一個默認(rèn)類型,盡管它有很多潛在的可能類型。 事實上,預(yù)聲明標(biāo)識符nil
是Go中唯一一個沒有默認(rèn)類型的類型不確定值。 我們必須在代碼中提供足夠的信息以便讓編譯器能夠推斷出一個類型不確定的nil
值的期望類型。
一個例子:
package main
func main() {
// 代碼中必須提供充足的信息來讓編譯器推斷出某個nil的類型。
_ = (*struct{})(nil)
_ = []int(nil)
_ = map[int]bool(nil)
_ = chan string(nil)
_ = (func())(nil)
_ = interface{}(nil)
// 下面這一組和上面這一組等價。
var _ *struct{} = nil
var _ []int = nil
var _ map[int]bool = nil
var _ chan string = nil
var _ func() = nil
var _ interface{} = nil
// 下面這行編譯不通過。
var _ = nil
}
預(yù)聲明標(biāo)識符?nil
?可以被更內(nèi)層的同名標(biāo)識符所遮擋。
一個例子:
package main
import "fmt"
func main() {
nil := 123
fmt.Println(nil) // 123
// 下面這行編譯報錯,因為此行中的nil是一個int值。
var _ map[string]int = nil
}
(順便說一下,其它語言中的null
和NULL
也不是關(guān)鍵字。)
一個類型的所有值的內(nèi)存布局都是一樣的,此類型nil值也不例外(假設(shè)此類型的零值使用nil
表示)。 所以同一個類型的nil值和非nil值的尺寸是一樣的。但是不同類型的nil值的尺寸可能是不一樣的。
一個例子:
package main
import (
"fmt"
"unsafe"
)
func main() {
var p *struct{} = nil
fmt.Println( unsafe.Sizeof( p ) ) // 8
var s []int = nil
fmt.Println( unsafe.Sizeof( s ) ) // 24
var m map[int]bool = nil
fmt.Println( unsafe.Sizeof( m ) ) // 8
var c chan string = nil
fmt.Println( unsafe.Sizeof( c ) ) // 8
var f func() = nil
fmt.Println( unsafe.Sizeof( f ) ) // 8
var i interface{} = nil
fmt.Println( unsafe.Sizeof( i ) ) // 16
}
上例打印出來的尺寸值取決于系統(tǒng)架構(gòu)和具體編譯器實現(xiàn)。 上例中的輸出是使用官方標(biāo)準(zhǔn)編譯器編譯并在64位的系統(tǒng)架構(gòu)上運行的結(jié)果。 在32位的系統(tǒng)架構(gòu)上,這些輸出值將減半。
對于官方標(biāo)準(zhǔn)編譯器,如果兩個類型屬于同一種(kind)類型,并且它們的零值用nil
表示,則這兩個類型的尺寸肯定相等。
比如,下例中的兩行中的比較均編譯不通過。
// error: 類型不匹配
var _ = (*int)(nil) == (*bool)(nil)
// error: 類型不匹配
var _ = (chan int)(nil) == (chan bool)(nil)
請閱讀Go中的值比較規(guī)則來了解哪些值可以相互比較。 類型確定的nil值也要遵循這些規(guī)則。
下面這些比較是合法的:
type IntPtr *int
// 類型IntPtr的底層類型為*int。
var _ = IntPtr(nil) == (*int)(nil)
// 任何類型都實現(xiàn)了interface{}類型。
var _ = (interface{})(nil) == (*int)(nil)
// 一個雙向通道可以隱式轉(zhuǎn)換為和它的
// 元素類型一樣的單項通道類型。
var _ = (chan int)(nil) == (chan<- int)(nil)
var _ = (chan int)(nil) == (<-chan int)(nil)
在Go中,映射類型、切片類型和函數(shù)類型是不支持比較類型。 比較同一個不支持比較的類型的兩個值(包括nil值)是非法的。 比如,下面的幾個比較都編譯不通過。
var _ = ([]int)(nil) == ([]int)(nil)
var _ = (map[string]int)(nil) == (map[string]int)(nil)
var _ = (func())(nil) == (func())(nil)
但是,映射類型、切片類型和函數(shù)類型的任何值都可以和類型不確定的裸nil
標(biāo)識符比較。
// 這幾行編譯都沒問題。
var _ = ([]int)(nil) == nil
var _ = (map[string]int)(nil) == nil
var _ = (func())(nil) == nil
如果可被比較的兩個nil值中的一個的類型為接口類型,而另一個不是,則比較結(jié)果總是false
。 原因是,在進(jìn)行此比較之前,此非接口nil值將被轉(zhuǎn)換為另一個nil值的接口類型,從而將此比較轉(zhuǎn)化為兩個接口值的比較。 從接口一文中,我們得知每個接口值可以看作是一個包裹非接口值的盒子。 一個非接口值被轉(zhuǎn)換為一個接口類型的過程可以看作是用一個接口值將此非接口值包裹起來的過程。 一個nil接口值中什么也沒包裹,但是一個包裹了nil非接口值的接口值并非什么都沒包裹。 一個什么都沒包裹的接口值和一個包裹了一個非接口值(即使它是nil)的接口值是不相等的。
一個例子:
fmt.Println( (interface{})(nil) == (*int)(nil) ) // false
訪問一個nil映射將得到此映射的類型的元素類型的零值。
比如:
fmt.Println( (map[string]int)(nil)["key"] ) // 0
fmt.Println( (map[int]bool)(nil)[123] ) // false
fmt.Println( (map[int]*int64)(nil)[123] ) //
遍歷nil映射和nil切片的循環(huán)步數(shù)均為零。
遍歷一個nil數(shù)組指針的循環(huán)步數(shù)為對應(yīng)數(shù)組類型的長度。 (但是,如果此數(shù)組類型的長度不為零并且第二個循環(huán)變量未被舍棄或者忽略,則對應(yīng)for-range
循環(huán)將導(dǎo)致一個恐慌。)
遍歷一個nil通道將使當(dāng)前協(xié)程永久阻塞。
比如,下面的代碼將輸出0
、1
、2
、3
和4
后進(jìn)入阻塞狀態(tài)。 Hello
、world
和Bye
不會被輸出。
for range []int(nil) {
fmt.Println("Hello")
}
for range map[string]string(nil) {
fmt.Println("world")
}
for i := range (*[5]int)(nil) {
fmt.Println(i)
}
for range chan bool(nil) { // 阻塞在此
fmt.Println("Bye")
}
一個例子:
package main
type Slice []bool
func (s Slice) Length() int {
return len(s)
}
func (s Slice) Modify(i int, x bool) {
s[i] = x // panic if s is nil
}
func (p *Slice) DoNothing() {
}
func (p *Slice) Append(x bool) {
*p = append(*p, x) // 如果p為空指針,則產(chǎn)生一個恐慌。
}
func main() {
// 下面這幾行中的選擇器不會造成恐慌。
_ = ((Slice)(nil)).Length
_ = ((Slice)(nil)).Modify
_ = ((*Slice)(nil)).DoNothing
_ = ((*Slice)(nil)).Append
// 這兩行也不會造成恐慌。
_ = ((Slice)(nil)).Length()
((*Slice)(nil)).DoNothing()
// 下面這兩行都會造成恐慌。但是恐慌不是因為nil
// 屬主實參造成的。恐慌都來自于這兩個方法內(nèi)部的
// 對空指針的解引用操作。
/*
((Slice)(nil)).Modify(0, true)
((*Slice)(nil)).Append(true)
*/
}
事實上,上面的Append
方法實現(xiàn)不完美。我們應(yīng)該像下面這樣實現(xiàn)之:
func (p *Slice) Append(x bool) {
if p == nil {
*p = []bool{x}
return
}
*p = append(*p, x)
}
一個例子:
package main
import "fmt"
func main() {
fmt.Println(*new(*int) == nil) // true
fmt.Println(*new([]int) == nil) // true
fmt.Println(*new(map[int]bool) == nil) // true
fmt.Println(*new(chan string) == nil) // true
fmt.Println(*new(func()) == nil) // true
fmt.Println(*new(interface{}) == nil) // true
}
在Go中,為了簡單和方便,nil
被設(shè)計成一個可以表示成很多種類型的零值的預(yù)聲明標(biāo)識符。 換句話說,它可以表示很多內(nèi)存布局不同的值,而不僅僅是一個值。
更多建議: