Go并发编程实战[试读]
1.1 Go语言特性一瞥
在真正地讲解Go语言之前,先让我们对它有个宏观的认识。本章将介绍Go语言是什么、有哪些特性、有哪些优势和不足,以及应该怎样学习它。现在就让我们开始吧。 1.1 Go语言特性一瞥 我们先来看看Go语言的主要特性。 开放源代码的通用计算机编程语言。开放源代码的软件(以下简称开源软件)更容易被修正和改进。因为,几乎所有的互联网用户都可以看到这些源代码,并可以提供自己的意见和建议,甚至还可以参与到实际的开发工作中去。与一些不公开源代码的软件(也可称为闭源软件)相比,开源软件的开发迭代周期要短很多,并且迭代速度要快很多。这正印证了“众人拾柴火焰高”这句俗语。 虽为静态类型、编译型的语言,... 查看全部[ 1.1 Go语言特性一瞥 ]
1.2 Go语言的优劣
在软件行业做过一段时间的人都知道,没有万能的编程语言,没有万能开发框架,也没有万能的解决方案。任何新技术的产生都应该归功于一部分人对老旧技术的强烈不满。Go语言也不例外。比如,C的依赖管理、C++的垃圾回收、Java笨重的类型系统和厚重的Java EE规范,以及脚本语言(如PHP、Python和Ruby)的性能,这些都是很多开发者社区经常争论和抱怨的问题。 Go语言是集多编程范式之大成者,体现了优秀的软件工程思想和原则,其特性可以使开发者快速地开发、测试和部署程序,大大提高了生产效率。下面我们来看看与其他主流语言相比,Go语言具有的优势。 相对于C/C++来讲,Go语言拥有清晰的依赖管... 查看全部[ 1.2 Go语言的优劣 ]
1.3 怎样学习Go语言
作为作者,我当然希望大家能够通过阅读本书来系统地学习Go语言。不过,客观地讲,我们还是可以通过很多渠道来学习它的。比如,可以通过浏览Go语言官方网站(http://golang.org)来了解所有规范和细节,还可以在代码包文档网站(http://godoc.org)中查询到标准库和几乎所有流行的第三方库中的代码包的文档和使用示例。 当然,我们能够利用的资源远不止这些。本书的附录会罗列出更多的学习资源,尤其是中文资源。不过,比查阅相关学习资源更重要的是——动手编码。毕竟我们学习的是软件开发技术,用代码说话是学习这类技术的有效途径。在编码过程中,我们可以感受到这门编程语言的鲜活性。只有积累了足... 查看全部[ 1.3 怎样学习Go语言 ]
1.4 本章小结
这一章相当简单。相信读者已经对Go语言有一个宏观的了解了。虽说任何编程语言都会有不尽如人意的方面,但是我认为Go语言已经做得很好了。并且,它在今后还会持续、快速地改进,变得更加优秀。不论你把学习它当作一种技术投资,还是真正将它作为你的主力编程语言,都是一个非常不错的选择。... 查看全部[ 1.4 本章小结 ]
4.1 基本流程控制
在上一章中,我们介绍了Go语言的词法、数据类型以及数据的使用方法,它们都是我们编写程序的根基。在本章,我们将讲述怎样编写成段的甚至是小有规模的代码。在这样的代码中,各种流程控制语句会是我们经常用到的。它们可以制造出各种条件判断、各种循环和各种流程跳转。除此之外,我们还会详细介绍Go语言中与众不同的特殊流程控制方式。 Go语言在流程控制结构方面有些像C语言,但是在很多重要方面都与C不同。Go语言在这方面的特点如下。 在Go语言中没有do和while循环,只有一个更加广义的for语句。 Go语言中的switch语句更加灵活多变。Go语言的switch语句还可以被用于进行类型判断。 ... 查看全部[ 4.1 基本流程控制 ]
4.2 defer语句
Go语言拥有一些特有的流程控制语句。其中最常用的就是defer语句。defer语句被用于预定对一个函数的调用。我们把这类被defer语句调用的函数称为延迟函数。注意,defer语句只能出现在函数或方法的内部。 一条defer语句总是以关键字defer开始。在defer的右边还必会有一条表达式语句,且它们之间要以空格“ ”分隔,就像这样: defer fmt.Println("The finishing touches.") 这里的表达式语句必须代表一个函数或方法的调用。注意,既然是表达式语句,那么一些调用表达式就是不被允许出现在这里的。比如,针对各种内建函数的那些调用... 查看全部[ 4.2 defer语句 ]
4.3 异常处理
我们在本书前面的内容中已经涉及了一些Go语言的异常处理方面的内容,比如接口类型error、内建函数panic和标准库代码包errors。在本节,我们会对Go语言的各种异常处理方法进行系统的讲解,并试图一窥这些方法背后的内涵和哲学。 4.3.1 error 在编写Go语言代码的时候,我们应该习惯使用error类型值来表明非正常的状态。作为惯用法,在Go语言标准库代码包中的很多函数和方法也会以返回error类型值来表明错误状态及其详细信息。 我们之前说过,error是一个预定义标识符,它代表了一个Go语言内建的接口类型。这个接口类型的声明如下: type error interface ... 查看全部[ 4.3 异常处理 ]
4.4 实战演练——Set
从本节开始,我们就要运用之前了解到的Go语言基础知识来实际开发一些高级数据结构。这些数据结构都是Go语言本身及其标准库中没有涉及的。 在很多编程语言中,集合(Set)的底层都是由哈希表(Hash Table)来实现的。比如,C++语言的代码库STL中的数据结构hash_set、Java语言的标准库中的java.util.HashSet类,以及Python语句的标准数据结构set,等等。 在Go语言的标准数据类型中并没有集合这种数据类型。但是,它却拥有作为Hash Table实现的字典(Map)类型。我们在对Set和Map进行比较之后会发现它们在一些主要特性上是极其相似的,如下所示。 ... 查看全部[ 4.4 实战演练——Set ]
4.5 实战演练——Ordered Map
我们已经知道,字典类型的值有一个共同特点,即其中的元素值的迭代顺序是不确定的。但是在一些应用场景下,我们是需要固定的元素迭代顺序的。 我们在前面的章节中多次提到过,如果要使元素可排序就需要让数据类型实现sort.Interface接口类型。该接口类型中的方法Len、Less和Swap的含义分别是获取元素的数量、比较相邻元素的大小以及交换它们的位置。我们在基于数组类型或切片类型的自定义数据类型之上可以非常轻松地实现这几个方法。但是,对于基于字典类型的扩展数据类型来说,实现它们可就不那么容易了。因为字典类型值中的元素值是无序的。我们没有任何方法可以确定它们的位置以及与它们相邻的元素值。 因此,... 查看全部[ 4.5 实战演练——Ordered Map ]
4.6 本章小结
本章我们先对Go语言的代码块和作用域进行了说明,这两个概念对于我们进一步理解Go语言程序的组织方式来说非常重要。 紧接着,我们对Go语言所支持的一些基本流程控制方式进行了详述。当前流行的很多编程语言都支持这些流程控制方式。这包括了if语句、switch语句、for语句、goto语句和标记语句。虽然这些流程控制语句被很多编程语言所支持,但是Go语言在它们的表现和语义的细节上却有着它自己的特点。 除了这些被广泛支持的流程控制语句之外,Go语言还有一些独特的、杀手级的流程控制方式。defer语句就是其中之一。我们可以利用该语句轻松地完成针对函数执行的收尾工作。另外,在Go语言中,报告错误状态的方... 查看全部[ 4.6 本章小结 ]
8.1 锁的使用
我们在之前多次提到,Go语言除了为应用程序开发者提供了自己特有的并发编程模型和工具之外,还提供了传统的同步工具。它们都在Go语言的标准库代码包sync和sync/atomic中。这些工具使我们有了第二种选择。它们很简单,也很直观。如果读者仔细阅读过我们在第6章介绍的多进程和多线程编程的话,应该还会记得原子操作、互斥量、条件变量等名词。在Go语言中,这些名词都被沿用了。当然,它们从概念和用法上也都是非常相似的。下面,我们就来介绍这些同步工具。 8.1 锁的使用 在本节,我们对Go语言所提供的与锁有关的API进行说明。这包括了互斥锁和读写锁。我们在第6章描述过互斥锁,但却没有提到过读写锁。这两... 查看全部[ 8.1 锁的使用 ]
8.2 条件变量
我们在第6章讲多线程编程的时候,详细说明过条件变量的概念、原理和适用场景。因此,本小节仅对sync代码包中与条件变量相关的API进行简单的介绍,并使用它们来改造我们之前实现的*myDataFile类型的相关方法。 在Go语言中,sync.Cond类型代表了条件变量。与互斥锁和读写锁不同,简单的声明无法创建出一个可用的条件变量。为了得到这样一个条件变量,我们需要用到sync.NewCond函数。该函数的声明如下: func NewCond(l Locker) *Cond 我们在第6章中说过,条件变量总是要与互斥量组合使用。因此,sync.NewCond函数的唯一参数是sync.Locker... 查看全部[ 8.2 条件变量 ]
8.3 原子操作
我们已经知道,原子操作即是进行过程中不能被中断的操作。针对某个值的原子操作在被进行的过程当中,CPU绝不会再去进行其他的针对该值的操作。无论这些“其他的操作”是否为原子操作都会是这样。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。只有这样才能够在并发环境下保证原子操作的绝对安全。 Go语言提供的原子操作都是非侵入式的。它们由标准库代码包sync/atomic中的众多函数代表。我们可以通过调用这些函数对几种简单的类型的值进行原子操作。这些类型包括int32、int64、uint32、uint64、uintptr和unsafe.Pointer类型,共6个。这些函数提供的原子... 查看全部[ 8.3 原子操作 ]
8.4 只会执行一次
现在,让我们再次聚焦到sync代码包。除了我们介绍过的互斥锁、读写锁和条件变量,该代码包还为我们提供了几个非常有用的API。其中一个比较有特色的就是结构体类型sync.Once和它的Do方法。 与代表锁的结构体类型sync.Mutex和sync.RWMutex一样,sync.Once也是开箱即用的。换句话说,我们仅需对它进行简单的声明即可使用,就像这样: var once sync.Once once.Do(func() { fmt.Println("Once!") }) 如上所示,我们声明了一个名为once的sync.Once类型的变量之后,立刻就可以调用它的指针... 查看全部[ 8.4 只会执行一次 ]
8.5 WaitGroup
我们在第6章多次提到过sync.WaitGroup类型和它的方法。sync.WaitGroup类型的值也是开箱即用的。例如,在声明 var wg sync.WaitGroup 之后,我们就可以直接正常使用wg变量了。该类型有3个指针方法,即Add、Done和Wait。 类型sync.WaitGroup是一个结构体类型。在它之中有一个代表计数的字段。当一个sync.WaitGroup类型的变量被声明之后,其值中的那个计数值将会是0。我们可以通过该值的Add方法增大或减少其中的计数值。例如: wg.Add(3) 或 wg.Add(-3) 虽然Add方法接受一个int类型的值,并且我们... 查看全部[ 8.5 WaitGroup ]
8.6 临时对象池
本节要讲解的是sync.Pool类型。我们可以把sync.Pool类型值看作是存放可被重复使用的值的容器。此类容器是自动伸缩的,高效的,同时也是并发安全的。为了描述方便,我们也会把sync.Pool类型的值称为临时对象池,而把存于其中的值称为对象值。至于为什么要加“临时“这两个字,我们稍后再解释。 我们在用复合字面量初始化一个临时对象池的时候,可以为它唯一的公开字段New赋值。该字段的类型是func() interface{},即一个函数类型。可以猜到,被赋给字段New的函数会被临时对象池用来创建对象值。不过,实际上,该函数几乎仅在池中无可用对象值的时候才会被调用。 类型sync.Pool... 查看全部[ 8.6 临时对象池 ]
8.7 实战演练——Concurrent Map
我们在本章前面的部分中对Go语言提供的各种传统同步工具和方法进行了逐一的介绍。在本节,我们将运用它们来构造一个并发安全的字典(Map)类型。 我们已经知道,Go语言提供的字典类型并不是并发安全的。因此,我们需要使用一些同步方法对它进行扩展。这看起来并不困难。我们只要使用读写锁将针对一个字典类型值的读操作和写操作保护起来就可以了。确实,读写锁应该是我们首先想到的同步工具。不过,我们还不能确定只使用它是否就足够了。不管怎样,让我们先来编写并发安全的字典类型的第一个版本。 我们先来确定并发安全的字典类型的行为。还记得吗?依然,这需要声明一个接口类型。我们在第4章带领读者编写过OrderedMap... 查看全部[ 8.7 实战演练——Concurrent Map ]
8.8 本章小结
本章讲述了Go语言提供的各种同步工具的使用方法。虽然,它们都不是Go语言在并发环境下共享和交换数据的推荐方式。但是,在一些应用场景下,它们也不失为一种选择。并且,在某些特例中,它们可能会更加适合,并且能够在开发效率和运行效率方面表现得更好。显然,熟悉它们可以让我们在程序设计和实现上拥有更大的灵活度。... 查看全部[ 8.8 本章小结 ]