Go語言中的零值,它有什么用?
今天想跟大家聊一聊Go語言中的零值。
大學時期我是一名C語言愛好者,工作了以后感覺Go語言和C語言很像,所以選擇了Go語言的工作,時不時就會把這兩種語言的一些特性做個比較,今天要比較的就是零值特性。熟悉C語言的朋友知道在C語言中默認情況下不初始化局部變量。未初始化的變量可以包含任何值,其使用會導致未定義的行為;如果我們未初始局部變量,在編譯時就會報警告 C4700,這個警告指示一個Bug,這個Bug可能導致程序中出現(xiàn)不可預測的結果或故障。而在Go語言就不會有這樣的問題,Go語言的設計者吸取了在設計C語言時的一些經驗,所以Go語言的零值規(guī)范如下:
以下內容來自官方blog:https://golang.org/ref/spec#The_zero_value
當通過聲明或 new 調用為變量分配存儲空間時,或通過復合文字或 make 調用創(chuàng)建新值時,且未提供顯式初始化,則給出變量或值一個默認值。此類變量或值的每個元素都為其類型設置為零值:布爾型為 false,數(shù)字類型為 0,字符串為 "",指針、函數(shù)、接口、切片、通道和映射為 nil。此初始化是遞歸完成的,例如,如果未指定任何值,則結構體數(shù)組的每個元素的字段都將其清零。
例如這兩個簡單的聲明是等價的:
var?i?int?
var?i?int?=?0
在或者這個結構體的聲明:
type?T?struct?{?i?int;?f?float64;?next?*T?}
t?:=?new(T)
這個結構體t中成員字段零值如下:
t.i?==?0
t.f?==?0.0
t.next?==?nil
Go語言中這種始終將值設置為已知默認值的特性對于程序的安全性和正確性起到了很重要的作用,這樣也使整個Go程序更簡單、更緊湊。
零值有什么用
通過零值來提供默認值
我們在看一些Go語言庫的時候,都會看到在初始化對象時采用"動態(tài)初始化"的模式,其實就是在創(chuàng)建對象時判斷如果是零值就使用默認值,比如我們在分析hystrix-go這個庫時,在配置Command時就是使用的這種方式:
func?ConfigureCommand(name?string,?config?CommandConfig)?{
?settingsMutex.Lock()
?defer?settingsMutex.Unlock()
?timeout?:=?DefaultTimeout
?if?config.Timeout?!=?0?{
??timeout?=?config.Timeout
?}
?max?:=?DefaultMaxConcurrent
?if?config.MaxConcurrentRequests?!=?0?{
??max?=?config.MaxConcurrentRequests
?}
?volume?:=?DefaultVolumeThreshold
?if?config.RequestVolumeThreshold?!=?0?{
??volume?=?config.RequestVolumeThreshold
?}
?sleep?:=?DefaultSleepWindow
?if?config.SleepWindow?!=?0?{
??sleep?=?config.SleepWindow
?}
?errorPercent?:=?DefaultErrorPercentThreshold
?if?config.ErrorPercentThreshold?!=?0?{
??errorPercent?=?config.ErrorPercentThreshold
?}
?circuitSettings[name]?=?&Settings{
??Timeout:????????????????time.Duration(timeout)?*?time.Millisecond,
??MaxConcurrentRequests:??max,
??RequestVolumeThreshold:?uint64(volume),
??SleepWindow:????????????time.Duration(sleep)?*?time.Millisecond,
??ErrorPercentThreshold:??errorPercent,
?}
}
通過零值判斷進行默認值賦值,增強了Go程序的健壯性。
開箱即用
為什么叫開箱即用呢?因為Go語言的零值讓程序變得更簡單了,有些場景我們不需要顯示初始化就可以直接用,舉幾個例子:
切片,他的零值是 nil,即使不用make進行初始化也是可以直接使用的,例如:
package?main
import?(
????"fmt"
????"strings"
)
func?main()?{
????var?s?[]string
????s?=?append(s,?"asong")
????s?=?append(s,?"真帥")
????fmt.Println(strings.Join(s,?"?"))
}
但是零值也并不是萬能的,零值切片不能直接進行賦值操作:
var?s?[]string
s[0]?=?"asong真帥"
這樣的程序就報錯了。
方法接收者的歸納
利用零值可用的特性,我們配合空結構體的方法接受者特性,可以將方法組合起來,在業(yè)務代碼中便于后續(xù)擴展和維護:
type?T?struct{}
func?(t?*T)?Run()?{
??fmt.Println("we?run")
}
func?main()?{
??var?t?T
??t.Run()
}
我在一些開源項目中看到很多地方都這樣使用了,這樣的代碼最結構化~。
標準庫無需顯示初始化
我們經常使用sync包中的mutex、once、waitgroup都是無需顯示初始化即可使用,拿mutex包來舉例說明,我們看到mutex的結構如下:
type?Mutex?struct?{
?state?int32
?sema??uint32
}
這兩個字段在未顯示初始化時默認零值都是0,所以我們就看到上鎖代碼就針對這個特性來寫的:
func?(m?*Mutex)?Lock()?{
?//?Fast?path:?grab?unlocked?mutex.
?if?atomic.CompareAndSwapInt32(&m.state,?0,?mutexLocked)?{
??if?race.Enabled?{
???race.Acquire(unsafe.Pointer(m))
??}
??return
?}
?//?Slow?path?(outlined?so?that?the?fast?path?can?be?inlined)
?m.lockSlow()
}
原子操作交換時使用的old值就是0,這種設計讓mutex調用者無需考慮對mutex的初始化則可以直接使用。
還有一些其他標準庫也使用零值可用的特性,使用方法都一樣,就不在舉例了。
零值并不是萬能
Go語言零值的設計大大便利了開發(fā)者,但是零值并不是萬能的,有些場景下零值是不可以直接使用的:
未顯示初始化的切片、map,他們可以直接操作,但是不能寫入數(shù)據,否則會引發(fā)程序panic:
var?s?[]string
s[0]?=?"asong"
var?m?map[string]bool
m["asong"]?=?true
這兩種寫法都是錯誤的使用。
零值的指針
零值的指針就是指向nil的指針,無法直接進行運算,因為是沒有無內容的地址:
var?p?*uint32
*p++?//?panic:?panic:?runtime?error:?invalid?memory?address?or?nil?pointer?dereference
這樣才可以:
func?main()?{
?var?p?*uint64
?a?:=?uint64(0)
?p?=?&a
?*p++
?fmt.Println(*p)?//?1
}
零值的error類型
error內置接口類型是表示錯誤條件的常規(guī)接口,nil值表示沒有錯誤,所以調用Error方法時類型error不能是零值,否則會引發(fā)panic:
func?main()?{
?rs?:=?res()
?fmt.Println(rs.Error())
}
func?res()?error?{
?return?nil
}
panic:?runtime?error:?invalid?memory?address?or?nil?pointer?dereference
[signal?SIGSEGV:?segmentation?violation?code=0x1?addr=0x0?pc=0x10a6f27]
閉包中的nil函數(shù)
在日常開發(fā)中我們會使用到閉包,但是這其中隱藏一個問題,如果我們函數(shù)忘記初始化了,那么就會引發(fā)panic:
var?f?func(a,b,c?int)
func?main(){
??f(1,2,3)?//?panic:?runtime?error:?invalid?memory?address?or?nil?pointer?dereference
}
零值channels
我們都知道channels的默認值是nil,給定一個nil channel c:
<-c從c接收將永遠阻塞c <- v發(fā)送值到c會永遠阻塞close(c)關閉c引發(fā)panic
關于零值不可用的場景先介紹這些,掌握這些才能在日常開發(fā)中減少寫bug的頻率。
總結
總結一下本文敘說的幾個知識點:
Go語言中所有變量或者值都有默認值,對程序的安全性和正確性起到了很重要的作用Go語言中的一些標準庫利用零值特性來實現(xiàn),簡化操作可以利用"零值可用"的特性可以提升代碼的結構化、使代碼更簡單、更緊湊 零值也不是萬能的,有一些場景下零值是不可用的,開發(fā)時要注意
? ?

???
