首頁  >  文章  >  後端開發  >  聊一聊Go語言中的零值,它有什麼用?

聊一聊Go語言中的零值,它有什麼用?

Golang菜鸟
Golang菜鸟轉載
2023-08-08 16:23:011236瀏覽

背景

哈嘍,大家好,我是asong。今天與大家聊聊Go語言中的零值。大學時期我是個C語言愛好者,工作了以後感覺Go語言和C語言很像,所以我選擇了Go語言的工作,時不時就會把這兩種語言的一些特性做個比較,今天要比較的就是零值特性。熟悉C語言的朋友知道在C語言中預設不初始化局部變數。未初始化的變數可以包含任何值,其使用會導致未定義的行為;如果我們未初始局部變量,在編譯時就會報警告C4700,這個警告指示一個Bug,這個 Bug可能導致程式中出現不可預測的結果或故障。而在Go語言就不會有這樣的問題,Go語言的設計者吸收了在設計C語言時的一些經驗,所以Go語言的零值規格如下:

以下內容來自官方blog:https://golang.org/ref/spec#The_zero_value

當透過宣告或new 呼叫為變數分配儲存空間時,或透過複合文字或make 呼叫建立新值時,且未提供明確初始化,則給予變數或值一個預設值。 此類變數或值的每個元素都為其類型設定為零值:布林型為false,數字類型為0,字串為"",指標、函數、介面、切片、通道和映射為nil。 此初始化是遞歸完成的,例如,如果未指定任何值,則結構體陣列的每個元素的欄位都將其清除。

例如這兩個簡單的宣告是等價的:

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语言库的时候,都会看到在初始化对象时采用"动态初始化"的模式,其实就是在创建对象时判断如果是零值就使用默认值,比如我们在分析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真帅"

这样的程序就报错了。

  • 方法接收者的归纳

利用零值可用的特性,我们配合空结构体的方法接受者特性,可以将方法组合起来,在业务代码中便于后续扩展和维护:

type T struct{}

func (t *T) Run() {
  fmt.Println("we run")
}

func main() {
  var t T
  t.Run()
}

我在一些开源项目中看到很多地方都这样使用了,这样的代码最结构化~。

  • 标准库无需显示初始化

我们经常使用sync包中的mutexoncewaitgroup都是无需显示初始化即可使用,拿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语言零值的设计大大便利了开发者,但是零值并不是万能的,有些场景下零值是不可以直接使用的:

  • 未显示初始化的切片、map,他们可以直接操作,但是不能写入数据,否则会引发程序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内置接口类型是表示错误条件的常规接口,nil值表示没有错误,所以调用Error方法时类型error不能是零值,否则会引发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函数

在日常开发中我们会使用到闭包,但是这其中隐藏一个问题,如果我们函数忘记初始化了,那么就会引发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:

  • <-cc 接收將永遠阻塞
  • c <- v 發送值到c 會永遠阻塞
  • close(c) 關閉c 引發panic

關於零值不可用的場景先介紹這些,掌握這些才能在日常開發中減少寫bug的頻率。

總結

總結本文敘說的幾個知識點:

  • Go語言中所有變數或值都有預設值,對程式的安全性和正確性起到了很重要的作用
  • Go語言中的一些標準函式庫利用零值特性來實現,簡化操作
  • #可以利用"零值可用"的特性可以提升程式碼的結構化、讓程式碼更簡單、更緊湊
  • 零值也不是萬能的,有些場景下零值是不可用的,開發時要注意
#

以上是聊一聊Go語言中的零值,它有什麼用?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:Golang菜鸟。如有侵權,請聯絡admin@php.cn刪除