深入理解nil
9.2 深入理解nil
nil
是Go中熟悉且重要的預先宣告的識別符號。它是多種型別零值的字面表示。許多具有其他一些流行語言經驗的新Go程式設計師可能會將其 nil
視為 null
(或 NULL
)其他語言的對應物 。這部分是正確的,但 nil
在Go和 null
(或 NULL
)其他語言之間存在許多差異。
按照Go語言規範,任何型別在未初始化時都對應一個零值:布林型別是false,整型是0,字串是"",而指標,函式,interface,slice,channel和map的零值都是nil。
nil
沒有預設型別
Go中的每個其他預先宣告的識別符號都具有預設型別。例如,
- 預設型別為
true
和false
都是bool
型別。 - 預設型別
iota
是int
。
但是 nil
它沒有預設型別,儘管它有許多可能的型別。編譯器必須有足夠的資訊來 nil
從上下文中推匯出值的型別 。
示例:
package main func main() { // This following line doesn't compile. /* v := nil */ // There must be sufficient information for compiler // to deduce the type of a nil value. _ = (*struct{})(nil) _ = []int(nil) _ = map[int]bool(nil) _ = chan string(nil) _ = (func())(nil) _ = interface{}(nil) // This lines are equivalent to the above lines. var _ *struct{} = nil var _ []int = nil var _ map[int]bool = nil var _ chan string = nil var _ func() = nil var _ interface{} = nil }
nil Go中是一個預先宣告的識別符號
您可以在 nil
不宣告的情況下使用它。
nil
可以表示多種型別的零值
在Go中, nil
可以表示以下型別的零值:
- pointer types (including type-unsafe ones).
- map types.
- slice types.
- function types.
- channel types.
- interface types.
示例:
package main import "fmt" type Person struct { Idint Name string Info interface{} } func main() { var p Person fmt.Println(p)// {0<nil>} }
nil
在Go中不是關鍵字
預先宣佈的 nil
可以被遮蔽。
示例:
package main import "fmt" func main() { nil := 123 fmt.Println(nil) // 123 }
nil
具有不同種類的價值的大小可能不同
一個型別的所有值的記憶體佈局總是相同的。 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 }
nil 使用場景
pointers
nil pointer
- 指向 nil, 又名 nothing
- pointer 的零值
var p *int// 宣告一個 int 型別的指標 println(p)// <nil> p == nil// true *p// panic: runtime error: invalid memory address or nil pointer dereference
指標表示指向記憶體的地址,如果對 nil 的指標進行解引用的話就會導致 panic。那麼為 nil 的指標有什麼用呢? 先來看看一個計算二叉樹和的例子:
type tree struct { v int l *tree r *tree } // first solution func (t *tree) Sum() int { sum := t.v if t.l != nil { sum += t.l.Sum() } if t.r != nil { sum += t.r.Sum() } return sum }
上面程式碼有兩個問題:
-
一個是程式碼重複
if v != nil { v.m() }
另一個是當 t 是 nil 的時候會 panic:
var t *tree sum := t.Sum()// panic: invalid memory address or nil pointer dereference
那,怎麼解決上面的問題呢? 我們先來看看一個指標接收器的例子:
type Person struct{} func sayHi(p *Person) {fmt.Println("hi")} func (p *Person) sayHi() {fmt.Println("hi")} var p *Person p.sayHi()// hi
對於指標物件的方法來說,就算指標的值為 nil, 也是可以呼叫的,基於此,我們可以對剛剛計算的二叉樹的例子進行一下改造:
func (t *tree) Sum() int { if t == nil { return 0 } return t.v + t.l.Sum() + t.r.Sum() }
跟剛才的程式碼一對比是不是簡潔了很多? 對於 nil
指標,只需要在方法前面判斷一下就 OK 了,無需重複判斷。換成列印二叉樹的值或者查詢二叉樹的某個值都是一樣的:Coding Time
func(t *tree) String() string { if t == nil { return "" } return fmt.Sprint(t.l, t.v, t.r) } // nil receivers are useful: Find func (t *tree) Find(v int) bool { if t == nil { return false } return t.v == v || t.l.Find(v) || t.r.Find(v) }
所以如果不是很需要的話,不要用NewX()去初始化值,而是使用它們的預設值。
slices
// nil slices var s []T len(s) // 0 cap(s) // 0 for range s { } // iterates zero times s[i] // panic: index out of range
一個為 nil
的 slice
,除了不能索引外,其他的操作都是可以的, slice
有三個元素,分別是長度、容量、指向陣列的指標,當你需要填充值的時候可以使用 append
函式, slice
會自動進行擴充。所以我們並不需要擔心 slice
的大小,使用 append
的話 slice
會自動擴容。
var s []int for i := 0; i < 10; i++ { fmt.Printf("len: %2d cap: %2d\n", len(s), cap(s)) s = append(s, i) }
那麼為 nil
的slice的底層結構是怎樣的呢?根據官方的文件,slice有三個元素,分別是長度、容量、指向陣列的指標:
map
對於 Go 來說,map,function, channel 都是特殊的指標,指向各自特定的實現,這個我們暫時可以不用管。
// nil maps var m map[t]u len(m)// 0 for range m// interates zero times v, ok := m[i]// zero(u), false m[i] = x// panic: assignment to entry in nil map
對於 nil
的 map
, 我們可以簡單把它看成是一個只讀的 map,不能進行寫操作,否則就會 panic。那麼, nil
的 map 有什麼用呢? 看下這個例子:
func NewGet(url string, headers map[string]string) (*http.Request, error) { req, er := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } for k, v := range headers { req.Header.Set(k, v) } return req, nil }
對於 NewGet 來說,我們需要傳入一個型別為 map 的引數,並且這個函式只是對這個引數進行讀取,我們可以傳入一個非空的值:
NewGet("http://google.com", map[string]string) { "USER_AGENT":"golang/gopher", } // 或者,這樣傳 NewGet("http://google.com", map[string]string{}) // 但是,前面也說了,map 的零值是 nil, 所以當 header 為空的時候,我們也可以直接傳入一個 nil NewGet("http://google.com", nil)
是不是,簡潔很多? so, 把 nil
map 作為一個只讀的空的 map 進行讀取吧
channels
// nil channels var c chan t <- c// blocks forever c <- x// blocks forever close(c)// panic: close of nil channel
關閉一個 nil
的 channel 會導致程式 panic (如何關閉 channel 可以看看這篇文章: 如何優雅的關閉Go Channel ). 舉個例子,假如現在有兩個 channel 負責輸入,一個 channel 負責彙總,簡單的程式碼實現:
func merge(out chan<- int, a, b <-chan int) { for { select { case v := <- a: out <- v case v := <- b: out <- v } } }
closed channels
var c chan t v, ok <- c// zero(t), false c <- x// panic: send on closed channel close(c)// panic: close of nil channel
如果在外部呼叫中關閉了 a 或者 b, 那麼就會不斷地從 a 或者 b 中讀出 0,這和我們想要的不一樣,我們想關閉 a 或 b 後就停止彙總,修改一下程式碼:
func merge (out chan<- int, a, b <-chan int) { for a != nil || b !=nil { select { case v, ok := <-a: if !ok { a = nil fmt.Println("a is nil") continue } out <- v case v, ok := <-b: if !ok { a = nil fmt.Println("b is nil") continue } out <- v } } fmt.Println("close out") close(out) }
在知道 channel 關閉之後,將 channel 的值設為 nil, 這樣子就相當於將這個 select case 子句給停用了,因為 nil
的 channel 是永遠阻塞的。
functions
函式可以被用作結構體欄位, 邏輯上,預設的零值為 nil
type Foo struct { f func() error }
nil funcs for default values
lazy initialization of variables, nil can also imply default behavior
func NewServer(logger func(string, ...interface{})) { if logger == nil { logger = logger.Printf } logger("initializing %s", os.Getenv("hostname")) // ... }
interfaces
interface 並不是一個指標,它的底層實現由兩部分組成,一個是型別,一個是值,也就類似於:(Type, Value). 只有當型別和值都是 nil 的時候,才等於 nil. 看看下面的程式碼:
func do() error { // error: (*doError, nil) var err *doError return err// nil of type *doError } func main() { err := do() fmt.Println(err == nil) // false }
輸出結果:false. do 函式聲明瞭一個 *doError 的變數 err, 然後返回,返回值是 error 介面,但是這個時候的 Type 已經變成了:(*doError, nil), 所以和 nil 肯定是不會相等的。所以我們在寫函式的時候,不要宣告具體的 error 變數,而是應該直接返回 nil:
func do() error { return nil } // 再來看看這個例子 func do() *doError { // nil of type *doError return nil } func wrapDo() error { // error (*doError, nil) return do() // nil of type *doError } func main() { err := wrapDo()// error(*doError, nil) fmt.Println(err == nil) // false }
這裡最終的輸出結果也是 false。 為什麼呢? 儘管 wrapDo 函式返回的是 error 型別, 但是 do 返回的卻是 *doError 型別,也就是變成了 (*doError, nil), 自然也就和 nil 不相等了。因此,不要返回具體的錯誤型別。遵從這兩條建議,才可以放心的使用 if x != nil
.
在Go中, nil
只是一個識別符號,可用於表示某些型別的零值。它不是單一的價值。相反,它可以表示具有不同
儲存器佈局的許多值。
links
- 目錄
- 上一節:
- 下一節: