GO語言中ni,零值與空結(jié)構(gòu)體的使用
go語言的初學(xué)者,特別是java開發(fā)者新學(xué)習(xí)go語言,對于一些和java類似但是又有差異的概念很容易混淆,比如說go中的零值,nil 和 空結(jié)構(gòu)體。本文就來詳細(xì)探討一下go中這些特殊概念的含義和實際場景中的應(yīng)用:
零值
零值(The Zero Value)可以看作為當(dāng)你聲明了一個變量,但沒有顯式的初始化的時候,系統(tǒng)為變量賦予的一個默認(rèn)初始值。官方對零值的定義如下:
When storage is allocated for a variable, either through a declaration or a call of new, or when a new value is created, either through a composite literal or a call of make, and no explicit initialization is provided, the variable or value is given a default value. Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for numeric types, “” for strings, and nil for pointers, functions, interfaces, slices, channels, and maps. This initialization is done recursively, so for instance each element of an array of structs will have its fields zeroed if no value is specified.
據(jù)此我們可總結(jié)出:
值類型 布爾類型為 false, 數(shù)值類型為 0,字符串為”“,數(shù)組和結(jié)構(gòu)體(struct)會遞歸初始化其元素或字段,即其初始值取決于元素或字段。這里所謂的值類型其實就相當(dāng)于java中的 primary 類型,只是需要注意的是string在java中是對象類型,而go中string則是值類型。
引用類型 均為 nil,包括指針 pointer,函數(shù) function,接口 interface,切片 slice,管道 channel,映射 map。
tip: 其實go里面沒有真正的引用類型,可以粗略的理解為值類型的變量直接存儲值,引用類型的變量存儲的是一個地址,這個地址用于存儲最終的值
值類型
因為有零值的存在,使得我們在使用變量時,大部分情況下可以不必進(jìn)行初始化而直接使用,這樣能夠保持代碼的簡潔性,也能夠盡量避免出現(xiàn)Java開發(fā)中常見的**NullPointerException,**以下是一些例子:
package main import "sync" type Value struct { mu sync.Mutex //無需初始化,聲明就能用 val int } func (v *Value)Incr(){ defer v.mu.Unlock() v.mu.Lock() v.val++ } func main() { var i Value i.Incr() }
sync.Mutex本質(zhì)上是一個結(jié)構(gòu)體:
// A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { state int32 sema uint32 }
那么如果是引用類型,零值為nil,是不是就不能直接用了呢?這個實際上也要分情況,按照類型我們一個個來看:
切片(Slices)
切片的零值是一個nil slice,除了不能按照序號索引查詢以外,其它的操作都能做:
func testNilSlice() { var nilSlice []string fmt.Println(nilSlice == nil) // true fmt.Println(nilSlice[0]) //index out of range emptySlice = append(nilSlice, "dd") // append操作會自動擴(kuò)容 fmt.Println(nilSlice[0]) //輸出dd }
nil slice與not nil slice的區(qū)別:
type Person { Friends []string } var f1 []string //nil切片 json1, _ := json.Marshal(Person{Friends: f1}) fmt.Printf("%s\n", json1) //output:{"Friends": null} f2 := make([]string, 0) //non-nil空切片 ,等價于 f2 := []string{} json2, _ := json.Marshal(Person{Friends: f2}) fmt.Printf("%s\n", json2) //output: {"Friends": []}
推薦在日常使用時,沒有特殊需求都使用var nilSlice []string 這樣的形式聲明空切片:https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices
Map
對于nil的map,我們可以簡單把它看成是一個只讀的map,不能進(jìn)行寫操作,否則就會panic:
func testNilMap() { var m map[string]string fmt.Println(m["key"]) //輸出"" m["key"]="value" //panic: assignment to entry in nil map }
那么nil map有啥用呢,可以看看以下的例子:
func NewGet(url string, headers map[string]string) (*http.Request, error) { req, err := 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 } //調(diào)用該方法時如果沒有header,可以傳入一個空的map,例如: NewGet("http://google.com", map[string]string{}) //也可以直接傳入nil NewGet("http://google.com", nil)
Channel
nil channel會阻塞對該channel的所有讀、寫。所以,可以將某個channel設(shè)置為nil,進(jìn)行強(qiáng)制阻塞,對于select分支來說,就是強(qiáng)制禁用此分支
func addIntegers(c chan int) { sum := 0 t := time.NewTimer(time.Second) for { select { case input := <-c: sum = sum + input case <-t.C: c = nil fmt.Println(sum) // 輸出10 } } } func main() { c := make(chan int, 1) go addIntegers(c) for i := 0; i <= 10; i++ { c <- i time.Sleep(time.Duration(200) * time.Millisecond) } }
指針(Pointers)
指針如果為nil,則對指針進(jìn)行解引用的話,會引發(fā)我們在java中非常熟悉的空指針錯誤
type Person struct { Name string Sex string Age int } var p *Person fmt.Println(p.Name) // panic: runtime error: invalid memory address or nil pointer dereference
神奇的nil
nil 是 Golang 中預(yù)先聲明的標(biāo)識符,其主要用來表示引用類型的零值(指針,接口,函數(shù),映射,切片和通道),表示它們未初始化的值。
// [src/builtin/builtin.go](https://golang.org/src/builtin/builtin.go#L98) // // nil is a predeclared identifier representing the zero value for a // pointer, channel, func, interface, map, or slice type. var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
nil在go語言里面不是一個關(guān)鍵字或者保留字,所以你可以用nil作為變量名(作死):
var nil = errors.New("my god")
nil沒有默認(rèn)的類型,所以不能給一個未聲明類型的變量賦值,也不能和自己比較:
a := nil // cannot declare variable as untyped nil: a fmt.Println(nil == nil) // invalid operation: nil == nil (operator == not defined on nil) fmt.Printf("%T", nil) // use of untyped nil
比較nil時一定要注意nil實際上是有類型的,不同類型的nil是不相等的,比如下面的例子:
var p *int var i interface{} fmt.Println(p) // <nil> fmt.Println(i) // <nil> fmt.Println(p == i) // false
再看一個在實際編碼里面很容易犯的錯誤:
type BusinessError struct { error errorCode int64 } func doBusiness() *BusinessError { return nil } func wrapDoBusiness() error { err := doBusiness() return err } func testError() { err := wrapDoBusiness() //這里面拿到的本質(zhì)上是一個<T:*BusinessError,V:nil>的nil fmt.Println(err == nil) }
建議:如果任何地方有判斷interface是否為 nil 值的邏輯,一定不要寫任何有關(guān)于將interface賦值為具體實現(xiàn)類型(可能為nil)的代碼,如果是 nil 值就直接賦給interface,而不要過具體類型的轉(zhuǎn)換
type BusinessError struct { error errorCode int64 } func doBusiness() *BusinessError { return nil } func wrapDoBusiness() error { err := doBusiness() if err == nil { return nil //如果返回值為nil,直接返回nil,不要做類型轉(zhuǎn)換 } else { return err } } func testError() { err := wrapDoBusiness() fmt.Println(err == nil) }
空結(jié)構(gòu)體
golang 正常的 struct 就是普通的一個內(nèi)存塊,必定是占用一小塊內(nèi)存的,并且結(jié)構(gòu)體的大小是要經(jīng)過邊界,長度的對齊的,但是“空結(jié)構(gòu)體”是不占內(nèi)存的,size 為 0;
var q struct{} fmt.Println(unsafe.Sizeof(q)) // 0
空結(jié)構(gòu)體 struct{ } 為什么會存在的核心理由就是為了節(jié)省內(nèi)存。當(dāng)你需要一個結(jié)構(gòu)體,但是卻絲毫不關(guān)系里面的內(nèi)容,那么就可以考慮空結(jié)構(gòu)體。以下是幾個經(jīng)典的用法:
map & struct{}
map 和 struct {} 一般的結(jié)合姿勢是這樣的:
// 創(chuàng)建 map m := make(map[int]struct{}) // 賦值 m[1] = struct{}{} // 判斷 key 鍵存不存在 _, ok := m[1]
一般 map 和 struct {} 的結(jié)合使用場景是:只關(guān)心 key,不關(guān)注值。比如查詢 key 是否存在就可以用這個數(shù)據(jù)結(jié)構(gòu),通過 ok 的值來判斷這個鍵是否存在,map 的查詢復(fù)雜度是 O(1) 的,查詢很快。這種方式在部分場景下可以起到類似Java中Set的作用
chan & struct{}
channel 和 struct{} 結(jié)合是一個最經(jīng)典的場景,struct{} 通常作為一個信號來傳輸,并不關(guān)注其中內(nèi)容。chan 本質(zhì)的數(shù)據(jù)結(jié)構(gòu)是一個管理結(jié)構(gòu)加上一個 ringbuffer ,如果 struct{} 作為元素的話,ringbuffer 就是 0 分配的。
chan 和 struct{} 結(jié)合基本只有一種用法,就是信號傳遞,空結(jié)構(gòu)體本身攜帶不了值,所以也只有這一種用法啦,一般來說,配合 no buffer 的 channel 使用。
waitc := make(chan struct{}) // ... goroutine 1: // 發(fā)送信號: 投遞元素 waitc <- struct{} // 發(fā)送信號: 關(guān)閉 close(waitc) goroutine 2: select { // 收到信號,做出對應(yīng)的動作 case <-waitc: }
到此這篇關(guān)于GO語言中ni,零值與空結(jié)構(gòu)體的文章就介紹到這了,更多相關(guān)GO ni 零值 空結(jié)構(gòu)體內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言中的空值(nil)與零值(zerovalue)區(qū)別詳解
在Go語言中,空值(nil)和零值(zero value)是兩個不同的概念,它們在語義、使用場景以及實際的編程實踐中有著明顯的區(qū)別,理解這兩者的差異對于編寫清晰、健壯的Go代碼至關(guān)重要,需要的朋友可以參考下2024-06-06詳解Go語言中new和make關(guān)鍵字的區(qū)別
本篇文章來介紹一道非常常見的面試題,到底有多常見呢?可能很多面試的開場白就是由此開始的。那就是 new 和 make 這兩個內(nèi)置函數(shù)的區(qū)別,希望對大家有所幫助2023-03-03基于Go?goroutine實現(xiàn)一個簡單的聊天服務(wù)
對于聊天服務(wù),想必大家都不會陌生,因為在我們的生活中經(jīng)常會用到,本文我們用?Go?并發(fā)來實現(xiàn)一個聊天服務(wù)器,這個程序可以讓一些用戶通過服務(wù)器向其它所有用戶廣播文本消息,文中通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06