标签 MacOSX 下的文章

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

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
}

func main() {
    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
package main

import (
    "fmt"
    "reflect"
)

type I interface {
    Method1()
    Method2()
}

func main() {
    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
package main

import "./utils"

type T struct {
}

func (t T) Method1() {
}

func (t *T) Method2() {
}

func (t *T) Method3() {
}

func main() {
    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:
     Method1
     Method2
     Method3

可以看出类型T的Method set仅包含一个receiver类型为T的方法:Method1,而*T的Method Set则包含了T的Method Set以及所有receiver类型为*T的Method。

如果此时我们有一个interface type如下:

type I interface {
    Method1()
    Method2()
}

那下面哪个赋值语句合法呢?合不合法完全依赖于右值类型是否实现了interface type I的所有方法,即右值类型的Method Set是否包含了I的 所有方法。

var t T
var pt *T

var i I = t

or

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
package main

import "./utils"

type I1 interface {
    I1Method1()
    I1Method2()
}
type I2 interface {
    I2Method()
}

type I3 interface {
    I1
    I2
}

func main() {
    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:
     I1Method1
     I1Method2
     I2Method

可以看出嵌入interface type的interface type I3的Method Set包含了被嵌入的interface type:I1I2的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:

type T struct {
    I1
}

func (T) Method1() {

}

… …
func main() {
    … …
    var t T
    utils.DumpMethodSet(&t)
    var pt = &T{
        I1: I1Impl{},
    }
    utils.DumpMethodSet(&pt)

}

输出结果与预期一致:

main.T's method sets:
     I1Method1
     I1Method2
     Method1

*main.T's method sets:
     I1Method1
     I1Method2
     Method1

【struct embeding struct】

在struct中embeding struct提供了一种“继承”的手段,外部的Struct可以“继承”嵌入struct的所有方法(无论receiver是T还是*T类型)实现,但 Method Set可能会略有不同。看下面例子:

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

import "./utils"

type T struct {
}

func (T) InstMethod1OfT() {

}

func (T) InstMethod2OfT() {

}

func (*T) PtrMethodOfT() {

}

type S struct {
}

func (S) InstMethodOfS() {

}

func (*S) PtrMethodOfS() {
}

type C struct {
    T
    *S
}

func main() {
    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:
     InstMethod1OfT
     InstMethod2OfT
     InstMethodOfS
     PtrMethodOfS
     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
package main

import "./utils"

type I interface {
    IMethod1()
    IMethod2()
}

type T struct {
}

func (T) InstMethod() {

}
func (*T) PtrMethod() {

}

type MyInterface I
type MyStruct T

func main() {
    utils.DumpMethodSet((*I)(nil))

    var t T
    utils.DumpMethodSet(&t)
    var pt = &T{}
    utils.DumpMethodSet(&pt)

    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

main.T's method sets:
     InstMethod

*main.T's method sets:
     InstMethod
     PtrMethod

main.MyInterface's method sets:
     IMethod1
     IMethod2

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:

type T struct {
    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的一般使用方式如下:

var t T
t.Get()
t.Set(1)

不过我们也可以像普通function那样使用它,根据上面的Method Type定义:

var t T
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
4 5
4 5
4 5
4 5

各个goroutine中输出的i,v值都是for range循环结束后的i, v最终值,而不是各个goroutine启动时的i, v值。一个可行的fix方法:

    for i, v := range m {
        go func(i, v int) {
            time.Sleep(time.Second * 3)
            fmt.Println(i, v)
        }(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]

但实际输出结果却是:

a =  [1 2 3 4 5]
r =  [1 2 3 4 5]
a =  [1 12 13 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
        if i == 0 {
            a[1] = 12
            a[2] = 13
        }
        r[i] = v
    }

Go中的数组在内部表示为连续的字节序列,虽然长度是Go数组类型的一部分,但长度并不包含的数组的内部表示中,而是由编译器在编译期计算出 来。这个例子中,对range表达式的拷贝,即对一个数组的拷贝,a'则是Go临时分配的连续字节序列,与a完全不是一块内存。因此无论a被 如何修改,其副本a'依旧保持原值,并且参与循环的是a',因此v从a'中取出的仍旧是a的原值,而非修改后的值。

我们再来试试pointer to array:

func pointerToArrayRangeExpression() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int

    fmt.Println("pointerToArrayRangeExpression result:")
    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)
    fmt.Println("a = ", a)
    fmt.Println("")
}

这回的输出结果如下:

pointerToArrayRangeExpression result:
a =  [1 2 3 4 5]
r =  [1 12 13 4 5]
a =  [1 12 13 4 5]

我们看到这次r数组的值与最终a被修改后的值一致了。这个例子中我们使用了*[5]int作为range表达式,其副本依旧是一个指向原数组 a的指针,因此后续所有循环中均是&a指向的原数组亲自参与的,因此v能从&a指向的原数组中取出a修改后的值。

idiomatic go建议我们尽可能的用slice替换掉array的使用,这里用slice能否实现预期的目标呢?我们来试试:

func sliceRangeExpression() {
    var a = [5]int{1, 2, 3, 4, 5}
    var r [5]int

    fmt.Println("sliceRangeExpression result:")
    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)
    fmt.Println("a = ", a)
    fmt.Println("")
}

pointerToArrayRangeExpression result:
a =  [1 2 3 4 5]
r =  [1 12 13 4 5]
a =  [1 12 13 4 5]

显然用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:")
    fmt.Println("a = ", a)

    for i, v := range a {
        if i == 0 {
            a = append(a, 6, 7)
        }

        r = append(r, v)
    }

    fmt.Println("r = ", r)
    fmt.Println("a = ", a)
}

输出结果:

a =  [1 2 3 4 5]
r =  [1 2 3 4 5]
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
jim 23

或其他可能。

如果map中的某项在循环到达前被在循环体中删除了,那么它将不会被iteration variable获取到。
    counter := 0
    for k, v := range m {
        if counter == 0 {
            delete(m, "tony")
        }
        counter++
        fmt.Println(k, v)
    }
    fmt.Println("counter is ", counter)

反复运行多次,我们得到的两个结果:

tony 21
tom 22
jim 23
counter is  3

tom 22
jim 23
counter is  2

如果在循环体中新创建一个map元素项,那该项元素可能出现在后续循环中,也可能不出现:

    m["tony"] = 21
    counter = 0

    for k, v := range m {
        if counter == 0 {
            m["lucy"] = 24
        }
        counter++
        fmt.Println(k, v)
    }
    fmt.Println("counter is ", counter)

执行结果:

tony 21
tom 22
jim 23
lucy 24
counter is  4

or

tony 21
tom 22
jim 23
counter is  3

以上代码可以在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
func main() {
    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)

    go func() {
        time.Sleep(3 * time.Second)
        c <- 1
    }()

    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
}

func main() {
    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
invoke takeASendChannel
invoke getANumToChannel

例外的是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() {
    defer func() {
        fmt.Println("bar defer func invoked")
    }()
    fmt.Println("bar invoked")

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

func zoo() {
    defer func() {
        fmt.Println("zoo defer func invoked")
    }()

    fmt.Println("zoo invoked")
    panic("runtime exception")
}

func main() {
    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() {
    defer func() {
        fmt.Println("zoo defer func1 invoked")
    }()

    defer func() {
        if x := recover(); x != nil {
            log.Printf("recover panic: %v in zoo recover defer func", x)
        }
    }()

    defer func() {
        fmt.Println("zoo defer func2 invoked")
    }()

    fmt.Println("zoo invoked")
    panic("zoo runtime exception")
}

… …

这回的执行结果如下:

$go run recover.go
foo invoked
bar invoked
zoo invoked
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
bar defer func invoked
do something after bar in foo
foo defer func invoked

由于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。示例代码在这里可以下载。

我就是这样一种人:对任何自己感兴趣且有极大热情去做的事情都喜欢刨根问底,彻底全面地了解其中细节,否则我就会有一种“不安全 感”。我不知道在心理学范畴这样的我属于那种类别^_^。

Golang开发环境搭建-Vim篇

虽说sublimetext3+gosublime+gocode是目前较为 流行的Golang开发环境组合,但作为一名VIMer,没有一套得心应手的Vim for Golang dev心里总是过不去的。Golang虽然年轻,但即便是从Go 1版本发布(2012年3月28日)算起,掐指算来也有小三年了。全世界的开发者已经为Golang贡献了较为成熟的Vim插件了。有了这些插件,搭建出 一套高效的Golang开发环境还是不难的,网上也有大量的资料可以参考,其中就有vim-go作者自己发表的一篇文章《Go development environment for Vim》。不过看别人 写的与自己搭建体验的还是有大不同的,于是想来想去还是把整个过程记录下来。

一、一个干净的环境

找个干净的基础环境,方便确认每个搭建步骤后的效果:

Ubuntu 14.04 x86_64
vim version 7.4.52
go version go1.4beta1 linux/amd64

再准备一个编辑Go源码的测试源文件:

//hellogolang.go

package main

import "fmt"

func main() {
        fmt.Println("Hello Golang!")
}

用于验证每个搭建步骤后的变化。

二、严格按照vim-go的官方说明逐一搭建

Vim-go是当前使用最为广泛的用于搭建Golang开发环境的vim插件,这里我同样使用vim-go作为核心和基础进行环境搭建的。vim-go利 用开源Vim插件管理器安装,gmarik/Vundle.vim是目前被推荐次数更多的Vim插件管理器,超过了pathogen。这里我们 就用vundle来作为Vim的插件管理工具。

1、安装Vundle.vim

Vundle.vim的安装步骤如下:

mkdir ~/.vim/bundle
git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim   
                                                                  

创建~/.vimrc文件(如果你没有这个文件的话),在文件顶部添加有关Vundle.vim的配置:

set nocompatible              " be iMproved, required
filetype off                  " required

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'gmarik/Vundle.vim'

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

此时Vim仅安装了Vundle.vim这一个插件。编辑hellogolang.go时与编辑普通文本文件无异,一切都还是Vim的默认属性。

2、安装Vim-go

编辑~/.vimrc,在vundle#beginvundle#end间增加一行:

Plugin 'fatih/vim-go'

在Vim内执行 :P luginInstall

Vundle.vim会在左侧打开一个Vundle Installer Preview子窗口,窗口下方会提示:“Processing 'fatih/vim-go'”,待安装完毕后,提示信息变 成“Done!”。

这时,我们可以看到.vim/bundle下多了一个vim-go文件夹:

$ ls .vim/bundle/
vim-go/  Vundle.vim/

此时,再次编辑hellogolang.go,语法高亮有了, 保存时自动format(利用$GOBIN/gofmt)也有了,但其他高级功能,比如自动import缺失的 package、自动补齐仍然没有,我们还要继续安装一些东东。

3、安装go.tools Binaries

vim-go安装说明中提到所有必要的binary需要先安装好,比如gocode、godef、goimports等。

通过:GoInstallBinaries,这些vim-go依赖的二进制工具将会自动被下载,并被安装到$GOBIN下或$GOPATH/bin下。(这个工具需要依赖git或hg,需要提前安装到你的OS中。)

:GoInstallBinaries的执行是交互式的,你需要回车确认:

vim-go: gocode not found. Installing github.com/nsf/gocode to folder /home/tonybai/go/bin
vim-go: goimports not found. Installing code.google.com/p/go.tools/cmd/goimports to folder /home/tonybai/go/bin/
vim-go: godef not found. Installing code.google.com/p/rog-go/exp/cmd/godef to folder /home/tonybai/go/bin/
vim-go: oracle not found. Installing code.google.com/p/go.tools/cmd/oracle to folder /home/tonybai/go/bin/
vim-go: gorename not found. Installing code.google.com/p/go.tools/cmd/gorename to folder /home/tonybai/go/bin/
vim-go: golint not found. Installing github.com/golang/lint/golint to folder /home/tonybai/go/bin/

vim-go: errcheck not found. Installing github.com/kisielk/errcheck to folder /home/tonybai/go/bin/

不过这些代码多在code.google.com上托管,因此由于众所周知的原因,vim-go的自动安装很可能以失败告终,这样就需要你根据上 面日志中提到的各个工具的源码地址逐一去下载并本地安装。无法搭梯子的,可以通过http://gopm.io 下载相关包。

安装后,$GOBIN下的新增Binaries如下:
-rwxr-xr-x  1 tonybai tonybai  5735552 11??  7 11:03 errcheck*
-rwxr-xr-x  1 tonybai tonybai  9951008 11??  7 10:33 gocode*
-rwxr-xr-x  1 tonybai tonybai  5742800 11??  7 11:07 godef*
-rwxr-xr-x  1 tonybai tonybai  4994120 11??  7 11:00 goimports*
-rwxr-xr-x  1 tonybai tonybai  5750152 11??  7 11:03 golint*
-rwxr-xr-x  1 tonybai tonybai  6381832 11??  7 11:01 gorename*
-rwxr-xr-x  1 tonybai tonybai  2954392 11??  7 10:38 gotags*
-rwxr-xr-x  1 tonybai tonybai  9222856 11??  7 11:01 oracle*

安装好这些Binaries后,我们来看看哪些特性被支持了。

再次编辑hellogolang.go

         - 新起一行输入fmt.,然后ctrl+x, ctrl+o,Vim 会弹出补齐提示下拉框,不过并非实时跟随的那种补齐,这个补齐是由gocode提供的。
    – 输入一行代码:time.Sleep(time.Second),执行:GoImports,Vim会自动导入time包。
    – 将光标移到Sleep函数上,执行:GoDef或命令模式下敲入gd,Vim会打开$GOROOT/src/time/sleep.go中 的Sleep函数的定义。执行:b 1返回到hellogolang.go。
    – 执行:GoLint,运行golint在当前Go源文件上。
    – 执行:GoDoc,打开当前光标对应符号的Go文档。
    – 执行:GoVet,在当前目录下运行go vet在当前Go源文件上。
    – 执行:GoRun,编译运行当前main package。
    – 执行:GoBuild,编译当前包,这取决于你的源文件,GoBuild不产生结果文件。
    – 执行:GoInstall,安装当前包。
    – 执行:GoTest,测试你当前路径下地_test.go文件。
    – 执行:GoCoverage,创建一个测试覆盖结果文件,并打开浏览器展示当前包的情况。
    – 执行:GoErrCheck,检查当前包种可能的未捕获的errors。
    – 执行:GoFiles,显示当前包对应的源文件列表。
    – 执行:GoDeps,显示当前包的依赖包列表。
    – 执行:GoImplements,显示当前类型实现的interface列表。
    – 执行:GoRename [to],将当前光标下的符号替换为[to]。

三、其他插件

到目前为止,我们还有若干特性没能实现,重点是:

    – 实时跟随的代码补齐
    – Code Snippet support

1、安装YCM(Your Complete Me)

在~/.vimrc中添加一行:

Plugin 'Valloric/YouCompleteMe'

保存退出后,再打开~/.vimrc并执行 :P luginInstall

安装完后,下面的提示栏提示:

ycm_client_support.[so|pyd|dll] and ycm_core.[so|pyd|dll] not detected; you need to compile YCM before using it. Read the docs!

似乎YCM是用了C++编写的模块对性能进行优化了,于是需要手工编译YCM的support库。步骤如下:

sudo apt-get install build-essential cmake python-dev
cd ~/.vim/bundle/YouCompleteMe
./install.sh

构建(编译C++很慢,需要耐心的等一会)ok后,再打开hellogolang.go,逐字的实时补全功能就具备了!Cool!

2、安装 UltiSnips

Vim-go默认是用ultisnips引擎插件,但这个插件需要单独安装。

同样,我们利用vundle来安装它,在~/.vimrc中添加一行:

Plugin 'SirVer/ultisnips'

snippet和snippet引擎是分开的。ultisnips是引擎,vim-go的go snippet定义在这里

https://github.com/fatih/vim-go/blob/master/gosnippets/snippets/go.snip

编辑hellogolang.go,按照go.snip中的说明,我们输入func后敲击tab键,我们发现期待的:

func name(params) type {
       
}

并没有出现。反倒是YCM的下拉提示显示在那里让你选择。似乎是ultisnips和YCM的键组合冲突了。ultisnips官方说明也的确如 此。ultisnips默认是用Tab展开snippet的,而YCM中的Tab用来选择补齐项,我们可以通过设置来避免这些。

我们在.vimrc中添加如下setting:

" YCM settings
let g:ycm_key_list_select_completion = ['', '']
let g:ycm_key_list_previous_completion = ['']
let g:ycm_key_invoke_completion = '<C-Space>'

" UltiSnips setting
let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<c-b>"
let g:UltiSnipsJumpBackwardTrigger="<c-z>"

这样让YCM通过回车和向下的箭头来做list item正向选择,通过向上箭头做反向选择。通过ctrl+space来原地触发补齐提示。

而ultisnips则是用tab做snippet展开,ctrl+b正向切换占位符,ctrl+z反向切换占位符。

3、安装molokai theme

Molokai theme是TextMate的theme的vim port,看着截图挺不错的,于是也安装了一下。

    mkdir ~/.vim/colors
    下载或copy https://github.com /fatih/molokai/blob/master/colors/molokai.vim到~/.vim /colors目录下
    在.vimrc添加一行:colorscheme molokai

四、.vimrc

前面讲到了vim-go有许多命令,在:xx模式下执行多显不便,于是你可以定义一些Mappings,比如:

" set mapleader
let mapleader = ","

" vim-go custom mappings
au FileType go nmap <Leader>s <Plug>(go-implements)
au FileType go nmap <Leader>i <Plug>(go-info)
au FileType go nmap <Leader>gd <Plug>(go-doc)
au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
au FileType go nmap <leader>r <Plug>(go-run)
au FileType go nmap <leader>b <Plug>(go-build)
au FileType go nmap <leader>t <Plug>(go-test)
au FileType go nmap <leader>c <Plug>(go-coverage)
au FileType go nmap <Leader>ds <Plug>(go-def-split)
au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
au FileType go nmap <Leader>dt <Plug>(go-def-tab)
au FileType go nmap <Leader>e <Plug>(go-rename)

这样我们在命令模式下,输入<,>+<r>就是运行 当前main包,以此类推。

另外下面这个配置使得我们在save file时既可以格式化代码,又可以自动插入包导入语句(或删除不用的包导入语句)。

" vim-go settings
let g:go_fmt_command = "goimports"

到这里,我们的Vim Golang开发环境就基本搭建好了。snippet+实时补齐让你Coding如飞!

五、附录:.vimrc文件

下面是截至目前为止全量.vimrc文件的内容:

set nocompatible              " be iMproved, required
filetype off                  " required
colorscheme molokai

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'gmarik/Vundle.vim'
Plugin 'fatih/vim-go'
Plugin 'Valloric/YouCompleteMe'

Plugin 'SirVer/ultisnips'

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

" set mapleader
let mapleader = ","

" vim-go custom mappings
au FileType go nmap <Leader>s <Plug>(go-implements)
au FileType go nmap <Leader>i <Plug>(go-info)
au FileType go nmap <Leader>gd <Plug>(go-doc)
au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
au FileType go nmap <leader>r <Plug>(go-run)
au FileType go nmap <leader>b <Plug>(go-build)
au FileType go nmap <leader>t <Plug>(go-test)
au FileType go nmap <leader>c <Plug>(go-coverage)
au FileType go nmap <Leader>ds <Plug>(go-def-split)
au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
au FileType go nmap <Leader>dt <Plug>(go-def-tab)
au FileType go nmap <Leader>e <Plug>(go-rename)

" vim-go settings
let g:go_fmt_command = "goimports"

" YCM settings
let g:ycm_key_list_select_completion = ['', '']
let g:ycm_key_list_previous_completion = ['', '']
let g:ycm_key_invoke_completion = '<C-Space>'

" UltiSnips settings
let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<c-b>"
let g:UltiSnipsJumpBackwardTrigger="<c-z>"

六、Mac OS X下Vim配置

1、MacVim替换

Mac OS X下的配置方法稍有不同,因为Mac下系统自带的Vim是7.3版本,YCM要求Vim 7.3.584+版本,因此我们需要安装MacVim以替代自带的Vim,目前MacVim最新版本是version 7.4.258,完全满足要求。在这里https://github.com/b4winckler/macvim/releases可以下载到最新的MacVim,下载后的MacVim可以通过如下步骤替换原Vim。

原Vim安装到/usr/bin/vim下。

MacVim解压后如下:

[tony@tonydeair ~/Downloads/MacVim-snapshot-73]$ls
MacVim.app/    README.txt    mvim*

我们执行以下步骤即可完成vim替换工作:

sudo mv /usr/bin/vim /usr/bin/vim.bak //备份一下原vim
cp mvim /usr/local/bin/
sudo ln -s /usr/local/bin/mvim /usr/bin/vim

2、插件安装和配置

按照上面Linux Vim的插件安装步骤和配置方法我们来配置MacVim,配置后,我们发现除了molokai的colorscheme没有生效外,其余插件工作均正常。而所有.go文件打开,均无molokai方案的颜色高亮,甚至连一般的颜色高亮都没有了。经过不断调试,发现了一个解决方法,在~/.vimrc中添加几行代码即可:

syntax on
au BufRead,BufNewFile *.go set filetype=go
colorscheme molokai

但这几行配置代码如果放在~/.vimrc的前面,则UltiSnips会无法工作,我将其移到~/.vimrc文件的末尾,这样就不存在冲突了(看来.vimrc的插件配置的先后顺序会对插件功能的正常使用有影响)。漂亮的molokai colorscheme也会展现出来!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats