《Go语言编程》第一章读书笔记

1.2语言特性

Go语言最主要的特性:

1、自动垃圾回收

2、更丰富的内置类型

3、函数多返回值

4、错误处理

5、匿名函数和包

6、类型和接口

7、并发编程

8、反射

9、语言交互性

1.2.1 自动垃圾回收

先看下不支持垃圾回收的语言的资源管理方式,以下为一小段C语言代码:

void foo()
{
  char* p = new char[128];
  ... // 对p指向的内存块进行赋值
  func1(p); // 使用内存指针
  delete[] p;
}
    各种非预期的原因,比如由于开发者的疏忽导致最后的 delete 语句没有被调用,都会引发
经典而恼人的内存泄露问题。假如该函数被调用得非常频繁,那么我们观察该进程执行时,会发
现该进程所占用的内存会一直疯长,直至占用所有系统内存并导致程序崩溃,而如果泄露的是系
统资源的话,那么后果还会更加严重,最终很有可能导致系统崩溃。
    手动管理内存的另外一个问题就是由于指针的到处传递而无法确定何时可以释放该指针所
指向的内存块。假如代码中某个位置释放了内存,而另一些地方还在使用指向这块内存的指针,
那么这些指针就变成了所谓的“野指针”(wild pointer)或者“悬空指针”(dangling pointer),对

这些指针进行的任何读写操作都会导致不可预料的后果。

    到目前为止,内存泄露的最佳解决方案是在语言级别引入自动垃圾回收算法(Garbage
Collection,简称GC)。所谓垃圾回收,即所有的内存分配动作都会被在运行时记录,同时任何对
该内存的使用也都会被记录,然后垃圾回收器会对所有已经分配的内存进行跟踪监测,一旦发现
有些内存已经不再被任何人使用,就阶段性地回收这些没人用的内存。当然,因为需要尽量最小
化垃圾回收的性能损耗,以及降低对正常程序执行过程的影响,现实中的垃圾回收算法要比这个
复杂得多,比如为对象增加年龄属性等,但基本原理都是如此。
自动垃圾回收在C/C++社区一直作为一柄双刃剑看待,虽然到C++0x(后命名为C++11)正
式发布时,这个呼声颇高的特性总算是被加入了,但按C++之父的说法,由于C++本身过于强大,
导致在C++中支持垃圾收集变成了一个困难的工作。假如C++支持垃圾收集,以下的代码片段在
运行时就会是一个严峻的考验:

int* p = new int;
p += 10; // 对指针进行了偏移,因此那块内存不再被引用
// …… 这里可能会发生针对这块int内存的垃圾收集 ……
p -= 10; // 咦,居然又偏移到原来的位置
*p = 10; // 如果有垃圾收集,这里就无法保证可以正常运行了
    Go语言作为一门新生的开发语言,当然不能忽略内存管理这个问题。又因为Go语言没有C++
这么“强大”的指针计算功能,因此可以很自然地包含垃圾回收功能。因为垃圾回收功能的支持,
开发者无需担心所指向的对象失效的问题,因此Go语言中不需要 delete 关键字,也不需要 free()
方法来明确释放内存。例如,对于以上的这个C语言例子,如果使用Go语言实现,我们就完全不
用考虑何时需要释放之前分配的内存的问题,系统会自动帮我们判断,并在合适的时候(比如CPU

相对空闲的时候)进行自动垃圾收集工作。

1.2.2 更丰富的内置类型

除了几乎所有语言都支持的简单内置类型(比如整型和浮点型等)外,Go语言也内置了一
些比较新的语言中内置的高级类型,比如C#和Java中的数组和字符串。除此之外,Go语言还内置
了一个对于其他静态类型语言通常用库方式支持的字典类型( map )。Go语言设计者对为什么内
置 map 这个问题的回答也颇为简单:既然绝大多数开发者都需要用到这个类型,为什么还非要每
个人都写一行 import 语句来包含一个库?这也是一个典型的实战派观点,与很多其他语言的学
院派气息迥然不同。
另外有一个新增的数据类型:数组切片( Slice )。我们可以认为数组切片是一种可动态增

长的数组。这几种数据结构基本上覆盖了绝大部分的应用场景。

1.2.3 函数多返回值

    目前的主流语言中除Python外基本都不支持函数的多返回值功能,不是没有这类需求,可能
是语言设计者没有想好该如何提供这个功能,或者认为这个功能会影响语言的美感。
    比如我们如果要定义一个函数用于返回个人名字信息,而名字信息因为包含多个部分——姓
氏、名字、中间名和别名,在不支持多返回值的语言中我们有以下两种做法:要么专门定义一个

结构体用于返回,或者以传出参数的方式返回多个结果。

    Go语言革命性地在静态开发语言阵营中率先提供了多返回值功能。这个特性让开发者可以
从原来用各种比较别扭的方式返回多个值的痛苦中解脱出来,既不用再区分参数列表中哪几个用

于输入,哪几个用于输出,也不用再只为了返回多个值而专门定义一个数据结构。

在Go语言中,上述的例子可以修改为以下的样子:

func getName()(firstName, middleName, lastName, nickName string){
    return "May", "M", "Chen", "Babe"
}
    因为返回值都已经有名字,因此各个返回值也可以用如下方式来在不同的位置进行赋值,从
而提供了极大的灵活性:

func getName()(firstName, middleName, lastName, nickName string){
    firstName = "May"
    middleName = "M"
    lastName = "Chen"
    nickName = "Babe"
    return
}
    并不是每一个返回值都必须赋值,没有被明确赋值的返回值将保持默认的空值。而函数的调

用相比C/C++语言要简化很多:

