天天看點

golang int 轉string_Golang 零值、空值與空結構

golang int 轉string_Golang 零值、空值與空結構
本文首發于 at7h 的個人部落格。

這篇文章我們讨論下有關 Golang 中的零值(The zero value)、空值(nil)和空結構(The empty struct)的相關問題以及它們的一些用途。

零值

零值是指當你聲明變量(配置設定記憶體)并未顯式初始化時,始終為你的變量自動設定一個預設初始值的政策。

首先我們來看看官方有關零值(The zero value)的規範:

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.

據此我們可總結出:

  • 對于 值類型 :布爾類型為

    false

    , 數值類型為 ,字元串為

    ""

    ,數組和結構會遞歸初始化其元素或字段,即其初始值取決于元素或字段。
  • 對于 引用類型 : 均為

    nil

    ,包括指針 pointer,函數 function,接口 interface,切片 slice,管道 channel,映射 map。

通常,為你聲明的變量賦予一個預設值是有用的,尤其是為你數組和結構中的元素或字段設定預設值,這是一種保證安全性和正确性的做法,同時也可以讓你的代碼保持簡潔。

比如,下面的示例是我們常用的,結構體

Value

中包含兩個 unexported 字段,

sync.Mutex

中也有兩個 unexported 字段。因為有預設零值,是以我們可以直接使用:

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

因為切片是引用類型的,是以其零值也是

nil

package main

import "fmt"
import "strings"

func main(){
    var s []string

    fmt.Println(s, len(s), cap(s)) // [] 0 0
    fmt.Println(s == nil) // true

    s = append(s, "Hello")
    s = append(s, "World")
    fmt.Println(strings.Join(s, ", ")) // Hello, World
}
           

下面的情況需要特别注意下,有時候不注意就容易混淆,

:=

文法糖是

聲明并且初始化變量

的,是以是一個真正的執行個體(為其配置設定了記憶體位址的),并不是零值

nil

package main

import "fmt"
import "reflect"

func main() {
    var s1 []string
    s2 := []string{} // 或者等同于 var s2 = []string{}

    fmt.Println(s1 == nil) // true
    fmt.Println(s2 == nil) // false

    fmt.Println(reflect.DeepEqual(s1, s2)) // false

    fmt.Println(reflect.DeepEqual(s1, []string{}))  // false
    fmt.Println(reflect.DeepEqual(s2, []string{}))  // true
}
           

另外,對于空結構的

nil

是可以調用該類型的方法的,這還可以用來簡單地提供預設值:

package main

import "fmt"

const defaultPath = "/usr/bin/"

type Config struct {
    path string
}

func (c *Config) Path() string {
    if c == nil {
            return defaultPath
    }
    return c.path
}

func main() {
    var c1 *Config
    var c2 = &Config{
            path: "/usr/local/bin/",
    }
    fmt.Println(c1.Path(), c2.Path())
}
           

nil

對于一個剛開始使用 Golang 的開發人員,剛開始接觸

nil

應該是使用它來檢查錯誤,大緻像這樣:

func doSomething() error {
    return nil
}

func main(){
    if doSomething() != nil {
        return err
    }
}
           

這是 Golang 慣用的,它鼓勵開發人員顯式的的将錯誤作為傳回值來處理。現在我們來讨論下這個

nil

,在其他語言中也有類似的定義,比如 C、C++、Java 等中的

null

,Python 中的

None

,但是 Goalng 中的

nil

與它們有着很多差別。

nil

是 Golang 中預先聲明的辨別符(非關鍵字保留字),其主要用來表示引用類型的零值(指針,接口,函數,映射,切片和通道),表示它們未初始化的值。

// [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

是 Golang 中唯一沒有預設類型的非類型化的值,它不是一個未定義的狀态。是以你不能像這樣使用它:

a := nil
// cannot declare variable as untyped nil: a
           

将一個并沒有類型

nil

的值賦給

a

是不對的,編譯器不知道它該給

a

配置設定什麼類型。

值得一提的是 Golang 中比較出名的

nil != nil

的問題,我們來看下面的一個例子:

var p *int
var i interface{}

fmt.Println(p)      // <nil>
fmt.Println(i)      // <nil>

fmt.Println(p == i) // false
           

Why?為什麼同樣都是

nil

卻不相等呢?

帶着問題,我們再來看一個下面的例子(來自官方 Why is my nil error value not equal to nil):

func Foo() error {
    var err *MyError = nil
    if bad() {
        err = ErrBad
    }
    return err
}

func main() {
    err := Foo()
    fmt.Println(err)        // <nil>
    fmt.Println(err == nil) // false
}
           

其罪魁禍首就是

interface

,接口相關的實作原理不在本文的讨論範圍,後面再具體分享。其大緻原理是,接口要确定一個變量需要兩個基礎屬性:

Type

and

Value

,下面我們給上面的兩段代碼加上注釋,就明白了:

var p *int          // (T=*int,V=nil)
var i interface{}   // (T=nil,V=nil)

fmt.Println(p == i) // (T=*int, V=nil) == (T=nil, V=nil) -> false
func Foo() error {
    var err *PathError = nil  // (T=*PathError, V=nil)
    if bad() {
        err = ErrBad
    }
    return err  // 這将始終傳回 non-nil 錯誤
}

func main() {
    err := Foo()
    fmt.Println(err)        // <nil>
    fmt.Println(err == nil) // (T=*PathError, V=nil) == (T=nil, V=nil) -> false
}
           
請注意:為了避免此問題,傳回錯誤時請永遠使用

error

接口,并且永遠不要初始化可能從函數傳回的空錯誤變量。

我們将上面的例子再改改,看下面的例子:

var p *int              // (T=*int, V=nil)
var i interface{}       // (T=nil, V=nil)

fmt.Println(p == nil)   // true
fmt.Println(i == nil)   // true

i = p

fmt.Println(i == nil)     // (T=*int, V=nil) == (T=nil, V=nil) -> false
           

這個問題的實質就是 Go Tour 中的 Interface values with nil underlying values。

示例中

i

可以傳遞給一個

interface{}

作為輸入參數的函數,你隻檢查

i == nil

是不夠的。是以對于接口類型的空指針的判斷,有些時候你并不能安全的依靠

v == nil

,盡管這種檢查的坑很少發生,但這有時候可能會使你的程式崩潰。對此,可以有兩種方式解決,你可以分别将類型和值分别和

nil

比較或者使用反射包

reflect

請記住:如果接口中已存儲任何具體值,那麼接口将不會是

nil

,詳見反射定律。

還有就是,也許你也感到困惑,還是上面的例子,為什麼下面的類型就可以直接比較并獲得準确的結果:

var p *int              // (T=*int, V=nil)

fmt.Println(p == nil)   // true
           

這是因為在進行上面的比較時,因為編譯器已經清楚的知道了

p

的類型,是以編譯器可以轉化為

p == (*int)(nil)

。但是對于接口,編譯器是沒法确定底層類型的,因為它是可以被更改的。

空結構

空結構是沒有任何字段的結構類型,例如:

type Q struct{}
var q struct{}
           

既然沒有任何字段,那它有什麼用呢?

我們知道,一個結構的執行個體的大小(即所占存儲空間的位元組數)是由其字段的寬度(size)和對齊(alignment)共同決定的,這樣有助于尋址速度,C 語言等都有類似的政策,關于 Golang 的具體政策請閱讀 Size and alignment guarantees。

很顯然,空結構的占用空間大小為零位元組:

var q struct {}
fmt.Println(unsafe.Sizeof(q)) // 0
           

由于空結構占用零位元組,是以不需要填充對齊,是以由嵌套空結構的空結構也不會占用存儲空間。

type Q struct {
        A struct{}
        B struct{
            C struct{}
        }
}
var q Q
fmt.Println(unsafe.Sizeof(q)) // 0
           

由于空結構不占用記憶體空間,是以我們聲明以空結構作為元素的數組或切片,也是不占用空間的(Orthogonality in Go):

var x [1000000000]struct{}
fmt.Println(unsafe.Sizeof(x)) // 0

var y = make([]struct{},1000000000)
fmt.Println(unsafe.Sizeof(x))// 24,背後關聯數組為 0
           

對于空結構(或者空數組),其占用的存儲大小為零,是以兩個不同的零大小的變量在記憶體中可能具有相同的位址。

來看下面幾個示例:

var a, b struct{}
fmt.Println(&a == &b)  // true


c := make([]struct{}, 10)
d := make([]struct{}, 20)
fmt.Println(&c[0] == &d[1]) // true


type Q struct{}

func (q *Q)addr() { fmt.Printf("%pn", q) }

func main() {
        var a, b Q
        a.addr()  // 0x5af5a60
        b.addr()  // 0x5af5a60
}

e := struct{}{} // 不是零值,一個真正的執行個體
f := struct{}{}
fmt.Println(e == f) // true
           

請注意,這種相等隻是可能,并不是一定的。

比如這個示例,相關問題解釋請看這個 issue。

說了半天,你可能會想,貌似這些也沒什麼實際的用途啊,下面列舉兩個比較實用的實踐用途:

1. 使用

chan struct{}

代替

chan bool

在 goroutines 之間傳遞信号

。使用

bool

容易讓人不了解該值,

true

or

false

,但是使用

chan struct{}

就很清楚,我們不在乎值,隻關心發生的事兒,更容易表達清楚一些。

2.

為了防止 unkeyed 初始化結構,可以添加

_ struct {}

字段:

type Q struct {
  X, Y int
  _    struct{}
           

這樣一來,使用

Q{X: 1, Y: 1}

可以,但使用

Q{1, 1}

就會出現編譯錯誤:

too few values in struct initializer

,同時這樣也幫助了

go ver

代碼檢查。

參考

  • The Go Programming Language Specification
  • Golang Frequently Asked Questions
  • Why Golang Nil Is Not Always Nil? Nil Explained