From 23ba7def7870f9cf0b61f6b34e957491e47ee24f Mon Sep 17 00:00:00 2001 From: chai2010 Date: Sun, 5 Aug 2018 08:01:21 +0800 Subject: [PATCH] =?UTF-8?q?ch1:=20=E8=A7=84=E8=8C=83=E5=8C=96=E5=86=85?= =?UTF-8?q?=E9=83=A8=E6=AE=B5=E8=90=BD=E7=BC=96=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ch1-basic/ch1-01-genesis.md | 4 ++-- ch1-basic/ch1-02-hello-revolution.md | 26 +++++++++++----------- ch1-basic/ch1-03-array-string-and-slice.md | 6 ++--- ch1-basic/ch1-04-func-method-interface.md | 6 ++--- ch1-basic/ch1-05-mem.md | 14 ++++++------ ch1-basic/ch1-06-goroutine.md | 17 +++++++------- ch1-basic/ch1-07-error-and-panic.md | 8 +++---- 7 files changed, 41 insertions(+), 40 deletions(-) diff --git a/ch1-basic/ch1-01-genesis.md b/ch1-basic/ch1-01-genesis.md index de06cc3..dd990fc 100644 --- a/ch1-basic/ch1-01-genesis.md +++ b/ch1-basic/ch1-01-genesis.md @@ -14,7 +14,7 @@ Go语言很多时候被描述为“类C语言”,或者是“21世纪的C语 Go语言其它的一些特性零散地来自于其他一些编程语言;比如iota语法是从APL语言借鉴,词法作用域与嵌套函数等特性来自于Scheme语言(和其他很多编程语言)。Go语言中也有很多自己发明创新的设计。比如Go语言的切片为轻量级动态数组提供了有效的随机存取的性能,这可能会让人联想到链表的底层的共享机制。还有Go语言新发明的defer语句(Ken发明)也是神来之笔。 -## 来自贝尔实验室特有基因 +## 1.1.1 来自贝尔实验室特有基因 作为Go语言标志性的并发编程特性则来自于贝尔实验室的Tony Hoare于1978年发表鲜为外界所知的关于并发研究的基础文献:顺序通信进程( communicating sequential processes ,缩写为CSP)。在最初的CSP论文中,程序只是一组没有中间共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。Tony Hoare的CSP并发模型只是一个用于描述并发性基本概念的描述语言,它并不是一个可以编写可执行程序的通用编程语言。 @@ -32,7 +32,7 @@ CSP并发模型最经典的实际应用是来自爱立信发明的Erlang编程 纵观整个贝尔实验室的编程语言的发展进程,从B语言、C语言、Newsqueak、Alef、Limbo语言一路走来,Go语言继承了来着贝尔实验室的半个世纪的软件设计基因,终于完成了C语言革新的使命。纵观这几年来的发展趋势,Go语言已经成为云计算、云存储时代最重要的基础编程语言。 -## 你好, 世界 +## 1.1.2 你好, 世界 按照惯例,介绍所有编程语言的第一个程序都是“Hello, World!”。虽然本教假设读者已经了解了Go语言,但是我们还是不想打破这个惯例(因为这个传统正是从Go语言的前辈C语言传承而来的)。不过,Go语言的这个程序输出的是中文“你好, 世界!”。 diff --git a/ch1-basic/ch1-02-hello-revolution.md b/ch1-basic/ch1-02-hello-revolution.md index bc31139..5a502e2 100644 --- a/ch1-basic/ch1-02-hello-revolution.md +++ b/ch1-basic/ch1-02-hello-revolution.md @@ -4,7 +4,7 @@ ![](../images/ch1-01-go-history.png) -## B语言 - Ken Thompson, 1972 +## 1.2.1 B语言 - Ken Thompson, 1972 首先是B语言,B语言是Go语言之父贝尔实验室的Ken Thompson早年间开发的一种通用的程序设计语言,设计目的是为了用于辅助UNIX系统的开发。但是因为B语言缺乏灵活的类型系统导致使用比较困难。后来,Ken Thompson的同事Dennis Ritchie以B语言为基础开发出了C语言,C语言提供了丰富的类型,极大地增加了语言的表达能力。到目前为止它依然是世界上最常用的程序语言之一。而B语言自从被它取代之后,则就只存在于各种文献之中,成为了历史。 @@ -25,7 +25,7 @@ c 'orld'; 总体来说,B语言简单,功能也比较简陋。 -## C语言 - Dennis Ritchie, 1974 ~ 1989 +## 1.2.2 C语言 - Dennis Ritchie, 1974 ~ 1989 C语言是由Dennis Ritchie在B语言的基础上改进而来,它增加了丰富的数据类型,并最终实现了用它重写UNIX的伟大目标。C语言可以说是现代IT行业最重要的软件基石,目前主流的操作系统几乎全部是由C语言开发的,许多基础系统软件也是C语言开发的。C系家族的编程语言占据统治地位达几十年之久,半个多世纪以来依然充满活力。 @@ -76,7 +76,7 @@ main(void) 至此,C语言本身的进化基本完成。后面的C92/C99/C11都只是针对一些语言细节做了完善。因为各种历史因素,C89依然是使用最广泛的标准。 -## Newsqueak - Rob Pike, 1989 +## 1.2.3 Newsqueak - Rob Pike, 1989 Newsqueak是Rob Pike发明的老鼠语言的第二代,是他用于实践CSP并发编程模型的战场。Newsqueak是新的squeak语言的意思,其中squeak是老鼠吱吱吱的叫声,也可以看作是类似鼠标点击的声音。Squeak是一个提供鼠标和键盘事件处理的编程语言,Squeak语言的管道是静态创建的。改进版的Newsqueak语言则提供了类似C语言语句和表达式的语法和类似Pascal语言的推导语法。Newsqueak是一个带自动垃圾回收的纯函数式语言,它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建的,属于第一类值,因此可以保存到变量中。 @@ -140,7 +140,7 @@ prime := sieve(); Newsqueak语言中并发体和管道的语法和Go语言已经比较接近了,后置的类型声明和Go语言的语法也很相似。 -## Alef - Phil Winterbottom, 1993 +## 1.2.4 Alef - Phil Winterbottom, 1993 在Go语言出现之前,Alef语言是作者心中比较完美的并发语言,Alef语法和运行时基本是无缝兼容C语言。Alef语言中的对线程和进程的并发体都提供了支持,其中`proc receive(c)`用于启动一个进程,`task receive(c)`用于启动一个线程,它们之间通过管道`c`进行通讯。不过由于Alef缺乏内存自动回收机制,导致并发体的内存资源管理异常复杂。而且Alef语言只在Plan9系统中提供过短暂的支持,其它操作系统并没有实际可以运行的Alef开发环境。而且Alef语言只有《Alef语言规范》和《Alef编程向导》两个公开的文档,因此在贝尔实验室之外关于Alef语言的讨论并不多。 @@ -176,7 +176,7 @@ void main(void) { Alef的语法和C语言基本保持一致,可以认为它是在C语言的语法基础上增加了并发编程相关的特性,可以看作是另一个维度的C++语言。 -## Limbo - Sean Dorward, Phil Winterbottom, Rob Pike, 1995 +## 1.2.5 Limbo - Sean Dorward, Phil Winterbottom, Rob Pike, 1995 Limbo(地狱)是用于开发运行在小型计算机上的分布式应用的编程语言,它支持模块化编程,编译期和运行时的强类型检查,进程内基于具有类型的通信管道,原子性垃圾收集和简单的抽象数据类型。Limbo被设计为:即便是在没有硬件内存保护的小型设备上,也能安全运行。Limbo语言主要运行在Inferno系统之上。 @@ -202,11 +202,11 @@ init(ctxt: ref Draw->Context, args: list of string) 从这个版本的“Hello World”程序中,我们已经可以发现很多Go语言特性的雏形。第一句`implement Hello;`基本对应Go语言的`package Hello`包声明语句。然后是`include "sys.m"; sys: Sys;`和`include "draw.m";`语句用于导入其它的模块,类似Go语言的`import "sys"`和`import "draw"`语句。然后Hello包模块还提供了模块初始化函数`init`,并且函数的参数的类型也是后置的,不过Go语言的初始化函数是没有参数的。 -## Go语言 - 2007~2009 +## 1.2.6 Go语言 - 2007~2009 贝尔实验室后来经历了多次动荡,包括Ken Thompson在内的Plan9项目原班人马最终加入了Google公司。在发明Limbo等前辈语言诞生10多年之后,在2007年底,Go语言三个最初的作者因为偶然的因素聚集到一起批斗C++(传说是C++语言的布道师在Google公司到处鼓吹的C++11各种牛逼特性彻底惹恼了他们),他们终于抽出了20%的自由时间创造了Go语言。最初的Go语言规范从2008年3月开始编写,最初的Go程序也是直接编译到C语言然后再二次编译为机器码。到了2008年5月,Google公司的领导们终于发现了Go语言的巨大潜力,从而开始全力支持这个项目(Google的创始人甚至还贡献了`func`关键字),让他们可以将全部工作时间投入到Go语言的设计和开发中。在Go语言规范初版完成之后,Go语言的编译器终于可以直接生成机器码了。 -### hello.go - 2008年6月 +### 1.2.6.1 hello.go - 2008年6月 ```go package main @@ -219,7 +219,7 @@ func main() int { 这是初期Go语言程序正式开始测试的版本。其中内置的用于调试的`print`语句已经存在,不过是以命令的方式使用。入口`main`函数还和C语言中的`main`函数一样返回`int`类型的值,而且需要`return`显式地返回值。每个语句末尾的分号也还存在。 -### hello.go - 2008年6月27日 +### 1.2.6.2 hello.go - 2008年6月27日 ```go package main @@ -231,7 +231,7 @@ func main() { 入口函数`main`已经去掉了返回值,程序默认通过隐式调用`exit(0)`来返回。Go语言朝着简单的方向逐步进化。 -### hello.go - 2008年8月11日 +### 1.2.6.3 hello.go - 2008年8月11日 ```go package main @@ -243,7 +243,7 @@ func main() { 用于调试的内置的`print`由开始的命令改为普通的内置函数,使得语法更加简单一致。 -### hello.go - 2008年10月24日 +### 1.2.6.4 hello.go - 2008年10月24日 ```go package main @@ -257,7 +257,7 @@ func main() { 作为C语言中招牌的`printf`格式化函数已经移植了到了Go语言中,函数放在`fmt`包中(`fmt`是格式化单词`format`的缩写)。不过`printf`函数名的开头字母依然是小写字母,采用大写字母表示导出的特性还没有出现。 -### hello.go - 2009年1月15日 +### 1.2.6.5 hello.go - 2009年1月15日 ```go package main @@ -271,7 +271,7 @@ func main() { Go语言开始采用是否大小写首字母来区分符号是否可以被导出。大写字母开头表示导出的公共符号,小写字母开头表示包内部的私有符号。国内用户需要注意的是,汉字中没有大小写字母的概念,因此以汉字开头的符号目前是无法导出的(针对问题中国用户已经给出相关建议,等Go2之后或许会调整对汉字的导出规则)。 -### hello.go - 2009年12月11日 +### 1.2.6.7 hello.go - 2009年12月11日 ```go package main @@ -286,7 +286,7 @@ func main() { Go语言终于移除了语句末尾的分号。这是Go语言在2009年11月10号正式开源之后第一个比较重要的语法改进。从1978年C语言教程第一版引入的分号分割的规则到现在,Go语言的作者们花了整整32年终于移除了语句末尾的分号。在这32年的演化的过程中必然充满了各种八卦故事,我想这一定是Go语言设计者深思熟虑的结果(现在Swift等新的语言也是默认忽略分号的,可见分号确实并不是那么的重要)。 -## 你好, 世界! - V2.0 +## 1.2.7 你好, 世界! - V2.0 在经过半个世纪的涅槃重生之后,Go语言不仅仅打印出了Unicode版本的“Hello, World”,而且可以方便地向全球用户提供打印服务。下面版本通过`http`服务向每个访问的客户端打印中文的“你好, 世界!”和当前的时间信息。 diff --git a/ch1-basic/ch1-03-array-string-and-slice.md b/ch1-basic/ch1-03-array-string-and-slice.md index 58171af..2b1008b 100644 --- a/ch1-basic/ch1-03-array-string-and-slice.md +++ b/ch1-basic/ch1-03-array-string-and-slice.md @@ -4,7 +4,7 @@ Go语言中数组、字符串和切片三者是密切相关的数据结构。这三种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。首先,Go语言的数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了只读限制。切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参数时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。其实Go语言的赋值和函数传参规则很简单,除了闭包函数以引用的方式对外部变量访问之外,其它赋值和函数传参数都是以传值的方式处理。要理解数组、字符串和切片三种不同的处理方式的原因需要详细了解它们的底层数据结构。 -## 数组 +## 1.3.1 数组 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。因为数组的长度是数组类型的一个部分,不同长度或不同类型的数据组成的数组都是不同的类型,因此在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值)。和数组对应的类型是切片,切片是可以动态增长和收缩的序列,切片的功能也更加灵活,但是要理解切片的工作原理还是要先理解数组。 @@ -141,7 +141,7 @@ var f = [...]int{} // 定义一个长度为0的数组 在Go语言中,数组类型是切片和字符串等结构的基础。以上数组的很多操作都可以直接用于字符串或切片中。 -## 字符串 +## 1.3.2 字符串 一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。由于Go语言的源代码要求是UTF8编码,导致Go源代码中出现的字符串面值常量一般也是UTF8编码的。源代码中的文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列。因为字节序列对应的是只读的字节序列,因此字符串可以包含任意的数据,包括byte值0。我们也可以用字符串表示GBK等非UTF8编码的数据,不过这种时候将字符串看作是一个只读的二进制数组更准确,因为`for range`等语法并不能支持非UTF8编码的字符串的遍历。 @@ -344,7 +344,7 @@ func runes2string(s []int32) string { 同样因为底层内存结构的差异,`[]rune`到字符串的转换也必然会导致重新构造字符串。这种强制转换并不存在前面提到的优化情况。 -## 切片(slice) +## 1.3.3 切片(slice) 简单地说,切片就是一种简化版的动态数组。因为动态数组的长度是不固定,切片的长度自然也就不能是类型的组成部分了。数组虽然有适用它们的地方,但是数组的类型和操作都不够灵活,因此在Go代码中数组使用的并不多。而切片则使用得相当广泛,理解切片的原理和用法是一个Go程序员的必备技能。 diff --git a/ch1-basic/ch1-04-func-method-interface.md b/ch1-basic/ch1-04-func-method-interface.md index 48aacba..3f36c53 100644 --- a/ch1-basic/ch1-04-func-method-interface.md +++ b/ch1-basic/ch1-04-func-method-interface.md @@ -8,7 +8,7 @@ Go语言程序的初始化和执行总是从`main.main`函数开始的。但是 要注意的是,在`main.main`函数执行之前所有代码都运行在同一个goroutine,也就是程序的主系统线程中。因此,如果某个`init`函数内部用go关键字启动了新的goroutine的话,新的goroutine只有在进入`main.main`函数之后才可能被执行到。 -## 函数 +## 1.4.1 函数 在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例。当然,Go语言中每个类型还可以有自己的方法,方法其实也是函数的一种。 @@ -157,7 +157,7 @@ func g() int { 第一个函数直接返回了函数参数变量的地址——这似乎是不可以的,因为如果参数变量在栈上的话,函数返回之后栈变量就失效了,返回的地址自然也应该失效了。但是Go语言的编译器和运行时比我们聪明的多,它会保证指针指向的变量在合适的地方。第二个函数,内部虽然调用`new`函数创建了`*int`类型的指针对象,但是依然不知道它具体保存在哪里。对于有C/C++编程经验的程序员需要强调的是:不用关心Go语言中函数栈和堆的问题,编译器和运行时会帮我们搞定;同样不要假设变量在内存中的位置是固定不变的,指针随时可能会变化,特别是在你不期望它变化的时候。 -## 方法 +## 1.4.2 方法 方法一般是面向对象编程(OOP)的一个特性,在C++语言中方法对应一个类对象的成员函数,是关联到具体对象上的虚表中的。但是Go语言的方法却是关联到类型的,这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。面向对象编程(OOP)进入主流开发领域一般认为是从C++开始的,C++就是在兼容C语言的基础之上支持了class等面向对象的特性。然后Java编程则号称是纯粹的面向对象语言,因为Java中函数是不能独立存在的,每个函数都必然是属于某个类的。 @@ -319,7 +319,7 @@ func (p *Cache) Lookup(key string) string { 在传统的面向对象语言(eg.C++或Java)的继承中,子类的方法是在运行时动态绑定到对象的,因此基类实现的某些方法看到的`this`可能不是基类类型对应的对象,这个特性会导致基类方法运行的不确定性。而在Go语言通过嵌入匿名的成员来“继承”的基类方法,`this`就是实现该方法的类型的对象,Go语言中方法是编译时静态绑定的。如果需要虚函数的多态特性,我们需要借助Go语言接口来实现。 -## 接口 +## 1.4.3 接口 Go语言之父Rob Pike曾说过一句名言:那些试图避免白痴行为的语言最终自己变成了白痴语言(Languages that try to disallow idiocy become themselves idiotic)。一般静态编程语言都有着严格的类型系统,这使得编译器可以深入检查程序员没有作出什么出格的举动。但是,过于严格的类型系统却会使得编程太过繁琐,让程序员把大好的青春都浪费在了和编译器的斗争中。Go语言试图让程序员能在安全和灵活的编程之间取得一个平衡。它在提供严格的类型检查的同时,通过接口类型实现了对鸭子类型的支持,使得安全动态的编程变得相对容易。 diff --git a/ch1-basic/ch1-05-mem.md b/ch1-basic/ch1-05-mem.md index a39b025..709040e 100644 --- a/ch1-basic/ch1-05-mem.md +++ b/ch1-basic/ch1-05-mem.md @@ -6,7 +6,7 @@ 常见的并行编程有多种模型,主要有多线程、消息传递等。从理论上来看,多线程和基于消息的并发编程是等价的。由于多线程并发模型可以自然对应到多核的处理器,主流的操作系统因此也都提供了系统级的多线程支持,同时从概念上讲多线程似乎也更直观,因此多线程编程模型逐步被吸纳到主流的编程语言特性或语言扩展库中。而主流编程语言对基于消息的并发编程模型支持则相比较少,Erlang语言是支持基于消息传递并发编程模型的代表者,它的并发体之间不共享内存。Go语言是基于消息并发模型的集大成者,它将基于CSP模型的并发编程内置到了语言中,通过一个go关键字就可以轻易地启动一个Goroutine,与Erlang不同的是Go语言的Goroutine之间是共享内存的。 -## Goroutine和系统线程 +## 1.5.1 Goroutine和系统线程 Goroutine是Go语言特有的并发体,是一种轻量级的线程,由go关键字启动。在真实的Go语言的实现中,goroutine和系统线程也不是等价的。尽管两者的区别实际上只是一个量的区别,但正是这个量变引发了Go语言并发编程质的飞跃。 @@ -16,7 +16,7 @@ Go的运行时还包含了其自己的调度器,这个调度器使用了一些 在Go语言中启动一个Goroutine不仅和调用函数一样简单,而且Goroutine之间调度代价也很低,这些因素极大地促进了并发编程的流行和发展。 -## 原子操作 +## 1.5.2 原子操作 所谓的原子操作就是并发编程中“最小的且不可并行化”的操作。通常,有多个并发体对一个共享资源的操作是原子操作的话,同一时刻最多只能有一个并发体对该资源进行操作。从线程角度看,在当前线程修改共享资源期间,其它的线程是不能访问该资源的。原子操作对于多线程并发编程模型来说,不会发生有别于单线程的意外情况,共享资源的完整性可以得到保证。 @@ -181,7 +181,7 @@ for i := 0; i < 10; i++ { 这是一个简化的生产者、消费者模型:后台线程生成最新的配置信息;前台多个工作者线程获取最新的配置信息。所有线程共享配置信息资源。 -## 顺序一致性内存模型 +## 1.5.3 顺序一致性内存模型 如果只是想简单地在线程之间进行数据同步的话,原子操作已经为编程人员提供了一些同步保障。不过这种保障有一个前提:顺序一致性的内存模型。要了解顺序一致性,我们先看看一个简单的例子: @@ -253,7 +253,7 @@ func main() { 可以确定后台线程的`mu.Unlock()`必然在`println("你好, 世界")`完成后发生(同一个线程满足顺序一致性),`main`函数的第二个`mu.Lock()`必然在后台线程的`mu.Unlock()`之后发生(`sync.Mutex`保证),此时后台线程的打印工作已经顺利完成了。 -## 初始化顺序 +## 1.5.4 初始化顺序 前面函数章节中我们已经简单介绍过程序的初始化顺序,这是属于Go语言面向并发的内存模型的基础规范。 @@ -265,7 +265,7 @@ Go程序的初始化和执行总是从`main.main`函数开始的。但是如果` 因为所有的`init`函数和`main`函数都是在主线程完成,它们也是满足顺序一致性模型的。 -## Goroutine的创建 +## 1.5.5 Goroutine的创建 `go`语句会在当前Goroutine对应函数返回前创建新的Goroutine. 例如: @@ -285,7 +285,7 @@ func hello() { 执行`go f()`语句创建Goroutine和`hello`函数是在同一个Goroutine中执行, 根据语句的书写顺序可以确定Goroutine的创建发生在`hello`函数返回之前, 但是新创建Goroutine对应的`f()`的执行事件和`hello`函数返回的事件则是不可排序的,也就是并发的。调用`hello`可能会在将来的某一时刻打印`"hello, world"`,也很可能是在`hello`函数执行完成后才打印。 -## 基于Channel的通信 +## 1.5.6 基于Channel的通信 Channel通信是在Goroutine之间进行同步的主要方法。在无缓存的Channel上的每一次发送操作都有与其对应的接收操作相配对,发送和接收操作通常发生在不同的Goroutine上(在同一个Goroutine上执行2个操作很容易导致死锁)。**无缓存的Channel上的发送操作总在对应的接收操作完成前发生.** @@ -368,7 +368,7 @@ func main() { 最后一句`select{}`是一个空的管道选择语句,该语句会导致`main`线程阻塞,从而避免程序过早退出。还有`for{}`、`<-make(chan int)`等诸多方法可以达到类似的效果。因为`main`线程被阻塞了,如果需要程序正常退出的话可以通过调用`os.Exit(0)`实现。 -## 不靠谱的同步 +## 1.5.7 不靠谱的同步 前面我们已经分析过,下面代码无法保证正常打印结果。实际的运行效果也是大概率不能正常输出结果。 diff --git a/ch1-basic/ch1-06-goroutine.md b/ch1-basic/ch1-06-goroutine.md index 4235731..f6a9586 100644 --- a/ch1-basic/ch1-06-goroutine.md +++ b/ch1-basic/ch1-06-goroutine.md @@ -11,7 +11,8 @@ Go语言最吸引人的地方是它内建的并发支持。Go语言并发体系 > 不要通过共享内存来通信,而应通过通信来共享内存。 这是更高层次的并发编程哲学(通过管道来传值是Go语言推荐的做法)。虽然像引用计数这类简单的并发问题通过原子操作或互斥锁就能很好地实现,但是通过信道来控制访问能够让你写出更简洁正确的程序。 -## 并发版本的Hello world + +## 1.6.1 并发版本的Hello world 我们先以在一个新的Goroutine中输出“Hello world”,`main`等待后台线程输出工作完成之后退出,这样一个简单的并发程序作为热身。 @@ -128,7 +129,7 @@ func main() { 其中`wg.Add(1)`用于增加等待事件的个数,必须确保在后台线程启动之前执行(如果放到后台线程之中执行则不能保证被正常执行到)。当后台线程完成打印工作之后,调用`wg.Done()`表示完成一个事件。`main`函数的`wg.Wait()`是等待全部的事件完成。 -## 生产者消费者模型 +## 1.6.2 生产者消费者模型 并发编程中最常见的例子就是生产者/消费者模式,该模式主要通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。简单地说,就是生产者生产一些数据,然后放到成果队列中,同时消费者从成果队列中来取这些数据。这样就让生产消费变成了异步的两个过程。当成果队列中没有数据时,消费者就进入饥饿的等待中;而当成果队列中数据已满时,生产者则面临因产品挤压导致CPU被剥夺的下岗问题。 @@ -181,7 +182,7 @@ func main() { 我们这个例子中有2个生产者,并且2个生产者之间并无同步事件可参考,它们是并发的。因此,消费者输出的结果序列的顺序是不确定的,这并没有问题,生产者和消费者依然可以相互配合工作。 -## 发布订阅模型 +## 1.6.3 发布订阅模型 发布/订阅(publish-and-subscribe)模型通常被简写为pub/sub模型。在这个模型中,消息生产者成为发布者(publisher),而消息消费者则称对应订阅者(subscriber),生产者和消费者是M:N的关系。在传统生产者和消费者模型中,成果是将消息发送到一个队列中,而发布/订阅模型则是将消息发布给一个主题。 @@ -318,7 +319,7 @@ func main() { 在发布订阅模型中,每条消息都会传送给多个订阅者。发布者通常不会知道、也不关心哪一个订阅者正在接收主题消息。订阅者和发布者可以在运行时动态添加是一种松散的耦合关心,这使得系统的复杂性可以随时间的推移而增长。在现实生活中,不同城市的象天气预报之类的应用就可以应用这个并发模式。 -## 控制并发数 +## 1.6.4 控制并发数 很多用户在适应了Go语言强大的并发特性之后,都倾向于编写最大并发的程序,因为这样似乎可以提供最大的性能。在现实中我们行色匆匆,但有时却需要我们放慢脚步享受生活,并发的程序也是一样:有时候我们需要适当地控制并发的程度,因为这样不仅仅可给其它的应用/任务让出/预留一定的CPU资源,也可以适当降低功耗缓解电池的压力。 @@ -382,7 +383,7 @@ func (fs gatefs) Lstat(p string) (os.FileInfo, error) { 我们不仅可以控制最大的并发数目,而且可以通过带缓存Channel的使用量和最大容量比例来判断程序运行的并发率。当管道为空的时候可以认为是空闲状态,当管道满了时任务是繁忙状态,这对于后台一些低级任务的运行是有参考价值的。 -## 赢者为王 +## 1.6.5 赢者为王 采用并发编程的动机有很多:并发编程可以简化问题,比如一类问题对应一个处理线程会更简单;并发编程还可以提升性能,在一个多核CPU上开2个线程一般会比开1个线程快一些。其实对于提升性能而言,程序并不是简单地运行速度快就表示用户体验好的;很多时候程序能快速响应用户请求才是最重要的,当没有用户请求需要处理的时候才合适处理一些低优先级的后台任务。 @@ -411,7 +412,7 @@ func main() { 通过适当开启一些冗余的线程,尝试用不同途径去解决同样的问题,最终以赢者为王的方式提升了程序的相应性能。 -## 素数筛 +## 1.6.6 素数筛 在“Hello world 的革命”一节中,我们为了演示Newsqueak的并发特性,文中给出了并发版本素数筛的实现。并发版本的素数筛是一个经典的并发例子,通过它我们可以更深刻地理解Go语言的并发特性。“素数筛”的原理如图: @@ -470,7 +471,7 @@ func main() { 素数筛展示了一种优雅的并发程序结构。但是因为每个并发体处理的任务粒度太细微,程序整体的性能并不理想。对于细力度的并发程序,CSP模型中固有的消息传递的代价太高了(多线程并发模型同样要面临线程启动的代价)。 -## 并发的安全退出 +## 1.6.7 并发的安全退出 有时候我们需要通知goroutine停止它正在干的事情,特别是当它工作在错误的方向上的时候。Go语言并没有提供在一个直接终止Goroutine的方法,由于这样会导致goroutine之间的共享变量落在未定义的状态上。但是如果我们想要退出两个或者任意多个Goroutine怎么办呢? @@ -612,7 +613,7 @@ func main() { 现在每个工作者并发体的创建、运行、暂停和退出都是在`main`函数的安全控制之下了。 -## context包 +## 1.6.8 context包 在Go1.7发布时,标准库增加了一个`context`包,用来简化对于处理单个请求的多个Goroutine之间与请求域的数据、超时和退出等操作,官方有博文对此做了专门介绍。我们可以用`context`包来重新实现前面的线程安全退出或超时的控制: diff --git a/ch1-basic/ch1-07-error-and-panic.md b/ch1-basic/ch1-07-error-and-panic.md index 61a1385..c7ff635 100644 --- a/ch1-basic/ch1-07-error-and-panic.md +++ b/ch1-basic/ch1-07-error-and-panic.md @@ -43,7 +43,7 @@ func main() { 捕获异常不是最终的目的。如果异常不可预测,直接输出异常信息是最好的处理方式。 -## 错误处理策略 +## 1.7.1 错误处理策略 让我们演示一个文件复制的例子:函数需要打开两个文件,然后将其中一个文件的内容复制到另一个文件: @@ -107,7 +107,7 @@ func ParseJSON(input string) (s *Syntax, err error) { Go语言库的实现习惯: 即使在包内部使用了`panic`,但是在导出函数时会被转化为明确的错误值。 -# 获取错误的上下文 +## 1.7.2 获取错误的上下文 有时候为了方便上层用户理解;很多时候底层实现者会将底层的错误重新包装为新的错误类型返回给用户: @@ -247,7 +247,7 @@ if err != nil { Go语言中大部分函数的代码结构几乎相同,首先是一系列的初始检查,用于防止错误发生,之后是函数的实际逻辑。 -# 错误的错误返回 +## 1.7.3 错误的错误返回 Go语言中的错误是一种接口类型。接口信息中包含了原始类型和原始的值。只有当接口的类型和原始的值都为空的时候,接口的值才对应`nil`。其实当接口中类型为空的时候,原始值必然也是空的;反之,当接口对应的原始值为空的时候,接口对应的原始类型并不一定为空的。 @@ -278,7 +278,7 @@ func returnsError() error { Go语言作为一个强类型语言,不同类型之间必须要显式的转换(而且必须有相同的基础类型)。但是,Go语言中`interface`是一个例外:非接口类型到接口类型,或者是接口类型之间的转换都是隐式的。这是为了支持方便的鸭子面向对象编程,当然会牺牲一定的安全特性。 -# 剖析异常 +## 1.7.4 剖析异常 `panic`支持抛出任意类型的异常(而不仅仅是`error`类型的错误),`recover`函数调用的返回值和`panic`函数的输入参数类型一致,它们的函数签名如下: