Go語言 關(guān)于Go中的nil

2023-02-16 17:40 更新

nil是Go中的一個使用頻率很高的預(yù)聲明標(biāo)識符。 很多種類的類型的零值都用nil表示。 很多有其它語言編程經(jīng)驗的程序員在初學(xué)Go語言的時候常將nil看成是其它語言中的null或者NULL。 這種看法只是部分上正確的,但是Go中的nil和其它語言中的null或者NULL也是有很大的區(qū)別的。

本文的剩余部分將列出和nil相關(guān)的各種事實。

nil是一個預(yù)聲明的標(biāo)識符

我們可以直接使用它。

預(yù)聲明的nil標(biāo)識符可以表示很多種類型的零值

在Go中,預(yù)聲明的nil可以表示下列種類(kind)的類型的零值:

  • 指針類型(包括類型安全和非類型安全指針)
  • 映射類型
  • 切片類型
  • 函數(shù)類型
  • 通道類型
  • 接口類型

預(yù)聲明標(biāo)識符nil沒有默認(rèn)類型

Go中其它的預(yù)聲明標(biāo)識符都有各自的默認(rèn)類型,比如

  • 預(yù)聲明標(biāo)識符truefalse的默認(rèn)類型均為內(nèi)置類型bool。
  • 預(yù)聲明標(biāo)識符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
}

nil不是一個關(guān)鍵字

預(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
}

(順便說一下,其它語言中的nullNULL也不是關(guān)鍵字。)

不同種類的類型的nil值的尺寸很可能不相同

一個類型的所有值的內(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表示,則這兩個類型的尺寸肯定相等。

兩個不同類型的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)

同一個類型的兩個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值可能并不相等

如果可被比較的兩個nil值中的一個的類型為接口類型,而另一個不是,則比較結(jié)果總是false。 原因是,在進(jìn)行此比較之前,此非接口nil值將被轉(zhuǎn)換為另一個nil值的接口類型,從而將此比較轉(zhuǎn)化為兩個接口值的比較。 從接口一文中,我們得知每個接口值可以看作是一個包裹非接口值的盒子。 一個非接口值被轉(zhuǎn)換為一個接口類型的過程可以看作是用一個接口值將此非接口值包裹起來的過程。 一個nil接口值中什么也沒包裹,但是一個包裹了nil非接口值的接口值并非什么都沒包裹。 一個什么都沒包裹的接口值和一個包裹了一個非接口值(即使它是nil)的接口值是不相等的。

一個例子:

fmt.Println( (interface{})(nil) == (*int)(nil) ) // false

訪問nil映射值的條目不會產(chǎn)生恐慌

訪問一個nil映射將得到此映射的類型的元素類型的零值。

比如:

fmt.Println( (map[string]int)(nil)["key"] ) // 0
fmt.Println( (map[int]bool)(nil)[123] )     // false
fmt.Println( (map[int]*int64)(nil)[123] )   // 

range關(guān)鍵字后可以跟隨nil通道、nil映射、nil切片和nil數(shù)組指針

遍歷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、34后進(jìn)入阻塞狀態(tài)。 Hello、worldBye不會被輸出。

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")
}

通過nil非接口屬主實參調(diào)用方法不會造成恐慌

一個例子:

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)
}

如果類型T的零值可以用預(yù)聲明的nil標(biāo)識符表示,則*new(T)的估值結(jié)果為一個T類型的nil值

一個例子:

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
}

總結(jié)一下

在Go中,為了簡單和方便,nil被設(shè)計成一個可以表示成很多種類型的零值的預(yù)聲明標(biāo)識符。 換句話說,它可以表示很多內(nèi)存布局不同的值,而不僅僅是一個值。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號