天天看點

關于Go,你可能不注意的7件事

轉自: https://tonybai.com/2015/09/17/7-things-you-may-not-pay-attation-to-in-go/

Go以簡潔著稱,但簡潔中不乏值得玩味的小細節。這些小細節不如goroutine、interface和channel那樣"高大上","屌 絲"得可能不經常被人注意到,但它們卻對了解Go語言有着重要的作用。這裡想挑出一些和大家一起通過詳實的例子來逐一展開和了解。本文内容較為基礎,适合初學者,高手可飄過:)

一、源檔案字元集和字元集編碼

Go源碼檔案預設采用Unicode字元集,Unicode碼點(code point)和記憶體中位元組序列(byte sequence)的變換實作使用了UTF-8:一種變長多位元組編碼,同時也是一種事實字元集編碼标準,為Linux、MacOSX 上的預設字元集編碼,是以使用Linux或MacOSX進行Go程式開發,你會省去很多字元集轉換方面的煩惱。但如果你是在Windows上使用 預設編輯器編輯Go源碼文本,當你編譯以下代碼時會遇到編譯錯誤:

//hello.go

package main

import "fmt"

func main() {

    fmt.Println("中國人")

}

$ go build hello.go

# command-line-arguments

hello.go:6 illegal UTF-8 sequence d6 d0

hello.go:6 illegal UTF-8 sequence b9

hello.go:6 illegal UTF-8 sequence fa c8

hello.go:6 illegal UTF-8 sequence cb 22

hello.go:6 newline in string

hello.go:7 syntax error: unexpected }, expected )

這是因為Windows預設采用的是CP936字元集編碼,也就是GBK編碼,“中國人”三個字的記憶體位元組序列為:

“d0d6    fab9    cbc8    000a” (通過iconv轉換,然後用od -x檢視)

這個位元組序列并非utf-8位元組序列,Go編譯器是以無法識别。要想通過編譯,需要将該源檔案轉換為UTF-8編碼格式。

字元集編碼對字元和字元串字面值(Literal)影響最大,在Go中對于字元串我們可以有三種寫法:

1) 字面值

var s = "中國人"

2) 碼點表示法

var s1 = "\u4e2d\u56fd\u4eba"

or

var s2 = "\U00004e2d\U000056fd\U00004eba"

3) 位元組序清單示法(二進制表示法)

var s3 = "\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba"

這三種表示法中,除字面值轉換為位元組序列存儲時根據編輯器儲存的源碼檔案編碼格式之外,其他兩種均不受編碼格式影響。我們可以通過逐位元組輸出來查 看位元組序列的内容:

    fmt.Println("s byte sequence:")

    for i := 0; i < len(s); i++ {

        fmt.Printf("0x%x ", s[i])

    }

    fmt.Println("")

二、續行

良好的代碼style一般會要求代碼中不能有太long的代碼行,否則會影響代碼閱讀者的體驗。在C中有續行符"\"專門用于代碼續行處理;但在 Go中沒有專屬續行符,如何續行需要依據Go的文法規則(參見Go spec)。

Go與C一樣,都是以分号(";")作為語句結束的辨別。不過大多數情況下,分号無需程式員手工輸入,而是由編譯器自動識别語句結束位置,并插入 分号。是以續行要選擇合法的位置。下面代碼展示了一些合法的續行位置:(别嫌太醜,這裡僅僅是展示合法位置的demo)

//details-in-go/2/newline.go

… …

var (

    s = "This is an example about code newline," +

        "for string as right value"

    d = 5 + 4 + 7 +

        4

    a = [...]int{5, 6, 7,

        8}

    m = make(map[string]int,

        100)

    c struct {

        m1     string

        m2, m3 int

        m4     *float64

    f func(int,

        float32) (int,

        error)

)

func foo(int, int) (string, error) {

    return "",

        nil

    if i := d; i >

        100 {

    var sum int

    for i := 0; i < 100; i = i +

        1 {

        sum += i

    foo(1,

        6)

    var i int

    fmt.Printf("%s, %d\n",

        "this is a demo"+

            " of fmt Printf",

        i)

實際編碼中,我們可能經常遇到的是fmt.Printf系列方法中format string太長的情況,但由于Go不支援相鄰字元串自動連接配接(concatenate),隻能通過+來連接配接fmt字元串,且+必須放在前一行末尾。另外Gofmt工具會自動調整一些不合理的續行處理,主要針對 for, if等控制語句。

三、Method Set

Method Set是Go文法中一個重要的隐式概念,在為interface變量做動态類型指派、embeding struct/interface、type alias、method expression時都會用到Method Set這個重要概念。

1、interface的Method Set

根據Go spec,interface類型的Method Set就是其interface(An interface type specifies a method set called its interface)。

type I interface {

    Method1()

    Method2()

I的Method Set包含的就是其literal中的兩個方法:Method1和Method2。我們可以通過reflect來擷取interface類型的 Method Set:

//details-in-go/3/interfacemethodset.go

import (

    "fmt"

    "reflect"

    var i *I

    elemType := reflect.TypeOf(i).Elem()

    n := elemType.NumMethod()

    for i := 0; i < n; i++ {

        fmt.Println(elemType.Method(i).Name)

運作結果:

$go run interfacemethodset.go

Method1

Method2

2、除interface type外的類型的Method Set

對于非interface type的類型T,其Method Set為所有receiver為T類型的方法組成;而類型*T的Method Set則包含所有receiver為T和*T類型的方法。

// details-in-go/3/othertypemethodset.go

import "./utils"

type T struct {

func (t T) Method1() {

func (t *T) Method2() {

func (t *T) Method3() {

    var t T

    utils.DumpMethodSet(&t)

    var pt *T

    utils.DumpMethodSet(&pt)

我們要dump出T和*T各自的Method Set,運作結果如下:

$go run othertypemethodset.go

main.T's method sets:

     Method1

*main.T's method sets:

     Method2

     Method3

可以看出類型T的Method set僅包含一個receiver類型為T的方法:Method1,而*T的Method Set則包含了T的Method Set以及所有receiver類型為*T的Method。

如果此時我們有一個interface type如下:

那下面哪個指派語句合法呢?合不合法完全依賴于右值類型是否實作了interface type I的所有方法,即右值類型的Method Set是否包含了I的 所有方法。

var t T

var pt *T

var i I = t

var i I = pt

編譯錯誤告訴我們:

     var i I = t // cannot use t (type T) as type I in assignment:

                  T does not implement I (Method2 method has pointer receiver)

T的Method Set中隻有Method1一個方法,沒有實作I接口中的 Method2,是以不能用t指派給i;而*T實作了I的所有接口,指派合 法。不過Method set校驗僅限于在指派給interface變量時進行,無論是T還是*T類型的方法集中的方法,對于T或*T類型變量都是可見且可以調用的,如下面代碼 都是合法的:

    pt.Method1()

    t.Method3()

因為Go編譯器會自動為你的代碼做receiver轉換:

    pt.Method1() <=> (*pt).Method1()

    t.Method3() <=> (&t).Method3()

很多人糾結于method定義時receiver的類型(T or *T),個人覺得有兩點考慮:

1) 效率

   Go方法調用receiver是以傳值的形式傳入方法中的。如果類型size較大,以value形式傳入消耗較大,這時指針類型就是首選。

2) 是否指派給interface變量、以什麼形式指派

   就像本節所描述的,由于T和*T的Method Set可能不同,我們在設計Method receiver type時需要考慮在interface指派時通過對Method set的校驗。

3、embeding type的Method Set

【interface embeding】

我們先來看看interface類型embeding。例子如下:

//details-in-go/3/embedinginterface.go

type I1 interface {

    I1Method1()

    I1Method2()

type I2 interface {

    I2Method()

type I3 interface {

    I1

    I2

    utils.DumpMethodSet((*I1)(nil))

    utils.DumpMethodSet((*I2)(nil))

    utils.DumpMethodSet((*I3)(nil))

$go run embedinginterface.go

main.I1's method sets:

     I1Method1

     I1Method2

main.I2's method sets:

     I2Method

main.I3's method sets:

可以看出嵌入interface type的interface type I3的Method Set包含了被嵌入的interface type:I1和I2的Method

Set。很多情況下,我們Go的interface type中僅包含有少量方法,常常僅是一個Method,通過interface type

embeding來定義一個新interface,這是Go的一個慣用法,比如我們常用的io包中的Reader,

Writer以及ReadWriter接口:

type Reader interface {

    Read(p []byte) (n int, err error)

type Writer interface {

    Write(p []byte) (n int, err error)

type ReadWriter interface {

    Reader

    Writer

【struct embeding interface】

在struct中嵌入interface type後,struct的Method Set中将包含interface的Method Set:

func (T) Method1() {

}

… …

func main() {

    … …

    var pt = &T{

        I1: I1Impl{},

輸出結果與預期一緻:

main.T's method sets:

【struct embeding struct】

在struct中embeding struct提供了一種“繼承”的手段,外部的Struct可以“繼承”嵌入struct的所有方法(無論receiver是T還是*T類型)實作,但 Method Set可能會略有不同。看下面例子:

//details-in-go/3/embedingstructinstruct.go

func (T) InstMethod1OfT() {

func (T) InstMethod2OfT() {

func (*T) PtrMethodOfT() {

type S struct {

func (S) InstMethodOfS() {

func (*S) PtrMethodOfS() {

type C struct {

    T

    *S

    var c = C{S: &S{}}

    utils.DumpMethodSet(&c)

    var pc = &C{S: &S{}}

    utils.DumpMethodSet(&pc)

    c.InstMethod1OfT()

    c.PtrMethodOfT()

    c.InstMethodOfS()

    c.PtrMethodOfS()

    pc.InstMethod1OfT()

    pc.PtrMethodOfT()

    pc.InstMethodOfS()

    pc.PtrMethodOfS()

$go run embedingstructinstruct.go

main.C's method sets:

     InstMethod1OfT

     InstMethod2OfT

     InstMethodOfS

     PtrMethodOfS

*main.C's method sets:

     PtrMethodOfT

可以看出:

類型C的Method Set = T的Method Set + *S的Method Set

類型*C的Method Set = *T的Method Set + *S的Method Set

同時通過例子可以看出,無論是T還是*S的方法,C或*C類型變量均可調用(編譯器甜頭),不會被局限在Method Set中。

4、alias type的Method Set

Go支援為已有類型定義alias type,如:

type MyInterface I

type Mystruct T

對于alias type, Method Set是如何定義的呢?我們看下面例子:

//details-in-go/3/aliastypemethodset.go

    IMethod1()

    IMethod2()

func (T) InstMethod() {

func (*T) PtrMethod() {

type MyStruct T

    utils.DumpMethodSet((*I)(nil))

    var t T

    var pt = &T{}

    utils.DumpMethodSet((*MyInterface)(nil))

    var m MyStruct

    utils.DumpMethodSet(&m)

    var pm = &MyStruct{}

    utils.DumpMethodSet(&pm)

$go run aliastypemethodset.go

main.I's method sets:

     IMethod1

     IMethod2

     InstMethod

     PtrMethod

main.MyInterface's method sets:

main.MyStruct's method set is empty!

*main.MyStruct's method set is empty!

從例子的結果上來看,Go對于interface和struct的alias type給出了“不一緻”的結果:

MyInterface的Method Set與接口類型I Method Set一緻;

而MyStruct并未得到T的哪怕一個Method,MyStruct的Method Set為空。

四、Method Type、Method Expression、Method Value

Go中沒有class,方法與對象通過receiver聯系在一起,我們可以為任何非builtin類型定義method:

    a int

func (t T) Get() int       { return t.a }

func (t *T) Set(a int) int { t.a = a; return t.a }

在C++等OO語言中,對象在調用方法時,編譯器會自動在方法的第一個參數中傳入this/self指針,而對于Go來 說,receiver也是同樣道理,将T的method轉換為普通function定義:

func Get(t T) int       { return t.a }

func Set(t *T, a int) int { t.a = a; return t.a }

這種function形式被稱為Method Type,也可以稱為Method的signature。

Method的一般使用方式如下:

t.Get()

t.Set(1)

不過我們也可以像普通function那樣使用它,根據上面的Method Type定義:

T.Get(t)

(*T).Set(&t, 1)

這種以直接以類型名T調用方法M的表達方法稱為Method Expression。類型T隻能調用T的Method Set中的方法;同理*T隻能調用*T的Method Set中的方法。上述例子中T的Method Set中隻有Get,是以T.Get是合法的。但T.Set則不合法:

    T.Set(2) //invalid method expression T.Set (needs pointer receiver: (*T).Set)

我們隻能使用(*T).Set(&t, 11)。

這樣看來Method Expression有些類似于C++中的static方法(以該類的某個對象執行個體作為第一個參數)。

另外Method express自身類型就是一個普通function,可以作為右值指派給一個函數類型的變量:

    f1 := (*T).Set //函數類型:func (t *T, int)int

    f2 := T.Get //函數類型:func(t T)int

    f1(&t, 3)

    fmt.Println(f2(t))

Go中還定義了一種與Method有關的文法:如果一個表達式t具有靜态類型T,M是T的Method Set中的一個方法,那麼t.M即為Method Value。注意這裡是t.M而不是T.M。

    f3 := (&t).Set //函數類型:func(int)int

    f3(4)

    f4 := t.Get//函數類型:func()int   

    fmt.Println(f4())

可以看出,Method value與Method Expression不同之處在于,Method value綁定了T對象執行個體,它的函數原型并不包含Method Expression函數原型中的第一個參數。完整例子參見:details-in-go/4/methodexpressionandmethodvalue.go。

五、for range“坑”大閱兵

for range的引入提升了Go的表達能力,但for range顯然不是”免費的午餐“,在享用這個美味前,需要搞清楚for range的一些坑。

1、iteration variable重用

for range的idiomatic的使用方式是使用short variable declaration(:=)形式在for

expression中聲明iteration variable,但需要注意的是這些variable在每次循環體中都會被重用,而不是重新聲明。

//details-in-go/5/iterationvariable.go

    var m = [...]int{1, 2, 3, 4, 5}

    for i, v := range m {

        go func() {

            time.Sleep(time.Second * 3)

            fmt.Println(i, v)

        }()

    time.Sleep(time.Second * 10)

在我的Mac上,輸出結果如下:

$go run iterationvariable.go

4 5

各個goroutine中輸出的i,v值都是for range循環結束後的i, v最終值,而不是各個goroutine啟動時的i, v值。一個可行的fix方法:

        go func(i, v int) {

        }(i, v)

2、range expression副本參與iteration

range後面接受的表達式的類型包括:array, pointer to array, slice, string, map和channel(有讀權限的)。我們以array為例來看一個簡單的例子:

//details-in-go/5/arrayrangeexpression.go

func arrayRangeExpression() {

    var a = [5]int{1, 2, 3, 4, 5}

    var r [5]int

    fmt.Println("a = ", a)

    for i, v := range a {

        if i == 0 {

            a[1] = 12

            a[2] = 13

        }

        r[i] = v

    fmt.Println("r = ", r)

我們期待輸出結果:

a =  [1 2 3 4 5]

r =  [1 12 13 4 5]

a =  [1 12 13 4 5]

但實際輸出結果卻是:

r =  [1 2 3 4 5]

我們原以為在第一次iteration,也就是i = 0時,我們對a的修改(a[1] = 12,a[2] = 13)會在第二次、第三次循環中被v取出,但結果卻是v取出的依舊是a被修改前的值:2和3。這就是for range的一個不大不小的坑:range expression副本參與循環。也就是說在上面這個例子裡,真正參與循環的是a的副本,而不是真正的a,僞代碼如 下:

    for i, v := range a' {//a' is copy from a

Go中的數組在内部表示為連續的位元組序列,雖然長度是Go數組類型的一部分,但長度并不包含的數組的内部表示中,而是由編譯器在編譯期計算出

來。這個例子中,對range表達式的拷貝,即對一個數組的拷貝,a'則是Go臨時配置設定的連續位元組序列,與a完全不是一塊記憶體。是以無論a被

如何修改,其副本a'依舊保持原值,并且參與循環的是a',是以v從a'中取出的仍舊是a的原值,而非修改後的值。

我們再來試試pointer to array:

func pointerToArrayRangeExpression() {

    fmt.Println("pointerToArrayRangeExpression result:")

    fmt.Println("a = ", a)

    for i, v := range &a {

        r[i] = v

這回的輸出結果如下:

pointerToArrayRangeExpression result:

a =  [1 2 3 4 5]

我們看到這次r數組的值與最終a被修改後的值一緻了。這個例子中我們使用了*[5]int作為range表達式,其副本依舊是一個指向原數組 a的指針,是以後續所有循環中均是&a指向的原數組親自參與的,是以v能從&a指向的原數組中取出a修改後的值。

idiomatic go建議我們盡可能的用slice替換掉array的使用,這裡用slice能否實作預期的目标呢?我們來試試:

func sliceRangeExpression() {

    fmt.Println("sliceRangeExpression result:")

    for i, v := range a[:] {

顯然用slice也能實作預期要求。我們可以分析一下slice是如何做到的。slice在go的内部表示為一個struct,由(*T,

len, cap)組成,其中*T指向slice對應的underlying

array的指針,len是slice目前長度,cap為slice的最大容量。當range進行expression複制時,它實際上複制的是一個

slice,也就是那個struct。副本struct中的*T依舊指向原slice對應的array,為此對slice的修改都反映到

underlying array a上去了,v從副本struct中*T指向的underlying

array中擷取數組元素,也就得到了被修改後的元素值。

slice與array還有一個不同點,就是其len在運作時可以被改變,而array的len是一個常量,不可改變。那麼len變化的 slice對for range有何影響呢?我們繼續看一個例子:

func sliceLenChangeRangeExpression() {

    var a = []int{1, 2, 3, 4, 5}

    var r = make([]int, 0)

    fmt.Println("sliceLenChangeRangeExpression result:")

            a = append(a, 6, 7)

        r = append(r, v)

輸出結果:

a =  [1 2 3 4 5 6 7]

在這個例子中,原slice a在for

range過程中被附加了兩個元素6和7,其len由5增加到7,但這對于r卻沒有産生影響。這裡的原因就在于a的副本a'的内部表示struct中的

len字段并沒有改變,依舊是5,是以for range隻會循環5次,也就隻擷取a對應的underlying數組的前5個元素。

range的副本行為會帶來一些性能上的消耗,尤其是當range

expression的類型為數組時,range需要複制整個數組;而當range expression類型為pointer to

array或slice時,這個消耗将小得多,僅僅需要複制一個指針或一個slice的内部表示(一個struct)即可。我們可以通過

benchmark test來看一下三種情況的消耗情況對比:

對于元素個數為100的int數組或slice,測試結果如下:

//details-in-go/5/arraybenchmark

go test -bench=.

testing: warning: no tests to run

PASS

BenchmarkArrayRangeLoop-4             20000000           116 ns/op

BenchmarkPointerToArrayRangeLoop-4    20000000            64.5 ns/op

BenchmarkSliceRangeLoop-4             20000000            70.9 ns/op

可以看到range expression類型為slice或pointer to array的性能相近,消耗都近乎是數組類型的1/2。

3、其他range expression類型

對于range後面的其他表達式類型,比如string, map, channel,for range依舊會制作副本。

【string】

對string來說,由于string的内部表示為struct {*byte,

len),并且string本身是immutable的,是以其行為和消耗和slice expression類似。不過for

range對于string來說,每次循環的機關是rune(code

point的值),而不是byte,index為疊代字元碼點的第一個位元組的position:

    var s = "中國人"

    for i, v := range s {

        fmt.Printf("%d %s 0x%x\n", i, string(v), v)

0 中 0x4e2d

3 國 0x56fd

6 人 0x4eba

如果s中存在非法utf8位元組序列,那麼v将傳回0xFFFD這個特殊值,并且在接下來一輪循環中,v将僅前進一個位元組:

//byte sequence of s: 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba

    var sl = []byte{0xe4, 0xb8, 0xad, 0xe5, 0x9b, 0xbd, 0xe4, 0xba, 0xba}

    for _, v := range sl {

        fmt.Printf("0x%x ", v)

    fmt.Println("\n")

    sl[3] = 0xd0

    sl[4] = 0xd6

    sl[5] = 0xb9

    for i, v := range string(sl) {

        fmt.Printf("%d %x\n", i, v)

0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba

0 4e2d

3 fffd

4 5b9

6 4eba

以上例子源碼在details-in-go/5/stringrangeexpression.go中可以找到。

【map】

對于map來說,map内部表示為一個指針,指針副本也指向真實map,是以for range操作均操作的是源map。

for range不保證每次疊代的元素次序,對于下面代碼:

 var m = map[string]int{

        "tony": 21,

        "tom":  22,

        "jim":  23,

    for k, v := range m {

        fmt.Println(k, v)

輸出結果可能是:

tom 22

jim 23

tony 21

也可能是:

tony 21

tom 22

或其他可能。

如果map中的某項在循環到達前被在循環體中删除了,那麼它将不會被iteration variable擷取到。

    counter := 0

    for k, v := range m {

        if counter == 0 {

            delete(m, "tony")

        counter++

    fmt.Println("counter is ", counter)

反複運作多次,我們得到的兩個結果:

counter is  3

counter is  2

如果在循環體中新建立一個map元素項,那該項元素可能出現在後續循環中,也可能不出現:

    m["tony"] = 21

    counter = 0

            m["lucy"] = 24

執行結果:

lucy 24

counter is  4

以上代碼可以在details-in-go/5/maprangeexpression.go中可以找到。

【channel】

對于channel來說,channel内部表示為一個指針,channel的指針副本也指向真實channel。

for range最終以阻塞讀的方式阻塞在channel expression上(即便是buffered channel,當channel中無資料時,for range也會阻塞在channel上),直到channel關閉:

//details-in-go/5/channelrangeexpression.go

    var c = make(chan int)

    go func() {

        time.Sleep(time.Second * 3)

        c <- 1

        c <- 2

        c <- 3

        close(c)

    }()

    for v := range c {

        fmt.Println(v)

1

2

3

如果channel變量為nil,則for range将永遠阻塞。

六、select求值 

golang引入的select為我們提供了一種在多個channel間實作“多路複用”的一種機制。select的運作機制這裡不贅述,但select的case expression的求值順序我們倒是要通過一個例子來了解一下:

// details-in-go/6/select.go

func takeARecvChannel() chan int {

    fmt.Println("invoke takeARecvChannel")

    c := make(chan int)

        time.Sleep(3 * time.Second)

    return c

func getAStorageArr() *[5]int {

    fmt.Println("invoke getAStorageArr")

    var a [5]int

    return &a

func takeASendChannel() chan int {

    fmt.Println("invoke takeASendChannel")

    return make(chan int)

func getANumToChannel() int {

    fmt.Println("invoke getANumToChannel")

    return 2

    select {

    //recv channels

    case (getAStorageArr())[0] = <-takeARecvChannel():

        fmt.Println("recv something from a recv channel")

        //send channels

    case takeASendChannel() <- getANumToChannel():

        fmt.Println("send something to a send channel")

$go run select.go

invoke takeARecvChannel

invoke takeASendChannel

invoke getANumToChannel

invoke getAStorageArr

recv something from a recv channel

通過例子我們可以看出:

1) select執行開始時,首先所有case expression的表達式都會被求值一遍,按文法先後次序。

invoke takeARecvChannel

例外的是recv channel的位于指派等号左邊的表達式(這裡是:(getAStorageArr())[0])不會被求值。

2) 如果選擇要執行的case是一個recv channel,那麼它的指派等号左邊的表達式會被求值:如例子中當goroutine 3s後向recvchan寫入一個int值後,select選擇了recv channel執行,此時對=左側的表達式 (getAStorageArr())[0] 開始求值,輸出“invoke getAStorageArr”。

七、panic的recover過程

Go沒有提供“try-catch-finally”這樣的異常處理設施,而僅僅提供了panic和recover,其中recover還要結合

defer使用。最初這也是被一些人诟病的點。但和錯誤碼傳回值一樣,漸漸的大家似乎适應了這些,征讨之聲漸稀,即便有也是排在“缺少generics”

之後了。

【panicking】

在沒有recover的時候,一旦panic發生,panic會按既定順序結束目前程序,這一過程成為panicking。下面的例子模拟了這一過程:

//details-in-go/7/panicking.go

func foo() {

    defer func() {

        fmt.Println("foo defer func invoked")

    fmt.Println("foo invoked")

    bar()

    fmt.Println("do something after bar in foo")

func bar() {

        fmt.Println("bar defer func invoked")

    fmt.Println("bar invoked")

    zoo()

    fmt.Println("do something after zoo in bar")

func zoo() {

        fmt.Println("zoo defer func invoked")

    fmt.Println("zoo invoked")

    panic("runtime exception")

    foo()

$go run panicking.go

foo invoked

bar invoked

zoo invoked

zoo defer func invoked

bar defer func invoked

foo defer func invoked

panic: runtime exception

goroutine 1 [running]:

exit status 2

從結果可以看出:

    panic在zoo中發生,在zoo真正退出前,zoo中注冊的defer函數會被逐一執行(FILO),由于zoo defer中沒有捕捉panic,是以panic被抛向其caller:bar。

這時對于bar而言,其函數體中的zoo的調用就好像變成了panic調用似的,zoo有些類似于“黑客帝國3”中裡奧被史密斯(panic)感

染似的,也變成了史密斯(panic)。panic在bar中擴充開來,bar中的defer也沒有捕捉和recover

panic,是以在bar中的defer func執行完畢後,panic繼續抛給bar的caller: foo;

    這時對于foo而言,bar就變成了panic,同理,最終foo将panic抛給了main

    main與上述函數一樣,沒有recover,直接異常傳回,導緻程序異常退出。

【recover】

recover隻有在defer函數中調用才能起到recover的作用,這樣recover就和defer函數有了緊密聯系。我們在zoo的defer函數中捕捉并recover這個panic:

//details-in-go/7/recover.go

func zoo() {

        fmt.Println("zoo defer func1 invoked")

    defer func() {

        if x := recover(); x != nil {

            log.Printf("recover panic: %v in zoo recover defer func", x)

        fmt.Println("zoo defer func2 invoked")

    panic("zoo runtime exception")

這回的執行結果如下:

$go run recover.go

zoo defer func2 invoked

2015/09/17 16:28:00 recover panic: zoo runtime exception in zoo recover defer func

zoo defer func1 invoked

do something after zoo in bar

do something after bar in foo

由于zoo在defer裡恢複了panic,這樣在zoo傳回後,bar不會感覺到任何異常,将按正常邏輯輸出函數執行内容,比如:“do something after zoo in bar”,以此類推。

但若如果在zoo defer func中recover panic後,又raise another panic,那麼zoo對于bar來說就又會變成panic了。

Last、參考資料

1、The Go Programming Language Specification (Version of August 5, 2015,Go 1.5);

2、Effective Go (Go 1.5);

3、Rob Pike: Go Course Day 1~3。

本文實驗環境:Go 1.5 darwin_amd64。示例代碼在這裡可以下載下傳。

我就是這樣一種人:對任何自己感興趣且有極大熱情去做的事情都喜歡刨根問底,徹底全面地了解其中細節,否則我就會有一種“不安全 感”。我不知道在心理學範疇這樣的我屬于那種類别^_^。

© 2015, bigwhite. 版權所有.

Related posts:

  1. Go程式設計語言(二)
  2. Go中的系統Signal處理
  3. Go程式設計語言(三)
  4. Go語言标準庫概覽
  5. Golang的演化曆程

關于Go,你可能不注意的7件事

  • 九月 17, 2015
  • 9 條評論

Go源碼檔案預設采用Unicode字元集,Unicode碼點(code point)和記憶體中位元組序列(byte sequence)的變換實作使用了UTF-8:一種變長多位元組編碼,同時也是一種事實字元集編碼标準,為Linux、MacOSX

上的預設字元集編碼,是以使用Linux或MacOSX進行Go程式開發,你會省去很多字元集轉換方面的煩惱。但如果你是在Windows上使用

預設編輯器編輯Go源碼文本,當你編譯以下代碼時會遇到編譯錯誤:

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

微信公衆号:  共鳴圈

歡迎讨論,郵件:  924948$qq.com       請把$改成@

QQ群:263132197

QQ:    924948

良辰美景補天漏,風雨雷電洗地塵