fn, mn, ln, nn := getName()

    如果开发者只对该函数其中的某几个返回值感兴趣的话,也可以直接用下划线作为占位符来
忽略其他不关心的返回值。下面的调用表示调用者只希望接收 lastName 的值,这样可以避免声

明完全没用的变量:

_, _, lastName, _ := getName()

1.2.4 错误处理

    整体上而言与C++和Java等语言中的异常捕获机制相比,Go语言的错误处理机制可以大量减少代码量,让开发者也无需仅仅为了程序安全性而添加大量一层套一层的 try-catch 语句。这对于代码的阅读者和维护者来说也是一件很好的事情,因为可

以避免在层层的代码嵌套中定位业务代码。

1.2.5 匿名函数和闭包

    在Go语言中,所有的函数也是值类型,可以作为参数传递。Go语言支持常规的匿名函数和
闭包,比如下列代码就定义了一个名为 f 的匿名函数,开发者可以随意对该匿名函数变量进行传
递和调用:
f := func(x, y int) int {
    return x + y
}

1.2.6 类型和接口

    Go语言的类型定义非常接近于C语言中的结构(struct),甚至直接沿用了 struct 关键字。相
比而言,Go语言并没有直接沿袭C++和Java的传统去设计一个超级复杂的类型系统,不支持继承
和重载,而只是支持了最基本的类型组合功能。

    Go语言也不是简单的对面向对象开发语言做减法,它还引入了一个无比强大的“非侵入式”
接口的概念,让开发者从以往对C++和Java开发中的接口管理问题中解脱出来。在C++中,我们
通常会这样来确定接口和类型的关系:

// 抽象接口
interface IFly
{
    virtual void Fly()=0;
};
// 实现类
class Bird : public IFly
{
public:
    Bird()
{}
virtual ~Bird()
{}
public:
    void Fly()
{
// 以鸟的方式飞行
}
};
void main()
{
    IFly* pFly = new Bird();
    pFly->Fly();
    delete pFly;
}
显然,在实现一个接口之前必须先定义该接口,并且将类型和接口紧密绑定,即接口的修改

会影响到所有实现了该接口的类型,而Go语言的接口体系则避免了这类问题:

type Bird struct {
    ...
}
func (b *Bird) Fly() {
    // 以鸟的方式飞行
}
我们在实现 Bird 类型时完全没有任何 IFly 的信息。我们可以在另外一个地方定义这个 IFly

接口:

type IFly interface {
    Fly()
}

这两者目前看起来完全没有关系,现在看看我们如何使用它们:

func main() {
    var fly IFly = new(Bird)
    fly.Fly()
}
    可以看出,虽然 Bird 类型实现的时候,没有声明与接口 IFly 的关系,但接口和类型可以直
接转换,甚至接口的定义都不用在类型定义之前,这种比较松散的对应关系可以大幅降低因为接

口调整而导致的大量代码调整工作。

1.2.7 并发编程

    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-1所示。

代码清单1-1 paracalc.go

package main
import "fmt"
func sum(values [] int, resultChan chan int) {
    sum := 0
    for _, value := range values {
    sum += value
    }
    resultChan <- sum // 将计算结果发送到channel中
}
func main() {
    values := [] int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    resultChan := make(chan int, 2)
    go sum(values[:len(values)/2], resultChan)
    go sum(values[len(values)/2:], resultChan)
    sum1, sum2 := <-resultChan, <-resultChan // 接收结果
    fmt.Println("Result:", sum1, sum2, sum1 + sum2)
}

1.2.8 反射

    反射最常见的使用场景是做对象的序列化(serialization,有时候也叫Marshal & Unmarshal)。
例如,Go语言标准库的encoding/json、encoding/xml、encoding/gob、encoding/binary等包就大量

依赖于反射功能来实现。

    这里先举一个小例子,可以利用反射功能列出某个类型中所有成员变量的值,如代码清单1-2所示。

代码清单1-2 reflect.go

package main
import (
"fmt"
"reflect"
)
type Bird struct {
Name string
LifeExpectance int
}
func (b *Bird) Fly() {
fmt.Println("I am flying...")
}
func main() {
sparrow := &Bird{"Sparrow", 3}
s := reflect.ValueOf(sparrow).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(),
f.Interface())
}
}

该程序的输出结果为:

0: Name string = Sparrow
1: LifeExpectance int = 3

1.3 第一个 Go 程序

hello.go

package main
import "fmt"// 我们需要使用fmt包中的Println()函数
func main() {
    fmt.Println("Hello, world. 你好,世界!")
}
    有一点需要注意,不得包含在源代码文件中没有用到的包,否则Go编译器会报编译错误。
这与下面提到的强制左花括号 { 的放置位置以及之后会提到的函数名的大小写规则,均体现了Go

语言在语言层面解决软件工程问题的设计哲学。

    所有Go函数(包括在对象编程中会提到的类型成员函数)以关键字 func 开头。一个常规的函数定义包含以下部分:

func 函数名(参数列表)(返回值列表) {
// 函数体
}

对应的一个实例如下:

func Compute(value1 int, value2 float64)(result float64, err error) {
    // 函数体
}
    Go支持多个返回值。以上的示例函数 Compute() 返回了两个值,一个叫 result ,另一个是
err 。并不是所有返回值都必须赋值。在函数返回时没有被明确赋值的返回值都会被设置为默认

值,比如 result 会被设为0.0, err 会被设为 nil 。

    此外,Go程序并不要求开发者在每个语句后面加上分号表示语句结束,这是与C和C++的一个明显不同

之处。

    另外,Go语言中不能将左花括号{另起一行放置,否则编译器会报错。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值