Go中必須談論的四個迷點

不少熟悉Go的程序員們都會說到Go是一門很簡單的語言,話雖如此,但實際上Go的簡單是基於複雜底層的極簡包裝。程序員

Go在不少地方均作了「隱式」的轉換,這也就致使了不少迷惑點,本文總結了Go開發中幾個使人迷惑的地方,若有不當之處請指正。golang

nil到底是什麼

首先明確一點:nil是值而非類型。nil值只能賦值給slice、map、chan、interface和指針。函數

在Go中,任何類型都會有一個初始值。數值類型的初始值爲0,slice、map、chan、interface和指針類型的初始值爲nil,對於nil值的變量,咱們能夠簡化理解爲初始狀態變量。ui

但nil在實際使用過程當中,仍有很多使人迷惑的地方。this

var err error
e := &err
if e != nil {
    fmt.Printf("&err is not nil:%p\n", e)
}
// 輸出:&err is not nil:0xc0000301f0

err是一個接口類型的變量,其初始值爲nil,而後對err進行取址操做會發現能成功取到地址,這就是Go和C++最大的不一樣之一。有C++基礎的人在剛接觸Go的時候,天然而然的會認爲nil是個空指針類型值,上面的代碼力證在Go中,nil只是一個表示初始狀態的值指針

對於slicemapchaninterface,當值爲nil時,不具有可寫性。code

// 1
var s []int
fmt.Printf("%v\n", s[0])
// 輸出panic

// 2
var c chan int
val := <-c
fmt.Printf("%v\n", val)
// 輸出panic

// 3
var m map[int]int
m[1] = 123
// 輸出panic

上面3段代碼均會出現panic,對於slicemapchan類型的nil值變量,能夠理解爲可讀不可寫,只有經過make(new)建立的對象實例知足可寫性。對象

接口的本質

Go官方文檔中表示:interface自己是引用類型,即接口類型自己是指針類型。接口

type Animal interface {
    Barking()
}

type Cat struct {
}

func (c *Cat) Barking() {
    fmt.Printf("Meow~~\n")
}

type Dog struct{}

func (d Dog) Barking() {
    fmt.Printf("W~W~W~\n")
}

Cat和Dog類型都實現了Barking接口,須要注意的是,Cat是以指針接收器方式實現Barking接口,Dog是以值傳遞方式實現Barking接口。在Go中,當調用接口方法時,會自動對指針進行解引用。下面的代碼能夠證實這一點:ci

d := &Dog{}
d.Barking()

c := Cat{}
c.Barking()
/* 輸出:
 W~W~W~
 Meow~~
*/

接口的做爲函數參數如何傳遞?

func AnimalBarking(a Animal) {
    a.Barking()
}

根據上面這段代碼,如何調用AnimalBarking方法呢?
首先明確Animal是引用類型(指針),因爲接口會自動對傳遞的指針進行解引用,因此當接口類型做爲函數參數傳遞時,有如下規則:

  • 當以指針接收器實現接口方法時,傳遞AnimalBarking的參數必須爲對象指針。
  • 當以對象接收器實現接口方法時,傳遞AnimalBarking的參數既能夠是對象指針(指針會自動解引用),也能夠是對象實例。

下面的代碼合法:

d1 := &Dog{}
AnimalBarking(d1)

d2 := Dog{}
AnimalBarking(d2)
指向接口的指針是無心義的。

接口自己是類型,接口類型在runtime中大概是這樣:

type eface struct {
    _type *_type    // 8bytes
    data  unsafe.Pointer    // 8bytes
}

其中_type是實現者(即實現了接口方法的struct),data是指向實現者的指針。那麼,指向接口的指針是什麼?

type Handler interface {
    Func()
}

type Server struct{}

func (s *Server) Func() {
    fmt.Printf("*Server.Func\n")
}

func Func(handler *Handler) {
    handler.Func()
}

上面的代碼在Go1.13下沒法經過編譯:handler.Func undefined (type *Handler is pointer to interface, not interface)

這裏要清楚,指向結構的指針和指向接口的指針是兩回事,接口直接存放告終構的類型信息以及結構指針。在Go中,沒法爲實現了接口方法的struct生成指向接口的指針並調用接口方法。

關於接口的延申閱讀:Go interface

defer機制

在Go中提供defer這樣優雅的函數退出後「收尾」操做,但不少人會忽略defer機制中的一點:defer在聲明時引用到的變量就已被實時編譯。下面的代碼:

var ErrNotFound error = errors.New("Not found")
func TestDefer1() error {
    var err error
    defer fmt.Printf("TestDefer1 err: %v\n", err)

    // ...
    err = ErrNotFound
    return err
}

/* 輸出:
 TestDefer1 err: <nil>
*/

當defer聲明func時,狀況不同了:

func TestDefer2() error {
    var err error
    defer func() {
        fmt.Printf("TestDefer2 err: %v\n", err)
    }()
    // ...
    err = ErrNotFound
    return err
}

/* 輸出:
 TestDefer2 err: Not found
*/

因此:defer在聲明語句時引用到的變量就已被實時編譯

讀寫chan是否應該加鎖

先說答案:不須要。具體緣由能夠從runtime/chan.go中知道。chan的原始struct以下:

type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // list of send waiters

    // lock protects all fields in hchan, as well as several
    // fields in sudogs blocked on this channel.
    //
    // Do not change another G's status while holding this lock
    // (in particular, do not ready a G), as this can deadlock
    // with stack shrinking.
    lock mutex
}

chanstruct定義上來看,有lock字段,再來看看chan的讀寫實現(簡化代碼):

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // ...
    lock(&c.lock)
    // ...
    unlock(&c.lock)
    // ...
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // ...
    lock(&c.lock)
    // ...
    unlock(&c.lock)
    // ...
}

chan的實現源代碼看到,其讀寫內部均加了鎖,實際上在關閉chan時內部也是加鎖了,因此實際應用中,多個coroutine同時讀寫chan時不須要加鎖。

相關文章
相關標籤/搜索