自动垃圾回收
内存泄露的最佳解决方案是在语言级别引入自动垃圾回收算法(Garbage Collection,简称GC)。所谓垃圾回收,即所有的内存分配动作都会被在运行时记录,同时任何对该内存的使用也都会被记录,然后垃圾回收器会对所有已经分配的内存进行跟踪监测,一旦发现有些内存已经不再被任何人使用,就阶段性地回收这些没人用的内存。当然,因为需要尽量最小化垃圾回收的性能损耗,以及降低对正常程序执行过程的影响,现实中的垃圾回收算法要比这个复杂得多,比如为对象增加年龄属性等,但基本原理都是如此。
Go语言作为一门新生的开发语言,当然不能忽略内存管理这个问题。又因为Go语言没有C++这么“强大”的指针计算功能,因此可以很自然地包含垃圾回收功能。因为垃圾回收功能的支持,开发者无需担心所指向的对象失效的问题,因此Go语言中不需要delete关键字,也不需要free()方法来明确释放内存。例如,对于以上的这个C语言例子,如果使用Go语言实现,我们就完全不用考虑何时需要释放之前分配的内存的问题,系统会自动帮我们判断,并在合适的时候(比如CPU相对空闲的时候)进行自动垃圾收集工作。
更丰富的内置类型
1. 简单内置类型(比如整型 浮点型等)
2. 高级类型(数组 字符串)
3. map类型
4. 新增的数据类型:数组切片(Slice)我们可以认为数组切片是一种可动态增长的数组。这几种数据结构基本上覆盖了绝大部分的应用场景。数组切片的功能与C++标准库中的vector非常类似。
因为是语言内置特性,我们并不用费事去添加依赖的包。
函数多返回值
目前的主流语言中除Python外基本上都不支持函数的多返回值。
Go语言革命性地在静态开发语言阵营中率先提供了多返回值功能。这个特性让开发者可以从原来用各种比较别扭的方式返回多个值的痛苦中解脱出来,既不用再区分参数列表中哪几个用于输入,哪几个用于输出,也不用再只为了返回多个值而专门定义一个数据结构。
例如我要获取某个人的姓名,而名字信息因为包含多个部分--姓氏、名字、中间名和别名。
1
2
3
4 1func getName()(firstName, middleName, lastName, nickName string) {
2 return "May", "M", "Chen", "Babe"
3}
4
因为返回值都已经有名字,因此各个返回值也可以用如下方式来在不同位置进行赋值,从而提供了极大的灵活性:
1
2
3
4
5
6
7
8 1func getName()(firstName, middleName, lastName, nickName string) {
2 firstName = "May"
3 middleName = "M"
4 lastName = "Chen"
5 nickName = "Babe"
6 return
7}
8
1
2 1
2
并不是每一个返回值都必须赋值,没有被明确赋值的返回值将保持默认的空值。而函数的调用更简化
1
2 1fn, mn, ln, nn := getName()
2
如果开发者只对函数其中的某几个返回值感兴趣的话,也可以直接用下划线作为占位符来忽略其他不关心的返回值。
1
2 1_, _, lastName, _ := getName()
2
后续会讲更多关于多重返回值的用法。
错误处理
Go语言引入了3个关键字用于标准的错误处理流程,这3个关键字分别为
defer、
panic和
recover。整体上而言与C++和Java等语言中的异常捕获机制相比Go语言的错误处理机制可以大量减少代码量,让开发者也无需仅仅为了程序安全性而添加大量一层套一层的try-catch语句。这对于代码的阅读者和维护者来说也是一件很好的事情,因为可以避免在层层的代码嵌套中定位业务代码。后续文章会更详尽的介绍这三个关键字。
匿名函数和闭包
在Go语言中,所有的函数也是值类型,可以作为参数传递。Go语言支持常规的匿名函数和闭包,比如下列代码就定义了一个名为f的匿名函数,开发者可以随意对该匿名函数变量进行传递和调用:
1
2
3
4 1f := func(x, y int) int {
2 return x + y
3}
4
类型和接口
Go语言的类型定义非常接近于C语言中的结构(struct),甚至直接沿用了struct关键字。相比而言,Go语言并没有直接沿袭C++和Java的传统去设计一个超级复杂的类型系统,不支持继承和重载,而只是支持了最基本的类型组合功能。
巧妙的是,虽然看起来支持的功能过于简洁,细用起来你却会发现,C++和Java使用那些复杂的类型系统实现的功能在Go语言中并不会出现无法表现的情况,这反而让人反思其他语言中引入这些复杂概念的必要性。我会后续详细描述Go语言的类型系统。
Go语言也不是简单的对面向对象开发语言做减法,它还引入了一个无比强大的“非侵入式”接口的概念,让开发者从以往对C++和Java开发中的接口管理问题中解脱出来。在C++中,我们通常会这样来确定接口和类型的关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 1// 抽象接口
2interface IFly
3{
4virtual void Fly()=0;
5};
6// 实现类
7class Bird : public IFly
8{
9public:
10Bird()
11{}
12virtual ~Bird()
13{}
14public:
15void Fly()
16{
17// 以鸟的方式飞行
18}
19};
20void main()
21{
22IFly* pFly = new Bird();
23pFly->Fly();
24delete pFly;
25}
26
1
2 1 显然,在实现一个接口之前必须先定义该接口,并且将类型和接口紧密绑定,即接口的修改会影响所有实现了该接口的类型,而Go语言的接口体系则避免了这类问题:
2
1
2
3
4
5
6
7 1type Bird struct {
2...
3}
4func (b *Bird) Fly() {
5// 以鸟的方式飞行
6}
7
1
2 1我们在实现Bird类型时完全没有任何IFly的信息。我们可以在另外一个地方定义这个IFly接口:
2
1
2
3
4 1type IFly interface {
2 Fly()
3}
4
1
2 1这两者目前看起来完全没有关系,现在看看我们如何使用它们:
2
1
2
3
4
5 1func main() {
2 var fly IFly = new(Bird)
3 fly.Fly()
4}
5
1
2 1可以看出,虽然Bird类型实现的时候,没有声明与接口IFly的关系,但接口和类型可以直接转换,甚至接口的定义都不用在类型定义之前,这种比较松散的对应关系可以大幅降低因为接口调整(
2
接口名称修改等,不包括修改接口方法名称或者增删接口方法)而导致的大量代码调整工作。
事例完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 1// TestTypeAndInterface
2package main
3
4import (
5 "fmt"
6)
7
8type Bird struct {
9}
10
11func (b *Bird) Fly() {
12 // 以鸟的方式飞行
13 fmt.Println("鸟飞行")
14}
15
16func (b *Bird) Run() {
17 // 以鸟的方式跑
18 fmt.Println("鸟跑")
19}
20
21type IInterface interface {
22 Fly()
23
24 Run()
25}
26
27func main() {
28 var fly IInterface = new(Bird)
29 fly.Fly()
30 fly.Run()
31}
32
并发编程
Go语言引入了
goroutine概念,它使得并发编程变得非常简单。通过使用goroutine而不是裸用操作系统的并发机制,以及使用消息传递来共享内存而不是使用共享内存来通信,Go语言让并发编程变得更加轻盈和安全。
通过在函数调用前使用
关键字go,我们即可让该函数以goroutine方式执行。goroutine是一种比线程更加轻盈、更省资源的协程。Go语言通过系统的线程来多路派遣这些函数的执行,使得每个用go关键字执行的函数可以运行成为一个单位协程。当一个协程阻塞的时候,调度器就会自动把其他协程安排到另外的线程中去执行,从而实现了程序无等待并行化运行。而且调度的开销非常小,一颗CPU调度的规模不下于每秒百万次,这使得我们能够创建大量的goroutine,从而可以很轻松地编写高并发程序,达到我们想要的目的。
Go语言实现了CSP(通信顺序进程,Communicating Sequential Process)模型来作为goroutine间的推荐通信方式。在CSP模型中,一个并发系统由若干并行运行的顺序进程组成,每个进程不能对其他进程的变量赋值。进程之间只能通过一对通信原语实现协作。Go语言用channel(通道)这个概念来轻巧地实现了CSP模型。channel的使用方式比较接近Unix系统中的管道(pipe)概念,可以方便地进行跨goroutine的通信。
另外,由于一个进程内创建的所有goroutine运行在同一个内存地址空间中,因此如果不同的goroutine不得不去访问共享的内存变量,访问前应该先获取相应的读写锁。Go语言标准库中的sync包提供了完备的读写锁功能。
下面我们用一个简单的例子来演示goroutine和channel的使用方式。这是一个并行计算的例子,由两个goroutine进行并行的累加计算,待这两个计算过程都完成后打印计算结果,具体如代码如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 1// paracalc
2package main
3
4import (
5 "fmt"
6)
7
8func sum(values []int, resultChan chan int) {
9 sum := 0
10 for _, value := range values {
11 sum += value
12 }
13
14 // 将计算结果发送到 channel 中
15 resultChan <- sum
16}
17
18func main() {
19 values := []int{1, 2, 3, 4, 5, 7, 8, 9, 10}
20
21 resultChan := make(chan int, 2)
22 go sum(values[:len(values)/2], resultChan)
23 go sum(values[len(values)/2:], resultChan)
24 sum1, sum2 := <-resultChan, <-resultChan // 接收结果
25 fmt.Println("Result:", sum1, sum2, sum1+sum2)
26}
27
反射
反射(reflection)是在Java语言出现后迅速流行起来的一种概念。通过反射,你可以获取对象类型的详细信息,并可动态操作对象。反射是把双刃剑,功能强大但代码可读性并不理想。若非必要,并不推荐使用反射。
Go语言的反射实现了反射的大部分功能,但没有像Java语言那样内置类型工厂,故而无法做到像Java那样通过类型字符串创建对象实例。在Java中,你可以读取配置并根据类型名称创建对应的类型,这是一种常见的编程手法,但在Go语言中这并不被推荐。
反射最常见的使用场景是做对象的序列化(serialization,有时候也叫Marshal & Unmarshal)。例如,Go语言标准库的encoding/json、encoding/xml、encoding/gob、encoding/binary等包就大量依赖于反射功能来实现。
这里先举一个小例子,可以利用反射功能列出某个类型中所有成员变量的值,代码如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 1// reflect
2package main
3
4import (
5 "fmt"
6 "reflect"
7)
8
9type Bird struct {
10 Name string
11 LifeExperience int
12}
13
14func (b *Bird) Fly() {
15 fmt.Println("I am flying……")
16}
17
18func main() {
19 sparrow := &Bird{"Sparrow", 3}
20 s := reflect.ValueOf(sparrow).Elem()
21 typeOfT := s.Type()
22 for i := 0; i < s.NumField(); i++ {
23 f := s.Field(i)
24 fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(),
25 f.Interface())
26 }
27}
28
语言交互性
由于Go语言与C语言之间的天生联系,Go语言的设计者们自然不会忽略如何重用现有C模块的这个问题,这个功能直接被命名为Cgo。Cgo既是语言特性,同时也是一个工具的名称。
在Go代码中,可以按Cgo的特定语法混合编写C语言代码,然后Cgo工具可以将这些混合的C代码提取并生成对于C功能的调用包装代码。开发者基本上可以完全忽略这个Go语言和C语言的边界是如何跨越的。
与Java中的JNI不同,Cgo的用法非常简单,比如下列代码就可以实现在Go中调用C语言标准库的puts函数。
1
2
3
4
5
6
7
8
9
10
11
12 1// cprint
2package main
3
4import "C"
5import "unsafe"
6
7func main() {
8 cstr := C.CString("Hello, world!")
9 C.puts(cstr)
10 C.free(unsafe.Pointer(cstr))
11}
12