修正半角标点符号

pull/62/head
kimw 2018-06-09 16:27:25 +00:00
parent a81bfabcdc
commit 7ebd75aeae
60 changed files with 132 additions and 132 deletions

View File

@ -10,7 +10,7 @@
Go语言的面向对象机制与一般语言不同。它没有类层次结构甚至可以说没有类仅仅通过组合而不是继承简单的对象来构建复杂的对象。方法不仅可以定义在结构体上而且可以定义在任何用户自定义的类型上并且具体类型和抽象类型接口之间的关系是隐式的所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论接口在第七章讨论。
第八章讨论了基于顺序通信进程(CSP)概念的并发编程使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。
第八章讨论了基于顺序通信进程CSP概念的并发编程使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。
第十章描述了包机制和包的组织结构。这一章还展示了如何有效地利用Go自带的工具使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。

View File

@ -4,7 +4,7 @@
`os`包以跨平台的方式提供了一些与操作系统交互的函数和变量。程序的命令行参数可从os包的Args变量获取os包外部使用os.Args访问该变量。
os.Args变量是一个字符串string的*切片*slice译注slice和Python语言中的切片类似是一个简版的动态数组切片是Go语言的基础概念稍后详细介绍。现在先把切片s当作数组元素序列序列的长度动态变化用`s[i]`访问单个元素,用`s[m:n]`获取子序列(译注和python里的语法差不多)。序列的元素数目为len(s)。和大多数编程语言类似区间索引时Go言里也采用左闭右开形式区间包括第一个索引元素不包括最后一个因为这样可以简化逻辑。译注比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3]不包含最后一个元素。比如s[m:n]这个切片0 ≤ m ≤ n ≤ len(s)包含n-m个元素。
os.Args变量是一个字符串string的*切片*slice译注slice和Python语言中的切片类似是一个简版的动态数组切片是Go语言的基础概念稍后详细介绍。现在先把切片s当作数组元素序列序列的长度动态变化用`s[i]`访问单个元素,用`s[m:n]`获取子序列译注和python里的语法差不多。序列的元素数目为len(s)。和大多数编程语言类似区间索引时Go言里也采用左闭右开形式区间包括第一个索引元素不包括最后一个因为这样可以简化逻辑。译注比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3]不包含最后一个元素。比如s[m:n]这个切片0 ≤ m ≤ n ≤ len(s)包含n-m个元素。
os.Args的第一个元素os.Args[0]是命令本身的名字其它的元素则是程序启动时传给它的参数。s[m:n]形式的切片表达式产生从第m个元素到第n-1个元素的切片下个例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表达式的m或n会默认传入0或len(s)因此前面的切片可以简写成os.Args[1:]。

View File

@ -46,7 +46,7 @@ counts[line] = counts[line] + 1
`map`中不含某个键时不用担心,首次读到新行时,等号右边的表达式`counts[line]`的值将被计算为其类型的零值,对于`int`即0。
为了打印结果,我们使用了基于`range`的循环,并在`counts`这个`map`上迭代。跟之前类似,每次迭代得到两个结果,键和其在`map`中对应的值。`map`的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。(译注具体可以参见这里http://stackoverflow.com/questions/11853396/google-go-lang-assignment-order)
为了打印结果,我们使用了基于`range`的循环,并在`counts`这个`map`上迭代。跟之前类似,每次迭代得到两个结果,键和其在`map`中对应的值。`map`的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。译注具体可以参见这里http://stackoverflow.com/questions/11853396/google-go-lang-assignment-order
继续来看`bufio`包,它使处理输入和输出方便又高效。`Scanner`类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。

View File

@ -1,6 +1,6 @@
## 1.4. GIF动画
下面的程序会演示Go语言标准库里的image这个package的用法我们会用这个包来生成一系列的bit-mapped图然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形(Lissajous figures)这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。图1.1是这样的一个例子:
下面的程序会演示Go语言标准库里的image这个package的用法我们会用这个包来生成一系列的bit-mapped图然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形Lissajous figures这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。图1.1是这样的一个例子:
![](../images/ch1-01.png)
@ -76,7 +76,7 @@ func lissajous(out io.Writer) {
[]color.Color{...}和gif.GIF{...}这两个表达式就是我们说的复合声明4.2和4.4.1节有说明。这是实例化Go语言里的复合类型的一种写法。这里的前者生成的是一个slice切片后者生成的是一个struct结构体。
gif.GIF是一个struct类型参考4.4节。struct是一组值或者叫字段的集合不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量并且其内部变量LoopCount字段会被设置为nframes而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点(.)来进行访问就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。
gif.GIF是一个struct类型参考4.4节。struct是一组值或者叫字段的集合不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量并且其内部变量LoopCount字段会被设置为nframes而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点.来进行访问就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。
lissajous函数内部有两层嵌套的for循环。外层循环会循环64次每一次都会生成一个单独的动画帧。它生成了一个包含两种颜色的201*201大小的图片白色和黑色。所有像素点都会被默认设置为其零值也就是调色板palette里的第0个值这里我们设置的是白色。每次外层循环都会生成一张新图片并将一些像素设置为黑色。其结果会append到之前结果之后。这里我们用到了append(参考4.2.1)内置函数将结果append到anim中的帧列表末尾并设置一个默认的80ms的延迟值。循环结束后所有的延迟值被编码进了GIF图片中并将结果写入到输出流。out这个变量是io.Writer类型这个类型支持把输出结果写到很多目标很快我们就可以看到例子。

View File

@ -63,7 +63,7 @@ goroutine是一种函数的并发执行方式而channel是用来在goroutine
main函数中用make函数创建了一个传递string类型参数的channel对每一个命令行参数我们都用go这个关键字来创建一个goroutine并且让函数在这个goroutine异步执行http.Get方法。这个程序里的io.Copy会把响应的Body内容拷贝到ioutil.Discard输出流中译注可以把这个变量看作一个垃圾桶可以向里面写一些不需要的数据因为我们需要这个方法返回的字节数但是又不想要其内容。每当请求返回内容时fetch函数都会往ch这个channel里写入一个字符串由main函数里的第二个for循环来处理并打印channel里的这个字符串。
当一个goroutine尝试在一个channel上做send或者receive操作时这个goroutine会阻塞在调用处直到另一个goroutine从这个channel里接收或者写入值这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression)(<-ch)mainfetchgoroutinemain退
当一个goroutine尝试在一个channel上做send或者receive操作时这个goroutine会阻塞在调用处直到另一个goroutine从这个channel里接收或者写入值这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中每一个fetch函数在执行时都会往channel里发送一个值ch <- expression<-chmainfetchgoroutinemain退
**练习 1.10** 找一个数据量比较大的网站用本小节中的程序调研网站的缓存策略对每个URL执行两遍请求查看两次时间是否有较大的差别并且每次获取到的响应内容是否一致修改本节中的程序将响应结果输出以便于进行对比。

View File

@ -34,9 +34,9 @@ func Signum(x int) int {
这种形式叫做无tag switch(tagless switch)这和switch true是等价的。
像for和if控制语句一样switch也可以紧跟一个简短的变量声明一个自增表达式、赋值语句或者一个函数调用(译注:比其它语言丰富)
像for和if控制语句一样switch也可以紧跟一个简短的变量声明一个自增表达式、赋值语句或者一个函数调用(译注:比其它语言丰富)
break和continue语句会改变控制流。和其它语言中的break和continue一样break会中断当前的循环并开始执行循环之后的内容而continue会跳过当前循环并开始执行下一次循环。这两个语句除了可以控制for循环还可以用来控制switch和select语句(之后会讲到)在1.3节中我们看到continue会跳过内层的循环如果我们想跳过的是更外层的循环的话我们可以在相应的位置加上label这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。
break和continue语句会改变控制流。和其它语言中的break和continue一样break会中断当前的循环并开始执行循环之后的内容而continue会跳过当前循环并开始执行下一次循环。这两个语句除了可以控制for循环还可以用来控制switch和select语句(之后会讲到)在1.3节中我们看到continue会跳过内层的循环如果我们想跳过的是更外层的循环的话我们可以在相应的位置加上label这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。
**命名类型:** 类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长所以我们总要给这种struct取一个名字。本章中就有这样一个例子二维点类型

View File

@ -23,4 +23,4 @@ func main() {
第二个例外,包所在的目录中可能有一些文件名是以``_test.go``为后缀的Go源文件译注前面必须有其它的字符因为以`_`或`.`开头的源文件会被构建工具忽略),并且这些源文件声明的包名也是以`_test`为后缀名的。这种目录可以包含两种包:一种是普通包,另一种则是测试的外部扩展包。所有以`_test`为后缀包名的测试外部扩展包都由go test命令独立编译普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖具体细节我们将在11.2.4节中介绍。
第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如"gopkg.in/yaml.v2"。这种情况下包的名字并不包含版本号后缀而是yaml。
第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如“gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀而是yaml。

View File

@ -36,6 +36,6 @@ import (
导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。
导入包重命名是一个有用的特性它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重特别是在一些自动生成的代码中这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如如果文件中已经有了一个名为path的变量那么我们可以将"path"标准包重命名为pathpkg。
导入包重命名是一个有用的特性它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重特别是在一些自动生成的代码中这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如如果文件中已经有了一个名为path的变量那么我们可以将“path”标准包重命名为pathpkg。
每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况Go语言的构建工具将报告错误。

View File

@ -2,6 +2,6 @@
go test命令是一个按照一定的约定和组织来测试代码的程序。在包目录内所有以`_test.go`为后缀名的源文件在执行go build时不会被构建成包的一部分它们是go test测试的一部分。
在`*_test.go`文件中,有三种类型的函数:测试函数、基准测试(benchmark)函数、示例函数。一个测试函数是以Test为函数名前缀的函数用于测试程序的一些逻辑行为是否正确go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数它们用于衡量一些函数的性能go test命令会多次运行基准测试函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数提供一个由编译器保证正确性的示例文档。我们将在11.2节讨论测试函数的所有细节并在11.4节讨论基准测试函数的细节然后在11.6节讨论示例函数的细节。
在`*_test.go`文件中,有三种类型的函数:测试函数、基准测试benchmark函数、示例函数。一个测试函数是以Test为函数名前缀的函数用于测试程序的一些逻辑行为是否正确go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数它们用于衡量一些函数的性能go test命令会多次运行基准测试函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数提供一个由编译器保证正确性的示例文档。我们将在11.2节讨论测试函数的所有细节并在11.4节讨论基准测试函数的细节然后在11.6节讨论示例函数的细节。
go test命令会遍历所有的`*_test.go`文件中符合上述命名规则的函数生成一个临时的main包用于调用相应的测试函数接着构建并运行、报告测试结果最后清理测试中生成的临时文件。

View File

@ -8,7 +8,7 @@
当我们准备TestEcho测试的时候我们修改了echo函数使用包级的out变量作为输出对象因此测试代码可以用另一个实现代替标准输出这样可以方便对比echo输出的数据。使用类似的技术我们可以将产品代码的其他部分也替换为一个容易测试的伪对象。使用伪对象的好处是我们可以方便配置容易预测更可靠也更容易观察。同时也可以避免一些不良的副作用例如更新生产数据库或信用卡消费行为。
下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。当用户使用了超过90%的存储配额之后将发送提醒邮件。(译注一般在实现业务机器监控包括磁盘、cpu、网络等的时候需要类似的到达阈值=>触发报警的逻辑,所以是很实用的案例)
下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。当用户使用了超过90%的存储配额之后将发送提醒邮件。译注一般在实现业务机器监控包括磁盘、cpu、网络等的时候需要类似的到达阈值=>触发报警的逻辑,所以是很实用的案例。)
<u><i>gopl.io/ch11/storage1</i></u>
```Go

View File

@ -1,6 +1,6 @@
## 11.5. 剖析
基准测试(Benchmark)对于衡量特定操作的性能是有帮助的但是当我们试图让程序跑的更快的时候我们通常并不知道从哪里开始优化。每个码农都应该知道Donald Knuth在1974年的“Structured Programming with go to Statements”上所说的格言。虽然经常被解读为不重视性能的意思但是从原文我们可以看到不同的含义
基准测试Benchmark对于衡量特定操作的性能是有帮助的但是当我们试图让程序跑的更快的时候我们通常并不知道从哪里开始优化。每个码农都应该知道Donald Knuth在1974年的“Structured Programming with go to Statements”上所说的格言。虽然经常被解读为不重视性能的意思但是从原文我们可以看到不同的含义
> 毫无疑问对效率的片面追求会导致各种滥用。程序员会浪费大量的时间在非关键程序的速度上实际上这些尝试提升效率的行为反倒可能产生很大的负面影响特别是当调试和维护的时候。我们不应该过度纠结于细节的优化应该说约97%的场景:过早的优化是万恶之源。
>

View File

@ -1,6 +1,6 @@
## 12.2. reflect.Type 和 reflect.Value
反射是由 reflect 包提供的。它定义了两个重要的类型Type 和 Value。一个 Type 表示一个Go类型。它是一个接口有许多方法来区分类型以及检查它们的组成部分例如一个结构体的成员或一个函数的参数等。唯一能反映 reflect.Type 实现的是接口的类型描述信息(§7.5),也正是这个实体标识了接口值的动态类型。
反射是由 reflect 包提供的。它定义了两个重要的类型Type 和 Value。一个 Type 表示一个Go类型。它是一个接口有许多方法来区分类型以及检查它们的组成部分例如一个结构体的成员或一个函数的参数等。唯一能反映 reflect.Type 实现的是接口的类型描述信息§7.5,也正是这个实体标识了接口值的动态类型。
函数 reflect.TypeOf 接受任意的 interface{} 类型,并以 reflect.Type 形式返回其动态类型:

View File

@ -8,9 +8,9 @@ Display是一个用于显示结构化数据的调试工具但是它并不能
```
42 integer
"hello" string (带有Go风格的引号)
foo symbol (未用引号括起来的名字)
(1 2 3) list (括号包起来的0个或多个元素)
"hello" string带有Go风格的引号
foo symbol(未用引号括起来的名字)
(1 2 3) list 括号包起来的0个或多个元素
```
布尔型习惯上使用t符号表示true空列表或nil符号表示false但是为了简单起见我们暂时忽略布尔类型。同时忽略的还有chan管道和函数因为通过反射并无法知道它们的确切状态。我们忽略的还有浮点数、复数和interface。支持它们是练习12.3的任务。
@ -141,7 +141,7 @@ omin.)" "Best Picture (Nomin.)")) (Sequel nil))
在12.6节中我们将给出S表达式解码器的实现步骤但是在那之前我们还需要先了解如何通过反射技术来更新程序的变量。
**练习 12.3** 实现encode函数缺少的分支。将布尔类型编码为t和nil浮点数编码为Go语言的格式复数1+2i编码为#C(1.0 2.0)格式。接口编码为类型名和值对,例如("[]int" (1 2 3))但是这个形式可能会造成歧义reflect.Type.String方法对于不同的类型可能返回相同的结果。
**练习 12.3** 实现encode函数缺少的分支。将布尔类型编码为t和nil浮点数编码为Go语言的格式复数1+2i编码为#C(1.0 2.0)格式。接口编码为类型名和值对,例如"[]int" (1 2 3)但是这个形式可能会造成歧义reflect.Type.String方法对于不同的类型可能返回相同的结果。
**练习 12.4** 修改encode函数以上面的格式化形式输出S表达式。

View File

@ -17,15 +17,15 @@ Sizeof函数返回的大小只包括数据结构中固定的部分例如字
类型 | 大小
------------------------------- | -----------------------------
`bool` | 1个字节
`intN, uintN, floatN, complexN` | N/8个字节(例如float64是8个字节)
`intN, uintN, floatN, complexN` | N/8个字节例如float64是8个字节
`int, uint, uintptr` | 1个机器字
`*T` | 1个机器字
`string` | 2个机器字(data,len)
`[]T` | 3个机器字(data,len,cap)
`string` | 2个机器字data、len
`[]T` | 3个机器字data、len、cap
`map` | 1个机器字
`func` | 1个机器字
`chan` | 1个机器字
`interface` | 2个机器字(type,value)
`interface` | 2个机器字type、value
Go语言的规范并没有要求一个字段的声明顺序和内存中的顺序是一致的所以理论上一个编译器可以随意地重新排列每个字段的内存位置虽然在写作本书的时候编译器还没有这么做。下面的三个结构体虽然有着相同的字段但是第一种写法比另外的两个需要多50%的内存。

View File

@ -56,7 +56,7 @@ fmt.Println(i, i+1, i*i) // "127 -128 1"
这里是一元的加法和减法运算符:
```
+ 一元加法 (无效果)
+ 一元加法(无效果)
- 负数
```
@ -68,7 +68,7 @@ Go语言还提供了以下的bit位操作运算符前面4个操作运算符
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空 (AND NOT)
&^ 位清空AND NOT
<< 左移
>> 右移
```

View File

@ -151,7 +151,7 @@ func f(x, y float64) float64 {
**练习 3.2** 试验math包中其他函数的渲染图形。你是否能输出一个egg box、moguls或a saddle图案?
**练习 3.3** 根据高度给每个多边形上色,那样峰值部将是红色(#ff0000),谷部将是蓝色(#0000ff)
**练习 3.3** 根据高度给每个多边形上色,那样峰值部将是红色#ff0000谷部将是蓝色#0000ff
**练习 3.4** 参考1.7节Lissajous例子的函数构造一个web服务器用于计算函数曲面然后返回SVG数据给客户端。服务器必须设置Content-Type头部

View File

@ -20,8 +20,8 @@
\r 回车
\t 制表符
\v 垂直制表符
\' 单引号 (只用在 '\'' 形式的rune符号面值中)
\" 双引号 (只用在 "..." 形式的字符串面值中)
\' 单引号(只用在 '\'' 形式的rune符号面值中
\" 双引号(只用在 "..." 形式的字符串面值中)
\\ 反斜杠
```

View File

@ -97,7 +97,7 @@ func equal(x, y []string) bool {
上面关于两个slice的深度相等测试运行的时间并不比支持==操作的数组或字符串更多但是为何slice不直接支持比较运算符呢这方面有两个原因。第一个原因一个slice的元素是间接引用的一个slice甚至可以包含自身。虽然有很多办法处理这种情形但是没有一个是简单有效的。
第二个原因因为slice的元素是间接引用的一个固定的slice值(译注指slice本身的值不是元素的值)在不同的时刻可能包含不同的元素因为底层数组的元素可能会被修改。而例如Go语言中map的key只做简单的浅拷贝它要求key在整个生命周期内保持不变性(译注例如slice扩容就会导致其本身的值/地址变化)。而用深度相等判断的话显然在map的key这种场合不合适。对于像指针或chan之类的引用类型==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的也能临时解决map类型的key问题但是slice和数组不同的相等测试行为会让人困惑。因此安全的做法是直接禁止slice之间的比较操作。
第二个原因因为slice的元素是间接引用的一个固定的slice值译注指slice本身的值不是元素的值在不同的时刻可能包含不同的元素因为底层数组的元素可能会被修改。而例如Go语言中map的key只做简单的浅拷贝它要求key在整个生命周期内保持不变性译注例如slice扩容就会导致其本身的值/地址变化)。而用深度相等判断的话显然在map的key这种场合不合适。对于像指针或chan之类的引用类型==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的也能临时解决map类型的key问题但是slice和数组不同的相等测试行为会让人困惑。因此安全的做法是直接禁止slice之间的比较操作。
slice唯一合法的比较操作是和nil比较例如

View File

@ -151,7 +151,7 @@ $ ./fetch https://golang.org | ./outline
正如你在上面实验中所见大部分HTML页面只需几层递归就能被处理但仍然有些页面需要深层次的递归。
大部分编程语言使用固定大小的函数调用栈常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度当你用递归处理大量数据时需要避免栈溢出除此之外还会导致安全性问题。与此相反Go语言使用可变栈栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
大部分编程语言使用固定大小的函数调用栈常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度当你用递归处理大量数据时需要避免栈溢出除此之外还会导致安全性问题。与此相反Go语言使用可变栈栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
**练习 5.1** 修改findlinks代码中遍历n.FirstChild链表的部分将循环调用visit改成递归调用。

View File

@ -114,4 +114,4 @@ return words, images, err
**练习 5.5** 实现countWordsAndImages。参考练习4.9如何分词)
**练习 5.6** 修改gopl.io/ch3/surface (§3.2) 中的corner函数将返回值命名并使用bare return。
**练习 5.6** 修改gopl.io/ch3/surface§3.2中的corner函数将返回值命名并使用bare return。

View File

@ -26,7 +26,7 @@ fmt.Println(err)
fmt.Printf("%v", err)
```
通常当函数返回non-nil的error时其他的返回值是未定义的(undefined)这些未定义的返回值应该被忽略。然而有少部分函数在发生错误时仍然会返回一些有用的返回值。比如当读取文件发生错误时Read函数会返回可以读取的字节数以及错误信息。对于这种情况正确的处理方式应该是先处理这些不完整的数据再处理错误。因此对函数的返回值要有清晰的说明以便于其他人使用。
通常当函数返回non-nil的error时其他的返回值是未定义的undefined这些未定义的返回值应该被忽略。然而有少部分函数在发生错误时仍然会返回一些有用的返回值。比如当读取文件发生错误时Read函数会返回可以读取的字节数以及错误信息。对于这种情况正确的处理方式应该是先处理这些不完整的数据再处理错误。因此对函数的返回值要有清晰的说明以便于其他人使用。
在Go中函数运行失败时会返回错误信息这些错误信息被认为是一种预期的值而非异常exception这使得Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制但这些机制仅被使用在处理那些未被预料到的错误即bug而不是那些在健壮程序中应该被避免的程序错误。对于Go的异常机制我们将在5.9介绍。

View File

@ -23,7 +23,7 @@ func (p Point) Distance(q Point) float64 {
}
```
上面的代码里那个附加的参数p叫做方法的接收器(receiver),早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。
上面的代码里那个附加的参数p叫做方法的接收器receiver,早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。
在Go语言中我们并不会像其它语言那样用this或者self作为接收器我们可以任意的选择接收器的名字。由于接收器的名字经常会被使用到所以保持其在方法间传递时的一致性和简短性是不错的主意。这里的建议是可以使用其类型的第一个字母比如这里使用了Point的首字母p。
@ -38,7 +38,7 @@ fmt.Println(p.Distance(q)) // "5", method call
可以看到上面的两个函数调用都是Distance但是却没有发生冲突。第一个Distance的调用实际上用的是包级别的函数geometry.Distance而第二个则是使用刚刚声明的Point调用的是Point类下声明的Point.Distance方法。
这种p.Distance的表达式叫做选择器因为他会选择合适的对应p这个对象的Distance方法来执行。选择器也会被用来选择一个struct类型的字段比如p.X。由于方法和字段都是在同一命名空间所以如果我们在这里声明一个X方法的话编译器会报错因为在调用p.X时会有歧义(译注:这里确实挺奇怪的)
这种p.Distance的表达式叫做选择器因为他会选择合适的对应p这个对象的Distance方法来执行。选择器也会被用来选择一个struct类型的字段比如p.X。由于方法和字段都是在同一命名空间所以如果我们在这里声明一个X方法的话编译器会报错因为在调用p.X时会有歧义(译注:这里确实挺奇怪的)
因为每种类型都有其方法的命名空间我们在用Distance这个名字的时候不同的Distance调用指向了不同类型里的Distance方法。让我们来定义一个Path类型这个Path代表一个线段的集合并且也给这个Path定义一个叫Distance的方法。
@ -57,7 +57,7 @@ func (path Path) Distance() float64 {
}
```
Path是一个命名的slice类型而不是Point那样的struct类型然而我们依然可以为它定义方法。在能够给任意类型定义方法这一点上Go和很多其它的面向对象的语言不太一样。因此在Go语言里我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。我们可以给同一个包内的任意命名类型定义方法只要这个命名类型的底层类型(译注:这个例子里,底层类型是指[]Point这个slicePath就是命名类型)不是指针或者interface。
Path是一个命名的slice类型而不是Point那样的struct类型然而我们依然可以为它定义方法。在能够给任意类型定义方法这一点上Go和很多其它的面向对象的语言不太一样。因此在Go语言里我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。我们可以给同一个包内的任意命名类型定义方法只要这个命名类型的底层类型(译注:这个例子里,底层类型是指[]Point这个slicePath就是命名类型不是指针或者interface。
两个Distance方法有不同的类型。他们两个方法之间没有任何关系尽管Path的Distance方法会在内部调用Point.Distance方法来计算每个连接邻接点的线段的长度。

View File

@ -43,7 +43,7 @@ func (v Values) Add(key, value string) {
}
```
这个定义向外部暴露了一个map的命名类型并且提供了一些能够简单操作这个map的方法。这个map的value字段是一个string的slice所以这个Values是一个多维map。客户端使用这个变量的时候可以使用map固有的一些操作(make切片m[key]等等),也可以使用这里提供的操作方法,或者两者并用,都是可以的:
这个定义向外部暴露了一个map的命名类型并且提供了一些能够简单操作这个map的方法。这个map的value字段是一个string的slice所以这个Values是一个多维map。客户端使用这个变量的时候可以使用map固有的一些操作make切片m[key]等等),也可以使用这里提供的操作方法,或者两者并用,都是可以的:
<u><i>gopl.io/ch6/urlvalues</i></u>
```go

View File

@ -13,7 +13,7 @@ func (p *Point) ScaleBy(factor float64) {
在现实的程序里一般会约定如果Point这个类有一个指针作为接收器的方法那么所有Point的方法都必须有一个指针接收器即使是那些并不需要这个指针接收器的函数。我们在这里打破了这个约定只是为了展示一下两种方法的异同而已。
只有类型(Point)和指向他们的指针`(*Point)`,才可能是出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:
只有类型Point和指向他们的指针`(*Point)`,才可能是出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:
```go
type P *int

View File

@ -59,7 +59,7 @@ func (p *ColoredPoint) ScaleBy(factor float64) {
当Point.Distance被第一个包装方法调用时它的接收器值是p.Point而不是p当然了在Point类的方法里你是访问不到ColoredPoint的任何字段的。
在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一个*Point的指针。
在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一个*Point的指针。
```go
type ColoredPoint struct {
@ -86,9 +86,9 @@ type ColoredPoint struct {
然后这种类型的值便会拥有Point和RGBA类型的所有方法以及直接定义在ColoredPoint中的方法。当编译器解析一个选择器到方法时比如p.ScaleBy它会首先去找直接定义在这个类型里的ScaleBy方法然后找被ColoredPoint的内嵌字段们引入的方法然后去找Point和RGBA的内嵌字段引入的方法然后一直递归向下找。如果选择器有二义性的话编译器会报错比如你在同一级里有两个同名的方法。
方法只能在命名类型(像Point)或者指向类型的指针上定义但是多亏了内嵌有些时候我们给匿名struct类型来定义方法也有了手段。
方法只能在命名类型像Point或者指向类型的指针上定义但是多亏了内嵌有些时候我们给匿名struct类型来定义方法也有了手段。
下面是一个小trick。这个例子展示了简单的cache其使用两个包级别的变量来实现一个mutex互斥量(§9.2)和它所操作的cache
下面是一个小trick。这个例子展示了简单的cache其使用两个包级别的变量来实现一个mutex互斥量§9.2和它所操作的cache
```go
var (

View File

@ -1,6 +1,6 @@
## 6.4. 方法值和方法表达式
我们经常选择一个方法并且在同一个表达式里执行比如常见的p.Distance()形式实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”选择器会返回一个方法"值"->一个将方法(Point.Distance)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器(译注:因为已经在前文中指定过了),只要传入函数的参数即可:
我们经常选择一个方法并且在同一个表达式里执行比如常见的p.Distance()形式实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”选择器会返回一个方法“值”->一个将方法Point.Distance绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器(译注:因为已经在前文中指定过了),只要传入函数的参数即可:
```go
p := Point{1, 2}
@ -17,7 +17,7 @@ scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)
```
在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话方法"值"会非常实用(``=_=`真是绕)。举例来说下面例子中的time.AfterFunc这个函数的功能是在指定的延迟时间之后来执行一个(译注:另外的)函数。且这个函数操作的是一个Rocket对象r
在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话方法“值”会非常实用(``=_=`真是绕)。举例来说下面例子中的time.AfterFunc这个函数的功能是在指定的延迟时间之后来执行一个(译注:另外的)函数。且这个函数操作的是一个Rocket对象r
```go
type Rocket struct { /* ... */ }
@ -26,7 +26,7 @@ r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
```
直接用方法"值"传入AfterFunc的话可以更为简短
直接用方法“值”传入AfterFunc的话可以更为简短
```go
time.AfterFunc(10 * time.Second, r.Launch)
@ -34,9 +34,9 @@ time.AfterFunc(10 * time.Second, r.Launch)
译注:省掉了上面那个例子里的匿名函数。
和方法"值"相关的还有方法表达式。当调用一个方法时,与调用一个普通的函数相比,我们必须要用选择器(p.Distance)语法来指定方法的接收器。
和方法“值”相关的还有方法表达式。当调用一个方法时,与调用一个普通的函数相比,我们必须要用选择器p.Distance语法来指定方法的接收器。
当T是一个类型时方法表达式可能会写作`T.f`或者`(*T).f`,会返回一个函数"值",这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:
当T是一个类型时方法表达式可能会写作`T.f`或者`(*T).f`,会返回一个函数“值”,这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:
```go
p := Point{1, 2}

View File

@ -1,6 +1,6 @@
## 6.5. 示例: Bit数组
Go语言里的集合一般会用map[T]bool这种形式来表示T代表元素类型。集合用map类型来表示虽然非常灵活但我们可以以一种更好的形式来表示它。例如在数据流分析领域集合元素通常是一个非负整数集合会包含很多元素并且集合会经常进行并集、交集操作这种情况下bit数组会比map表现更加理想。(译注这里再补充一个例子比如我们执行一个http下载任务把文件按照16kb一块划分为很多块需要有一个全局变量来标识哪些块下载完成了这种时候也需要用到bit数组)
Go语言里的集合一般会用map[T]bool这种形式来表示T代表元素类型。集合用map类型来表示虽然非常灵活但我们可以以一种更好的形式来表示它。例如在数据流分析领域集合元素通常是一个非负整数集合会包含很多元素并且集合会经常进行并集、交集操作这种情况下bit数组会比map表现更加理想。译注这里再补充一个例子比如我们执行一个http下载任务把文件按照16kb一块划分为很多块需要有一个全局变量来标识哪些块下载完成了这种时候也需要用到bit数组。)
一个bit数组通常会用一个无符号数或者称之为“字”的slice来表示每一个元素的每一位都表示集合里的一个值。当集合的第i位被设置时我们才说这个集合包含元素i。下面的这个程序展示了一个简单的bit数组类型并且实现了三个函数来对这个bit数组来进行操作
@ -39,7 +39,7 @@ func (s *IntSet) UnionWith(t *IntSet) {
}
```
因为每一个字都有64个二进制位所以为了定位x的bit位我们用了x/64的商作为字的下标并且用x%64得到的值作为这个字内的bit的所在位置。UnionWith这个方法里用到了bit位的“或”逻辑操作符号|来一次完成64个元素的或计算。(在练习6.5中我们还会有程序用到这个64位字的例子。)
因为每一个字都有64个二进制位所以为了定位x的bit位我们用了x/64的商作为字的下标并且用x%64得到的值作为这个字内的bit的所在位置。UnionWith这个方法里用到了bit位的“或”逻辑操作符号|来一次完成64个元素的或计算。在练习6.5中我们还会有程序用到这个64位字的例子。
当前这个实现还缺少了很多必要的特性我们把其中一些作为练习题列在本小节之后。但是有一个方法如果缺失的话我们的bit数组可能会比较难混将IntSet作为一个字符串来打印。这里我们来实现它让我们来给上面的例子添加一个String方法类似2.5节中做的那样:

View File

@ -18,7 +18,7 @@ type IntSet struct {
type IntSet []uint64
```
尽管这个版本的IntSet在本质上是一样的但它也允许其它包中可以直接读取并编辑这个slice。换句话说相对于`*s`这个表达式会出现在所有的包中s.words只需要在定义IntSet的包中出现(译注:所以还是推荐后者吧的意思)
尽管这个版本的IntSet在本质上是一样的但它也允许其它包中可以直接读取并编辑这个slice。换句话说相对于`*s`这个表达式会出现在所有的包中s.words只需要在定义IntSet的包中出现(译注:所以还是推荐后者吧的意思)
这种基于名字的手段使得在语言中最小的封装单元是package而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性无论你的代码是写在一个函数还是一个方法里。
@ -49,7 +49,7 @@ func (b *Buffer) Grow(n int) {
}
```
封装的第三个优点也是最重要的优点是阻止了外部调用方对对象内部的值任意地进行修改。因为对象内部变量只可以被同一个包内的函数修改所以包的作者可以让这些函数确保对象内部的一些值的不变性。比如下面的Counter类型允许调用方来增加counter变量的值并且允许将这个值reset为0但是不允许随便设置这个值(译注:因为压根就访问不到)
封装的第三个优点也是最重要的优点是阻止了外部调用方对对象内部的值任意地进行修改。因为对象内部变量只可以被同一个包内的函数修改所以包的作者可以让这些函数确保对象内部的一些值的不变性。比如下面的Counter类型允许调用方来增加counter变量的值并且允许将这个值reset为0但是不允许随便设置这个值(译注:因为压根就访问不到)
```go
type Counter struct { n int }

View File

@ -1,6 +1,6 @@
# 第六章 方法
从90年代早期开始面向对象编程(OOP)就成为了称霸工程界和教育界的编程范式所以之后几乎所有大规模被应用的语言都包含了对OOP的支持go语言也不例外。
从90年代早期开始面向对象编程OOP就成为了称霸工程界和教育界的编程范式所以之后几乎所有大规模被应用的语言都包含了对OOP的支持go语言也不例外。
尽管没有被大众所接受的明确的OOP的定义从我们的理解来讲一个对象其实也就是一个简单的值或者一个变量在这个对象中会包含一些方法而一个方法则是一个一个和特殊类型关联的函数。一个面向对象的程序会用方法来表达其属性和对应的操作这样使用这个对象的用户就不需要直接去操作对象而是借助方法来做这些事情。

View File

@ -20,7 +20,7 @@ func Sprintf(format string, args ...interface{}) string {
}
```
Fprintf的前缀F表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是`*os.File`类型在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区然而它
Fprintf的前缀F表示文件File也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是`*os.File`类型在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区然而它
并不是一个文件类型尽管它在某种意义上和文件类型相似。
即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型这是一个接口类型定义如下
@ -43,9 +43,9 @@ type Writer interface {
io.Writer类型定义了函数Fprintf和这个函数调用者之间的约定。一方面这个约定需要调用者提供具体类型的值就像`*os.File`和`*bytes.Buffer`这些类型都有一个特定签名和行为的Write的函数。另一方面这个约定保证了Fprintf接受任何满足io.Writer接口的值都可以工作。Fprintf函数可能没有假定写入的是一个文件或是一段内存而是写入一个可以调用Write函数的值。
因为fmt.Fprintf函数没有对具体操作的值做任何假设而是仅仅通过io.Writer接口的约定来保证行为所以第一个参数可以安全地传入一个只需要满足io.Writer接口的任意具体类型的值。一个类型可以自由地被另一个满足相同接口的类型替换被称作可替换性(LSP里氏替换)。这是一个面向对象的特征。
因为fmt.Fprintf函数没有对具体操作的值做任何假设而是仅仅通过io.Writer接口的约定来保证行为所以第一个参数可以安全地传入一个只需要满足io.Writer接口的任意具体类型的值。一个类型可以自由地被另一个满足相同接口的类型替换被称作可替换性LSP里氏替换。这是一个面向对象的特征。
让我们通过一个新的类型来进行校验,下面`*ByteCounter`类型里的Write方法仅仅在丢弃写向它的字节前统计它们的长度。(在这个+=赋值语句中让len(p)的类型和`*c`的类型匹配的转换是必须的。)
让我们通过一个新的类型来进行校验,下面`*ByteCounter`类型里的Write方法仅仅在丢弃写向它的字节前统计它们的长度。(在这个+=赋值语句中让len(p)的类型和`*c`的类型匹配的转换是必须的。)
<u><i>gopl.io/ch7/bytecounter</i></u>
```go
@ -92,4 +92,4 @@ type Stringer interface {
func CountingWriter(w io.Writer) (io.Writer, *int64)
```
**练习 7.3** 为在gopl.io/ch4/treesort (§4.4)中的*tree类型实现一个String方法去展示tree类型的值序列。
**练习 7.3** 为在gopl.io/ch4/treesort§4.4中的*tree类型实现一个String方法去展示tree类型的值序列。

View File

@ -95,7 +95,7 @@ var _ io.Writer = (*bytes.Buffer)(nil)
非空的接口类型比如io.Writer经常被指针类型实现尤其当一个或多个接口方法像Write方法那样隐式的给接收者带来变化的时候。一个结构体的指针是非常常见的承载方法的类型。
但是并不意味着只有指针类型满足接口类型甚至连一些有设置方法的接口类型也可能会被Go语言中其它的引用类型实现。我们已经看过slice类型的方法(geometry.Path, §6.1)和map类型的方法(url.Values, §6.2.1),后面还会看到函数类型的方法的例子(http.HandlerFunc, §7.7)。甚至基本的类型也可能会实现一些接口就如我们在7.4章中看到的time.Duration类型实现了fmt.Stringer接口。
但是并不意味着只有指针类型满足接口类型甚至连一些有设置方法的接口类型也可能会被Go语言中其它的引用类型实现。我们已经看过slice类型的方法geometry.Path§6.1和map类型的方法url.Values§6.2.1后面还会看到函数类型的方法的例子http.HandlerFunc§7.7。甚至基本的类型也可能会实现一些接口就如我们在7.4章中看到的time.Duration类型实现了fmt.Stringer接口。
一个具体的类型可能实现了很多不相关的接口。考虑在一个组织出售数字文化产品比如音乐,电影和书籍的程序中可能定义了下列的具体类型:

View File

@ -49,7 +49,7 @@ type Value interface {
String方法格式化标记的值用在命令行帮组消息中这样每一个flag.Value也是一个fmt.Stringer。Set方法解析它的字符串参数并且更新标记变量的值。实际上Set方法和String是两个相反的操作所以最好的办法就是对他们使用相同的注解方式。
让我们定义一个允许通过摄氏度或者华氏温度变换的形式指定温度的celsiusFlag类型。注意celsiusFlag内嵌了一个Celsius类型(§2.5)因此不用实现本身就已经有String方法了。为了实现flag.Value我们只需要定义Set方法
让我们定义一个允许通过摄氏度或者华氏温度变换的形式指定温度的celsiusFlag类型。注意celsiusFlag内嵌了一个Celsius类型§2.5因此不用实现本身就已经有String方法了。为了实现flag.Value我们只需要定义Set方法
<u><i>gopl.io/ch7/tempconv</i></u>
```go

View File

@ -39,7 +39,7 @@ if out != nil {
![](../images/ch7-05.png)
动态分配机制依然决定(\*bytes.Buffer).Write的方法会被调用但是这次的接收者的值是nil。对于一些如\*os.File的类型nil是一个有效的接收者(§6.2.1),但是\*bytes.Buffer类型不在这些种类中。这个方法会被调用但是当它尝试去获取缓冲区时会发生panic。
动态分配机制依然决定(\*bytes.Buffer).Write的方法会被调用但是这次的接收者的值是nil。对于一些如\*os.File的类型nil是一个有效的接收者§6.2.1,但是\*bytes.Buffer类型不在这些种类中。这个方法会被调用但是当它尝试去获取缓冲区时会发生panic。
问题在于尽管一个nil的\*bytes.Buffer指针有实现这个接口的方法它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(\*bytes.Buffer).Write方法的接收者非空的隐含先觉条件所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer因此可以避免一开始就将一个不完整的值赋值给这个接口

View File

@ -125,7 +125,7 @@ Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
```
sort.Reverse函数值得进行更近一步的学习因为它使用了(§6.3)章中的组合这是一个重要的思路。sort包定义了一个不公开的struct类型reverse它嵌入了一个sort.Interface。reverse的Less方法调用了内嵌的sort.Interface值的Less方法但是通过交换索引的方式使排序结果变成逆序。
sort.Reverse函数值得进行更近一步的学习因为它使用了§6.3章中的组合这是一个重要的思路。sort包定义了一个不公开的struct类型reverse它嵌入了一个sort.Interface。reverse的Less方法调用了内嵌的sort.Interface值的Less方法但是通过交换索引的方式使排序结果变成逆序。
```go
package sort
@ -217,6 +217,6 @@ fmt.Println(sort.IntsAreSorted(values)) // "false"
**练习 7.8** 很多图形界面提供了一个有状态的多重排序表格插件主要的排序键是最近一次点击过列头的列第二个排序键是第二最近点击过列头的列等等。定义一个sort.Interface的实现用在这样的表格中。比较这个实现方式和重复使用sort.Stable来排序的方式。
**练习 7.9** 使用html/template包 (§4.6) 替代printTracks将tracks展示成一个HTML表格。将这个解决方案用在前一个练习中让每次点击一个列的头部产生一个HTTP请求来排序这个表格。
**练习 7.9** 使用html/template包§4.6替代printTracks将tracks展示成一个HTML表格。将这个解决方案用在前一个练习中让每次点击一个列的头部产生一个HTTP请求来排序这个表格。
**练习 7.10** sort.Interface类型也可以适用在其它地方。编写一个IsPalindrome(s sort.Interface) bool函数表明序列s是否是回文序列换句话说反向排序不会改变这个序列。假设如果!s.Less(i, j) && !s.Less(j, i)则索引i和j上的元素相等。

View File

@ -1,6 +1,6 @@
## 7.7. http.Handler接口
在第一章中我们粗略的了解了怎么用net/http包去实现网络客户端(§1.5)和服务器(§1.7)。在这个小节中我们会对那些基于http.Handler接口的服务器API做更进一步的学习
在第一章中我们粗略的了解了怎么用net/http包去实现网络客户端§1.5和服务器§1.7。在这个小节中我们会对那些基于http.Handler接口的服务器API做更进一步的学习
<u><i>net/http</i></u>
```go
@ -144,7 +144,7 @@ func (db database) price(w http.ResponseWriter, req *http.Request) {
}
```
让我们关注这两个注册到handlers上的调用。第一个db.list是一个方法值 (§6.4),它是下面这个类型的值。
让我们关注这两个注册到handlers上的调用。第一个db.list是一个方法值§6.4,它是下面这个类型的值。
```go
func(w http.ResponseWriter, req *http.Request)
@ -195,4 +195,4 @@ func main() {
**练习 7.11** 增加额外的handler让客户端可以创建读取更新和删除数据库记录。例如一个形如 `/update?item=socks&price=6` 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)
**练习 7.12** 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包(§4.6)可能会对你有帮助。
**练习 7.12** 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包§4.6可能会对你有帮助。

View File

@ -48,7 +48,7 @@ fmt.Printf("%#v\n", err)
// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}
```
这就是三个帮助函数是怎么工作的。例如下面展示的IsNotExist它会报出是否一个错误和syscall.ENOENT(§7.8)或者和有名的错误os.ErrNotExist相等(可以在§5.4.2中找到io.EOF或者是一个`*PathError`它内部的错误是syscall.ENOENT和os.ErrNotExist其中之一。
这就是三个帮助函数是怎么工作的。例如下面展示的IsNotExist它会报出是否一个错误和syscall.ENOENT§7.8或者和有名的错误os.ErrNotExist相等可以在§5.4.2中找到io.EOF或者是一个`*PathError`它内部的错误是syscall.ENOENT和os.ErrNotExist其中之一。
```go
import (

View File

@ -1,6 +1,6 @@
## 7.12. 通过类型断言询问行为
下面这段逻辑和net/http包中web服务器负责写入HTTP头字段例如"Content-type:text/html的部分相似。io.Writer接口类型的变量w代表HTTP响应写入它的字节最终被发送到某个人的web浏览器上。
下面这段逻辑和net/http包中web服务器负责写入HTTP头字段例如"Content-type:text/html"的部分相似。io.Writer接口类型的变量w代表HTTP响应写入它的字节最终被发送到某个人的web浏览器上。
```go
func writeHeader(w io.Writer, contentType string) error {

View File

@ -56,7 +56,7 @@ default: // ...
}
```
(§1.8)中的普通switch语句一样每一个case会被顺序的进行考虑并且当一个匹配找到时这个case中的内容会被执行。当一个或多个case类型是接口时case的顺序就会变得很重要因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。
§1.8中的普通switch语句一样每一个case会被顺序的进行考虑并且当一个匹配找到时这个case中的内容会被执行。当一个或多个case类型是接口时case的顺序就会变得很重要因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。
注意到在原来的函数中对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型类型分支语句有一个扩展的形式它可以将提取的值绑定到一个在每个case范围内都有效的新变量。

View File

@ -1,6 +1,6 @@
## 7.15. 一些建议
当设计一个新的包时新手Go程序员总是先创建一套接口然后再定义一些满足它们的具体类型。这种方式的结果就是有很多的接口它们中的每一个仅只有一个实现。不要再这么做了。这种接口是不必要的抽象它们也有一个运行时损耗。你可以使用导出机制(§6.6)来限制一个类型的方法或一个结构体的字段是否在包外可见。接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。
当设计一个新的包时新手Go程序员总是先创建一套接口然后再定义一些满足它们的具体类型。这种方式的结果就是有很多的接口它们中的每一个仅只有一个实现。不要再这么做了。这种接口是不必要的抽象它们也有一个运行时损耗。你可以使用导出机制§6.6来限制一个类型的方法或一个结构体的字段是否在包外可见。接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。
当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好方式。

View File

@ -4,4 +4,4 @@
很多面向对象的语言都有相似的接口概念但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说我们没有必要对于给定的具体类型定义所有满足的接口类型简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
在本章我们会开始看到接口类型和值的一些基本技巧。顺着这种方式我们将学习几个来自标准库的重要接口。很多Go程序中都尽可能多的去使用标准库中的接口。最后我们会在(§7.10)看到类型断言的知识,在(§7.13)看到类型开关的使用并且学到他们是怎样让不同的类型的概括成为可能。
在本章我们会开始看到接口类型和值的一些基本技巧。顺着这种方式我们将学习几个来自标准库的重要接口。很多Go程序中都尽可能多的去使用标准库中的接口。最后我们会在§7.10看到类型断言的知识§7.13看到类型开关的使用并且学到他们是怎样让不同的类型的概括成为可能。

View File

@ -49,9 +49,9 @@ Listen函数创建了一个net.Listener的对象这个对象会监听一个
handleConn函数会处理一个完整的客户端连接。在一个for死循环中用time.Now()获取当前时刻然后写到客户端。由于net.Conn实现了io.Writer接口我们可以直接向其写入内容。这个死循环会一直执行直到写入失败。最可能的原因是客户端主动断开连接。这种情况下handleConn函数会用defer调用关闭服务器侧的连接然后返回到主函数继续等待下一个连接请求。
time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板标识如何来格式化时间而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几,月份,一个月的第几天,等等)。可以以任意的形式来组合前面这个模板出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式比如time.RFC1123。在进行格式化的逆向操作time.Parse时也会用到同样的策略。(译注这是go语言和其它语言相比比较奇葩的一个地方。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700而不像其它语言那样Y-m-d H:i:s一样当然了这里可以用1234567的方式来记忆倒是也不麻烦)
time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板标识如何来格式化时间而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几、月份、一个月的第几天……)。可以以任意的形式来组合前面这个模板出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式比如time.RFC1123。在进行格式化的逆向操作time.Parse时也会用到同样的策略。译注这是go语言和其它语言相比比较奇葩的一个地方。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700而不像其它语言那样Y-m-d H:i:s一样当然了这里可以用1234567的方式来记忆倒是也不麻烦。)
为了连接例子里的服务器我们需要一个客户端程序比如netcat这个工具(nc命令),这个工具可以用来执行网络连接操作。
为了连接例子里的服务器我们需要一个客户端程序比如netcat这个工具nc命令,这个工具可以用来执行网络连接操作。
```
$ go build gopl.io/ch8/clock1

View File

@ -46,7 +46,7 @@ func main() {
}
```
当main goroutine从标准输入流中读取内容并将其发送给服务器时另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时例如用户在终端中按了Control-D(^D)在windows上是Control-Z这时程序就会被终止尽管其它goroutine中还有进行中的任务。(在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束)
当main goroutine从标准输入流中读取内容并将其发送给服务器时另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时例如用户在终端中按了Control-D(^D)在windows上是Control-Z这时程序就会被终止尽管其它goroutine中还有进行中的任务。在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束。
下面这个会话中,客户端的输入是左对齐的,服务端的响应会用缩进来区别显示。
客户端会向服务器“喊三次话”:

View File

@ -26,7 +26,7 @@ func makeThumbnails(filenames []string) {
}
```
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注embarrassingly parallel直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题译注embarrassingly parallel直译的话更像是尴尬并行。易并行问题是最容易被实现成并行的一类问题废话,并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。
下面让我们并行地执行这些操作从而将文件IO的延迟隐藏掉并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误之后再进行处理。
@ -71,7 +71,7 @@ for _, f := range filenames {
}
```
回忆一下之前在5.6.1节中匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循环所以当这些goroutine开始读取f的值时它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数我们能够确保使用的f是当go语句执行时的“当前”那个f。
回忆一下之前在5.6.1节中匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循环所以当这些goroutine开始读取f的值时它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数我们能够确保使用的f是当go语句执行时的“当前”那个f。
如果我们想要从每一个worker goroutine往主goroutine中返回值时该怎么办呢当我们调用thumbnail.ImageFile创建文件失败的时候它会返回一个错误。下一个版本的makeThumbnails会返回其在做拉伸操作时接收到的第一个错误
@ -98,9 +98,9 @@ func makeThumbnails4(filenames []string) error {
}
```
这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时都会永远地阻塞下去并且永远都不会退出。这种情况叫做goroutine泄露(§8.4.4)可能会导致整个程序卡住或者跑出out of memory的错误。
这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时都会永远地阻塞下去并且永远都不会退出。这种情况叫做goroutine泄露§8.4.4可能会导致整个程序卡住或者跑出out of memory的错误。
最简单的解决办法就是用一个具有合适大小的buffered channel这样这些worker goroutine向channel中发送错误时就不会被阻塞。(一个可选的解决办法是创建一个另外的goroutine当main goroutine返回第一个错误的同时去排空channel)
最简单的解决办法就是用一个具有合适大小的buffered channel这样这些worker goroutine向channel中发送错误时就不会被阻塞。一个可选的解决办法是创建一个另外的goroutine当main goroutine返回第一个错误的同时去排空channel。)
下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字附带生成时的错误。
@ -135,9 +135,9 @@ func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
}
```
我们最后一个版本的makeThumbnails返回了新文件们的大小总计数(bytes)。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里而是通过一个string的channel传过来所以我们无法对循环的次数进行预测。
我们最后一个版本的makeThumbnails返回了新文件们的大小总计数bytes。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里而是通过一个string的channel传过来所以我们无法对循环的次数进行预测。
为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始)我们需要一个递增的计数器在每一个goroutine启动时加一在goroutine退出时减一。这需要一种特殊的计数器这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup下面的代码就用到了这种方法
为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始)我们需要一个递增的计数器在每一个goroutine启动时加一在goroutine退出时减一。这需要一种特殊的计数器这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup下面的代码就用到了这种方法
```go
// makeThumbnails6 makes thumbnails for each file received from the channel.

View File

@ -38,7 +38,7 @@ func main() {
}
```
注意这里的crawl所在的goroutine会将link作为一个显式的参数传入来避免“循环变量快照”的问题(在5.6.1中有讲解)。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容却没有一端接收内容时发生死锁。当然这里我们也可以用buffered channel来解决问题这里不再赘述。
注意这里的crawl所在的goroutine会将link作为一个显式的参数传入来避免“循环变量快照”的问题在5.6.1中有讲解)。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容却没有一端接收内容时发生死锁。当然这里我们也可以用buffered channel来解决问题这里不再赘述。
现在爬虫可以高并发地运行起来并且可以产生一大坨的URL了不过还是会有俩问题。一个问题是在运行一段时间后可能会出现在log的错误信息里的
@ -58,9 +58,9 @@ https://golang.org/blog/
最初的错误信息是一个让人莫名的DNS查找失败即使这个域名是完全可靠的。而随后的错误信息揭示了原因这个程序一次性创建了太多网络连接超过了每一个进程的打开文件数限制既而导致了在调用net.Dial像DNS查找失败这样的问题。
这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情因为不管怎么说你的系统总是会有一些个限制因素比如CPU核心数会限制你的计算负载比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率比如你的网络带宽限制了你的下载速度上限或者是你的一个web服务的服务容量上限等等。为了解决这个问题我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用这里的n一般小于文件描述符的上限值比如20。这和一个夜店里限制客人数目是一个道理只有当有客人离开时才会允许新的客人进入店内(译注:……)
这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情因为不管怎么说你的系统总是会有一些个限制因素比如CPU核心数会限制你的计算负载比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率比如你的网络带宽限制了你的下载速度上限或者是你的一个web服务的服务容量上限等等。为了解决这个问题我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用这里的n一般小于文件描述符的上限值比如20。这和一个夜店里限制客人数目是一个道理只有当有客人离开时才会允许新的客人进入店内。
我们可以用一个有容量限制的buffered channel来控制并发这类似于操作系统里的计数信号量概念。从概念上讲channel里的n个空槽代表n个可以处理内容的token(通行证)从channel里接收一个值会释放其中的一个token并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。(这里可能我们拿channel里填充的槽来做token更直观一些不过还是这样吧~)。由于channel里的元素类型并不重要我们用一个零值的struct{}来作为其元素。
我们可以用一个有容量限制的buffered channel来控制并发这类似于操作系统里的计数信号量概念。从概念上讲channel里的n个空槽代表n个可以处理内容的token(通行证)从channel里接收一个值会释放其中的一个token并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。这里可能我们拿channel里填充的槽来做token更直观一些不过还是这样吧。由于channel里的元素类型并不重要我们用一个零值的struct{}来作为其元素。
让我们重写crawl函数将对links.Extract的调用操作用获取、释放token的操作包裹起来来确保同一时间对其只有20个调用。信号量数量和其能操作的IO资源数量应保持接近。
@ -82,7 +82,7 @@ func crawl(url string) []string {
}
```
第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。(当然除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题)。为了使这个程序能够终止我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。
第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。当然除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题。为了使这个程序能够终止我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。
```go
func main() {
@ -151,13 +151,13 @@ func main() {
所有的爬虫goroutine现在都是被同一个channel - unseenLinks喂饱的了。主goroutine负责拆分它从worklist里拿到的元素然后把没有抓过的经由unseenLinks channel发送给一个爬虫的goroutine。
seen这个map被限定在main goroutine中也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式这样的约束可以让我们从一定程度上保证程序的正确性。例如内部变量不能够在函数外部被访问到变量(§2.3.4)在没有发生变量逃逸(译注:局部变量被全局变量引用地址导致变量被分配在堆上)的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。
seen这个map被限定在main goroutine中也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式这样的约束可以让我们从一定程度上保证程序的正确性。例如内部变量不能够在函数外部被访问到变量§2.3.4)在没有发生变量逃逸(译注:局部变量被全局变量引用地址导致变量被分配在堆上)的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。
crawl函数爬到的链接在一个专有的goroutine中被发送到worklist中来避免死锁。为了节省篇幅这个例子的终止问题我们先不进行详细阐述了。
**练习 8.6** 为并发爬虫增加深度限制。也就是说如果用户设置了depth=3那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。
**练习 8.7** 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org开头译注外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。
**练习 8.7** 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面比如golang.org开头译注外链的应该就不算了。当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。
**译注:**

View File

@ -26,7 +26,7 @@ go func() {
}()
```
现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了当一切正常时的ticker channel就像NASA jorgon的"nominal"译注这梗估计我们是不懂了或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息如果我们这么做的话如果第一个channel中没有事件发过来那么程序就会立刻被阻塞这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了为了能够多路复用我们使用了select语句。
现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了当一切正常时的ticker channel就像NASA jorgon的"nominal"译注这梗估计我们是不懂了或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息如果我们这么做的话如果第一个channel中没有事件发过来那么程序就会立刻被阻塞这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用multiplex这些操作了为了能够多路复用我们使用了select语句。
```go
select {
@ -102,7 +102,7 @@ func main() {
}
```
time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine每次被唤醒时发送一个事件。当countdown函数返回时它会停止从tick中接收事件但是ticker这个goroutine还依然存活继续徒劳地尝试向channel中发送值然而这时候已经没有其它的goroutine会从该channel中接收值了--这被称为goroutine泄露(§8.4.4)
time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine每次被唤醒时发送一个事件。当countdown函数返回时它会停止从tick中接收事件但是ticker这个goroutine还依然存活继续徒劳地尝试向channel中发送值然而这时候已经没有其它的goroutine会从该channel中接收值了——这被称为goroutine泄露§8.4.4
Tick函数挺方便但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话我们应该使用下面的这种模式

View File

@ -131,7 +131,7 @@ $ ./du2 -v $HOME /usr /bin /etc
213201 files 62.7 GB
```
然而这个程序还是会花上很长时间才会结束。完全可以并发调用walkDir从而发挥磁盘系统的并行性能。下面这个第三个版本的du会对每一个walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup (§8.5)来对仍旧活跃的walkDir调用进行计数另一个goroutine会在计数器减为零的时候将fileSizes这个channel关闭。
然而这个程序还是会花上很长时间才会结束。完全可以并发调用walkDir从而发挥磁盘系统的并行性能。下面这个第三个版本的du会对每一个walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup§8.5来对仍旧活跃的walkDir调用进行计数另一个goroutine会在计数器减为零的时候将fileSizes这个channel关闭。
<u><i>gopl.io/ch8/du3</i></u>
```go

View File

@ -24,7 +24,7 @@ func cancelled() bool {
}
```
下面我们创建一个从标准输入流中读取内容的goroutine这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键)这个goroutine就会把取消消息通过关闭done的channel广播出去。
下面我们创建一个从标准输入流中读取内容的goroutine这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键)这个goroutine就会把取消消息通过关闭done的channel广播出去。
```go
// Cancel traversal when input is detected.
@ -81,7 +81,7 @@ func dirents(dir string) []os.FileInfo {
}
```
现在当取消发生时所有后台的goroutine都会迅速停止并且主函数会返回。当然当主函数返回时一个程序会退出而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用取代掉直接从主函数返回我们调用一个panic然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出他们可能没办法被正确地取消掉也有可能被取消但是取消操作会很花时间所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断看看最终到底是什么样的情况。
现在当取消发生时所有后台的goroutine都会迅速停止并且主函数会返回。当然当主函数返回时一个程序会退出而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用取代掉直接从主函数返回我们调用一个panic然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出他们可能没办法被正确地取消掉也有可能被取消但是取消操作会很花时间所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断看看最终到底是什么样的情况。
**练习 8.10** HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。提示http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之设置它的Cancel字段然后用http.DefaultClient.Do(req)来进行这个http请求。

View File

@ -23,7 +23,7 @@ func main() {
}
```
然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的"资格"信息。
然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的“资格”信息。
```go
type client chan<- string // an outgoing message channel
@ -112,7 +112,7 @@ You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
127.0.0.1:64211 has left”
```
当与n个客户端保持聊天session时这个程序会有2n+2个并发的goroutine然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中broadcaster所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例两个东西都是并发安全的。我们会在下一章中更多地讲解约束并发安全以及goroutine中共享变量的含义。
当与n个客户端保持聊天session时这个程序会有2n+2个并发的goroutine然而这个程序却并不需要显式的锁§9.2。clients这个map被限制在了一个独立的goroutine中broadcaster所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例两个东西都是并发安全的。我们会在下一章中更多地讲解约束并发安全以及goroutine中共享变量的含义。
**练习 8.12** 使broadcaster能够将arrival事件通知当前所有的客户端。这需要你在clients集合中以及entering和leaving的channel中记录客户端的名字。

View File

@ -2,6 +2,6 @@
并发程序指同时进行多个任务的程序随着硬件的发展并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的批处理问题——读取数据、计算、写输出现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。
Go语言中的并发程序可以用两种手段来实现。本章讲解goroutine和channel其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。CSP是一种现代的并发编程模型在这种编程模型中值会在不同的运行实例(goroutine)中传递尽管大多数情况下仍然是被限制在单一实例中。第9章覆盖更为传统的并发模型多线程共享内存如果你在其它的主流语言中写过并发程序的话可能会更熟悉一些。第9章也会深入介绍一些并发程序带来的风险和陷阱。
Go语言中的并发程序可以用两种手段来实现。本章讲解goroutine和channel其支持“顺序通信进程”communicating sequential processes或被简称为CSP。CSP是一种现代的并发编程模型在这种编程模型中值会在不同的运行实例goroutine中传递尽管大多数情况下仍然是被限制在单一实例中。第9章覆盖更为传统的并发模型多线程共享内存如果你在其它的主流语言中写过并发程序的话可能会更熟悉一些。第9章也会深入介绍一些并发程序带来的风险和陷阱。
尽管Go对并发的支持是众多强力特性之一但跟踪调试并发程序还是很困难在线性程序中形成的直觉往往还会使我们误入歧途。如果这是读者第一次接触并发推荐稍微多花一些时间来思考这两个章节中的样例。

View File

@ -1,6 +1,6 @@
## 9.1. 竞争条件
在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话)以此类推。在有两个或更多goroutine的程序中每一个goroutine内的语句也是按照既定的顺序去执行的但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话就说明x和y这两个事件是并发的。
在一个线性就是说只有一个goroutine的的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话)以此类推。在有两个或更多goroutine的程序中每一个goroutine内的语句也是按照既定的顺序去执行的但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话就说明x和y这两个事件是并发的。
考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么该类型便是并发安全的。
@ -8,7 +8,7 @@
相反包级别的导出函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine所以修改这些变量“必须”使用互斥条件。
一个函数在并发调用时没法工作的原因太多了,比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。
一个函数在并发调用时没法工作的原因太多了,比如死锁deadlock、活锁livelock和饿死resource starvation。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。
竞争条件指的是程序在多个goroutine交叉执行操作时没有给出正确的结果。竞争条件是很恶劣的一种场景因为这种问题会一直潜伏在你的程序里然后在非常少见的时候蹦出来或许只是会在很大的负载时才会发生又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断。
@ -49,7 +49,7 @@ Alice first Bob first Alice/Bob/Alice
所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易不过无论怎么着客户都不会在意。
但是事实是上面的直觉推断是错误的。第四种可能的结果是事实存在的这种情况下Bob的存款会在Alice存款操作中间在余额被读到(balance + amount)之后,在余额被更新之前(balance = ...)这样会导致Bob的交易丢失。而这是因为Alice的存款操作A1实际上是两个操作的一个序列读取然后写可以称之为A1r和A1w。下面是交叉时产生的问题
但是事实是上面的直觉推断是错误的。第四种可能的结果是事实存在的这种情况下Bob的存款会在Alice存款操作中间在余额被读到balance + amount之后在余额被更新之前balance = ...这样会导致Bob的交易丢失。而这是因为Alice的存款操作A1实际上是两个操作的一个序列读取然后写可以称之为A1r和A1w。下面是交叉时产生的问题
```
Data race
@ -60,11 +60,11 @@ A1w 200 balance = ...
A2 "= 200"
```
在A1r之后balance + amount会被计算为200所以这是A1w会写入的值并不受其它存款操作的干预。最终的余额是$200。银行的账户上的资产比Bob实际的资产多了$100。(译注因为丢失了Bob的存款操作所以其实是说Bob的钱丢了)
在A1r之后balance + amount会被计算为200所以这是A1w会写入的值并不受其它存款操作的干预。最终的余额是$200。银行的账户上的资产比Bob实际的资产多了$100。译注因为丢失了Bob的存款操作所以其实是说Bob的钱丢了。
这个程序包含了一个特定的竞争条件叫作数据竞争。无论任何时候只要有两个goroutine并发访问同一变量且至少其中的一个是写操作的时候就会发生数据竞争。
如果数据竞争的对象是一个比一个机器字(译注32位机器上一个字=4个字节)更大的类型时事情就变得更麻烦了比如interfacestring或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice
如果数据竞争的对象是一个比一个机器字译注32位机器上一个字=4个字节更大的类型时事情就变得更麻烦了比如interfacestring或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice
```go
var x []int
@ -73,7 +73,7 @@ go func() { x = make([]int, 1000000) }()
x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!
```
最后一个语句中的x的值是未定义的其可能是nil或者也可能是一个长度为10的slice也可能是一个长度为1,000,000的slice。但是回忆一下slice的三个组成部分指针(pointer)、长度(length)和容量(capacity)。如果指针是从第一个make调用来而长度从第二个make来x就变成了一个混合体一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置这种情况下难以对值进行预测而且debug也会变成噩梦。这种语义雷区被称为未定义行为对C程序员来说应该很熟悉幸运的是在Go语言里造成的麻烦要比C里小得多。
最后一个语句中的x的值是未定义的其可能是nil或者也可能是一个长度为10的slice也可能是一个长度为1,000,000的slice。但是回忆一下slice的三个组成部分指针pointer、长度length和容量capacity。如果指针是从第一个make调用来而长度从第二个make来x就变成了一个混合体一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置这种情况下难以对值进行预测而且debug也会变成噩梦。这种语义雷区被称为未定义行为对C程序员来说应该很熟悉幸运的是在Go语言里造成的麻烦要比C里小得多。
尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到数据竞争可能会有奇怪的结果。许多程序员甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争比如“互斥条件代价太高”“这个逻辑只是用来做logging”“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争那么在我们的程序中要如何做到呢
@ -112,7 +112,7 @@ func Icon(name string) image.Image { return icons[name] }
上面的例子里icons变量在包初始化阶段就已经被赋值了包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了icons就再也不会被修改。数据结构如果从不被修改或是不变量则是并发安全的无需进行同步。不过显然如果update操作是必要的我们就没法用这种方法比如说银行账户。
第二种避免数据竞争的方法是避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine而聊天服务器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。
第二种避免数据竞争的方法是避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫§8.6的main goroutine是唯一一个能够访问seen map的goroutine而聊天服务器§8.10中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。
由于其它的goroutine不能够直接访问变量它们只能使用一个channel来发送请求给指定的goroutine来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信使用通信来共享数据”。一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的monitor监控goroutine。例如broadcaster goroutine会监控clients map的全部访问。

View File

@ -1,6 +1,6 @@
## 9.2. sync.Mutex互斥锁
在8.6节中我们使用了一个buffered channel作为一个计数信号量来保证最多只有20个goroutine会同时执行HTTP请求。同理我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)
在8.6节中我们使用了一个buffered channel作为一个计数信号量来保证最多只有20个goroutine会同时执行HTTP请求。同理我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量binary semaphore
<u><i>gopl.io/ch9/bank2</i></u>
```go
@ -48,13 +48,13 @@ func Balance() int {
}
```
每次一个goroutine访问bank变量时(这里只有balance余额变量)它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。惯例来说被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符确保在文档里对你的做法进行说明。
每次一个goroutine访问bank变量时这里只有balance余额变量它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。惯例来说被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符确保在文档里对你的做法进行说明。
在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改这个代码段叫做临界区。锁的持有者在其他goroutine获取该锁之前需要调用Unlock。goroutine在结束后释放锁是必要的无论以哪条路径通过函数都需要释放即使是在错误路径中也要记得释放。
上面的bank程序例证了一种通用的并发模式。一系列的导出函数封装了一个或多个变量那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一开始就获取互斥锁并在最后释放锁从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor(这种老式单词的monitor是受"monitor goroutine"的术语启发而来的。两种用法都是一个代理人保证变量被顺序访问)
上面的bank程序例证了一种通用的并发模式。一系列的导出函数封装了一个或多个变量那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一开始就获取互斥锁并在最后释放锁从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor这种老式单词的monitor是受“monitor goroutine”的术语启发而来的。两种用法都是一个代理人保证变量被顺序访问
由于在存款和查询余额函数中的临界区代码这么短--只有一行,没有分支调用--在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中尤其是必须要尽早处理错误并返回的情况下就很难去(靠人)判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星我们用defer来调用Unlock临界区会隐式地延伸到函数作用域的最后这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。
由于在存款和查询余额函数中的临界区代码这么短——只有一行,没有分支调用——在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中尤其是必须要尽早处理错误并返回的情况下就很难去(靠人)判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星我们用defer来调用Unlock临界区会隐式地延伸到函数作用域的最后这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。
```go
func Balance() int {
@ -66,7 +66,7 @@ func Balance() int {
上面的例子里Unlock会在return语句读取完balance的值之后执行所以Balance函数是并发安全的。这带来的另一点好处是我们再也不需要一个本地变量b了。
此外一个deferred Unlock即使在临界区发生panic时依然会执行这对于用recover (§5.10)来恢复的程序来说是很重要的。defer调用只会比显式地调用Unlock成本高那么一点点不过却在很大程度上保证了代码的整洁性。大多数情况下对于并发程序来说代码的整洁性比过度的优化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。
此外一个deferred Unlock即使在临界区发生panic时依然会执行这对于用recover§5.10来恢复的程序来说是很重要的。defer调用只会比显式地调用Unlock成本高那么一点点不过却在很大程度上保证了代码的整洁性。大多数情况下对于并发程序来说代码的整洁性比过度的优化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。
考虑一下下面的Withdraw函数。成功的时候它会正确地减掉余额并返回true。但如果银行记录资金对交易来说不足那么取款就会恢复余额并返回false。
@ -100,9 +100,9 @@ func Withdraw(amount int) bool {
}
```
上面这个例子中Deposit会调用mu.Lock()第二次去获取互斥锁但因为mutex已经锁上了而无法被重入(译注go里没有重入锁关于重入锁的概念请参考java)--也就是说没法对一个已经锁上的mutex来再次上锁--这会导致程序死锁没法继续执行下去Withdraw会永远阻塞下去。
上面这个例子中Deposit会调用mu.Lock()第二次去获取互斥锁但因为mutex已经锁上了而无法被重入译注go里没有重入锁关于重入锁的概念请参考java——也就是说没法对一个已经锁上的mutex来再次上锁——这会导致程序死锁没法继续执行下去Withdraw会永远阻塞下去。
关于Go的mutex不能重入这一点我们有很充分的理由。mutex的目的是确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”但实际上这里对于mutex保护的变量来说不变性还包括其它方面。当一个goroutine获得了一个互斥锁时它会断定这种不变性能够被保持。在其获取并保持锁期间可能会去更新共享变量这样不变性只是短暂地被破坏。然而当其释放锁之后它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量但这种方式没法保证这些变量额外的不变性。(译注:这段翻译有点晕)
关于Go的mutex不能重入这一点我们有很充分的理由。mutex的目的是确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”但实际上这里对于mutex保护的变量来说不变性还包括其它方面。当一个goroutine获得了一个互斥锁时它会断定这种不变性能够被保持。在其获取并保持锁期间可能会去更新共享变量这样不变性只是短暂地被破坏。然而当其释放锁之后它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量但这种方式没法保证这些变量额外的不变性。(译注:这段翻译有点晕。)
一个通用的解决方案是将一个函数分离为多个函数比如我们把Deposit分离成两个一个不导出的函数deposit这个函数假设锁总是会被保持并去做实际的操作另一个是导出的函数Deposit这个函数会调用deposit但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式
@ -136,4 +136,4 @@ func deposit(amount int) { balance += amount }
当然这里的存款deposit函数很小实际上取款Withdraw函数不需要理会对它的调用尽管如此这里的表达还是表明了规则。
封装§6.6用限制一个程序中的意外交互的方式可以使我们获得数据结构的不变性。因为某种原因封装还帮我们获得了并发的不变性。当你使用mutex时确保mutex和其保护的变量没有被导出(在go里也就是小写且不要被大写字母开头的函数访问啦)无论这些变量是包级的变量还是一个struct的字段。
封装§6.6用限制一个程序中的意外交互的方式可以使我们获得数据结构的不变性。因为某种原因封装还帮我们获得了并发的不变性。当你使用mutex时确保mutex和其保护的变量没有被导出在go里也就是小写且不要被大写字母开头的函数访问啦无论这些变量是包级的变量还是一个struct的字段。

View File

@ -2,7 +2,7 @@
在100刀的存款消失时不做记录多少还是会让我们有一些恐慌Bob写了一个程序每秒运行几百次来检查他的银行余额。他会在家在工作中甚至会在他的手机上来运行这个程序。银行注意到这些陡增的流量使得存款和取款有了延时因为所有的余额查询请求是顺序执行的这样会互斥地获得锁并且会暂时阻止其它的goroutine运行。
由于Balance函数只需要读取变量的状态所以我们同时让多个Balance调用并发运行事实上是安全的只要在运行的时候没有存款或者取款操作就行。在这种场景下我们需要一种特殊类型的锁其允许多个只读操作并行执行但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock)Go语言提供的这样的锁是sync.RWMutex
由于Balance函数只需要读取变量的状态所以我们同时让多个Balance调用并发运行事实上是安全的只要在运行的时候没有存款或者取款操作就行。在这种场景下我们需要一种特殊类型的锁其允许多个只读操作并行执行但写操作会完全互斥。这种锁叫作“多读单写”锁multiple readers, single writer lockGo语言提供的这样的锁是sync.RWMutex
```go
var mu sync.RWMutex
@ -18,7 +18,7 @@ Balance函数现在调用了RLock和RUnlock方法来获取和释放一个读取
在这次修改后Bob的余额查询请求就可以彼此并行地执行并且会很快地完成了。锁在更多的时间范围可用并且存款请求也能够及时地被响应了。
RLock只能在临界区共享变量没有任何写入操作时可用。一般来说我们不应该假设逻辑上的只读函数/方法也不会去更新某一些变量。比如一个方法功能是访问一个变量,但它也有可能会同时去给一个内部的计数器+1(译注:可能是记录这个方法的访问次数啥的),或者去更新缓存--使即时的调用能够更快。如果有疑惑的话,请使用互斥锁。
RLock只能在临界区共享变量没有任何写入操作时可用。一般来说我们不应该假设逻辑上的只读函数/方法也不会去更新某一些变量。比如一个方法功能是访问一个变量,但它也有可能会同时去给一个内部的计数器+1(译注:可能是记录这个方法的访问次数啥的),或者去更新缓存——使即时的调用能够更快。如果有疑惑的话,请使用互斥锁。
RWMutex只有当获得锁的大部分goroutine都是读操作而锁在竞争条件下也就是说goroutine们必须等待才能获取到锁的时候RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录所以会让它比一般的无竞争锁的mutex慢一些。

View File

@ -2,7 +2,7 @@
你可能比较纠结为什么Balance方法需要用到互斥条件无论是基于channel还是基于互斥量。毕竟和存款不一样它只由一个简单的操作组成所以不会碰到其它goroutine在其执行“期间”执行其它逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二更重要的是“同步”不仅仅是一堆goroutine执行顺序的问题同样也会涉及到内存的问题。
在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率对内存的写入一般会在每一个处理器中缓冲并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。
在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存local cache。为了效率对内存的写入一般会在每一个处理器中缓冲并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。
考虑一下下面代码片段的可能输出:

View File

@ -6,7 +6,7 @@
var icons map[string]image.Image
```
这个版本的Icon用到了懒初始化(lazy initialization)
这个版本的Icon用到了懒初始化lazy initialization
```go
func loadIcons() {
@ -41,7 +41,7 @@ func loadIcons() {
}
```
因此一个goroutine在检查icons是非空时也并不能就假设这个变量的初始化流程已经走完了(译注可能只是塞了个空map里面的值还没填完也就是说填值的语句都没执行完呢)
因此一个goroutine在检查icons是非空时也并不能就假设这个变量的初始化流程已经走完了译注可能只是塞了个空map里面的值还没填完也就是说填值的语句都没执行完呢
最简单且正确的保证所有goroutine能够观察到loadIcons效果的方式是用一个mutex来同步检查。
@ -87,7 +87,7 @@ func Icon(name string) image.Image {
```
上面的代码有两个临界区。goroutine首先会获取一个读锁查询map然后释放锁。如果条目被找到了(一般情况下)那么会直接返回。如果没有找到那goroutine会获取一个写锁。不释放共享锁的话也没有任何办法来将一个共享锁升级为一个互斥锁所以我们必须重新检查icons变量是否为nil以防止在执行这一段代码的时候icons变量已经被其它gorouine初始化过了。
上面的代码有两个临界区。goroutine首先会获取一个读锁查询map然后释放锁。如果条目被找到了(一般情况下)那么会直接返回。如果没有找到那goroutine会获取一个写锁。不释放共享锁的话也没有任何办法来将一个共享锁升级为一个互斥锁所以我们必须重新检查icons变量是否为nil以防止在执行这一段代码的时候icons变量已经被其它gorouine初始化过了。
上面的模板使我们的程序能够更好的并发但是有一点太复杂且容易出错。幸运的是sync包为我们提供了一个专门的方案来解决这种一次性初始化的问题sync.Once。概念上来讲一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了互斥量用来保护boolean变量和客户端数据结构。Do这个唯一的方法需要接收初始化函数作为其参数。让我们用sync.Once来简化前面的Icon函数吧
@ -101,6 +101,6 @@ func Icon(name string) image.Image {
}
```
每一次对Do(loadIcons)的调用都会锁定mutex并会检查boolean变量(译注Go1.9中会先判断boolean变量是否为1(true)只有不为1才锁定mutex不再需要每次都锁定mutex)。在第一次调用时boolean变量的值是falseDo会调用loadIcons并会将boolean变量设置为true。随后的调用什么都不会做但是mutex同步会保证loadIcons对内存(这里其实就是指icons变量啦)产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话我们能够避免在变量被构建完成之前和其它goroutine共享该变量。
每一次对Do(loadIcons)的调用都会锁定mutex并会检查boolean变量译注Go1.9中会先判断boolean变量是否为1(true)只有不为1才锁定mutex不再需要每次都锁定mutex。在第一次调用时boolean变量的值是falseDo会调用loadIcons并会将boolean变量设置为true。随后的调用什么都不会做但是mutex同步会保证loadIcons对内存这里其实就是指icons变量啦产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话我们能够避免在变量被构建完成之前和其它goroutine共享该变量。
**练习 9.2** 重写2.6.2节中的PopCount的例子使用sync.Once只在第一次需要用到的时候进行初始化。(虽然实际上对PopCount这样很小且高度优化的函数进行同步可能代价没法接受)
**练习 9.2** 重写2.6.2节中的PopCount的例子使用sync.Once只在第一次需要用到的时候进行初始化。虽然实际上对PopCount这样很小且高度优化的函数进行同步可能代价没法接受。)

View File

@ -1,11 +1,11 @@
## 9.6. 竞争条件检测
即使我们小心到不能再小心但在并发程序中犯错还是太容易了。幸运的是Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具竞争检查器(the race detector)
即使我们小心到不能再小心但在并发程序中犯错还是太容易了。幸运的是Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具竞争检查器the race detector
只要在go buildgo run或者go test命令后面加上-race的flag就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外修改版的程序会记录下所有的同步事件比如go语句channel操作以及对`(*sync.Mutex).Lock``(*sync.WaitGroup).Wait`等等的调用。(完整的同步事件集合是在The Go Memory Model文档中有说明该文档是和语言文档放在一起的。译注https://golang.org/ref/mem)
只要在go buildgo run或者go test命令后面加上-race的flag就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外修改版的程序会记录下所有的同步事件比如go语句channel操作以及对`(*sync.Mutex).Lock``(*sync.WaitGroup).Wait`等等的调用。完整的同步事件集合是在The Go Memory Model文档中有说明该文档是和语言文档放在一起的。译注https://golang.org/ref/mem
竞争检查器会检查这些事件会寻找在哪一个goroutine中出现了这样的case例如其读或者写了一个共享变量这个共享变量是被另一个goroutine在没有进行干预同步操作便直接写入的。这种情况也就表明了是对一个共享变量的并发访问即数据竞争。这个工具会打印一份报告内容包含变量身份读取和写入的goroutine中活跃的函数的调用栈。这些信息在定位问题时通常很有用。9.7节中会有一个竞争检查器的实战样例。
竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你的包。
由于需要额外的记录因此构建时加了竞争检测的程序跑起来会慢一些且需要更大的内存即使是这样这些代价对于很多生产环境的工作来说还是可以接受的。对于一些偶发的竞争条件来说让竞争检查器来干活可以节省无数日夜的debugging。(译注多少服务端C和C++程序员为此竞折腰)
由于需要额外的记录因此构建时加了竞争检测的程序跑起来会慢一些且需要更大的内存即使是这样这些代价对于很多生产环境的工作来说还是可以接受的。对于一些偶发的竞争条件来说让竞争检查器来干活可以节省无数日夜的debugging。译注多少服务端C和C++程序员为此竞折腰。)

View File

@ -1,6 +1,6 @@
## 9.7. 示例: 并发的非阻塞缓存
本节中我们会做一个无阻塞的缓存,这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存(memoizing)函数(译注Memoization的定义 memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing.),也就是说,我们需要缓存函数的返回结果,这样在对函数进行调用的时候,我们就只需要一次计算,之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。
本节中我们会做一个无阻塞的缓存,这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存memoizing函数译注Memoization的定义 memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing,也就是说,我们需要缓存函数的返回结果,这样在对函数进行调用的时候,我们就只需要一次计算,之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。
我们将使用下面的httpGetBody函数作为我们需要缓存的函数的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比较大的所以我们尽量避免在不必要的时候反复调用。
@ -54,7 +54,7 @@ func (memo *Memo) Get(key string) (interface{}, error) {
}
```
Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是简单的函数返回的值对儿--一个值和一个错误值。继续下去我们会展示一些Memo的变种不过所有的例子都会遵循上面的这些方面。
Memo实例会记录需要缓存的函数f类型为Func以及缓存内容里面是一个string到result映射的map。每一个result都是简单的函数返回的值对儿——一个值和一个错误值。继续下去我们会展示一些Memo的变种不过所有的例子都会遵循上面的这些方面。
下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get并打印调用延时以及其返回的数据大小的log
@ -71,7 +71,7 @@ for url := range incomingURLs() {
}
```
我们可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。从下面的测试输出我们可以看到URL流包含了一些重复的情况尽管我们第一次对每一个URL的`(*Memo).Get`的调用都会花上几百毫秒但第二次就只需要花1毫秒就可以返回完整的数据了。
我们可以使用测试包第11章的主题来系统地鉴定缓存的效果。从下面的测试输出我们可以看到URL流包含了一些重复的情况尽管我们第一次对每一个URL的`(*Memo).Get`的调用都会花上几百毫秒但第二次就只需要花1毫秒就可以返回完整的数据了。
```
$ go test -v gopl.io/ch9/memo1
@ -113,9 +113,9 @@ n.Wait()
```
这次测试跑起来更快了然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss(缓存未命中),或者命中了缓存但却返回了错误的值,或者甚至会直接崩溃。
这次测试跑起来更快了然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss(缓存未命中),或者命中了缓存但却返回了错误的值,或者甚至会直接崩溃。
但更糟糕的是,有时候这个程序还是能正确的运行(译也就是最让人崩溃的偶发bug)所以我们甚至可能都不会意识到这个程序有bug。但是我们可以使用-race这个flag来运行程序竞争检测器(§9.6)会打印像下面这样的报告:
但更糟糕的是,有时候这个程序还是能正确的运行也就是最让人崩溃的偶发bug所以我们甚至可能都不会意识到这个程序有bug。但是我们可以使用-race这个flag来运行程序竞争检测器§9.6会打印像下面这样的报告:
```
$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1
@ -199,7 +199,7 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
这些修改使性能再次得到了提升但有一些URL被获取了两次。这种情况在两个以上的goroutine同一时刻调用Get来请求同样的URL时会发生。多个goroutine一起查询cache发现没有值然后一起调用f这个慢不拉叽的函数。在得到结果后也都会去更新map。其中一个获得的结果会覆盖掉另一个的结果。
理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个条目包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在条目的结果被设置之后这个channel就会被关闭以向其它goroutine广播(§8.9)去读取该条目内的结果是安全的了。
理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个条目包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在条目的结果被设置之后这个channel就会被关闭以向其它goroutine广播§8.9去读取该条目内的结果是安全的了。
<u><i>gopl.io/ch9/memo4</i></u>
```go
@ -242,7 +242,7 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
}
```
现在Get函数包括下面这些步骤了获取互斥锁来保护共享变量cache map查询map中是否存在指定条目如果没有找到那么分配空间插入一个新条目释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话可以直接从ready channel中读取由于这个读取操作在channel关闭之前一直是阻塞。
现在Get函数包括下面这些步骤了获取互斥锁来保护共享变量cache map查询map中是否存在指定条目如果没有找到那么分配空间插入一个新条目释放互斥锁。如果存在条目的话且其值没有写入完成也就是有其它的goroutine在调用f这个慢函数goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话可以直接从ready channel中读取由于这个读取操作在channel关闭之前一直是阻塞。
如果没有条目的话需要向map中插入一个没有准备好的条目当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。
@ -338,7 +338,7 @@ func (e *entry) deliver(response chan<- result) {
这个例子说明我们无论用上锁,还是通信来建立并发程序都是可行的。
上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注不是说好的golang推崇通信并发么)
上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。译注不是说好的golang推崇通信并发么。
**练习 9.3** 扩展Func类型和`(*Memo).Get`方法支持调用方提供一个可选的done channel使其具备通过该channel来取消整个操作的能力(§8.9)。一个被取消了的Func的调用结果不应该被缓存。
**练习 9.3** 扩展Func类型和`(*Memo).Get`方法支持调用方提供一个可选的done channel使其具备通过该channel来取消整个操作的能力§8.9。一个被取消了的Func的调用结果不应该被缓存。

View File

@ -1,7 +1,7 @@
### 9.8.1. 动态栈
每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费比如对于我们用到的一个只是用来WaitGroup之后关闭channel的goroutine来说。而对于go程序来说同时创建成百上千个goroutine是非常普遍的如果每一个goroutine都需要这么大的栈的话那这么多的goroutine就不太可能了。除去大小的问题之外固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率允许创建更多的线程并且可以允许更深的递归调用不过这两者是没法同时兼备的。
每一个OS线程都有一个固定大小的内存块一般会是2MB来做栈这个栈会用来存储当前正在被调用或挂起指在调用其它函数时的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费比如对于我们用到的一个只是用来WaitGroup之后关闭channel的goroutine来说。而对于go程序来说同时创建成百上千个goroutine是非常普遍的如果每一个goroutine都需要这么大的栈的话那这么多的goroutine就不太可能了。除去大小的问题之外固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率允许创建更多的线程并且可以允许更深的递归调用不过这两者是没法同时兼备的。
相反一个goroutine会以一个很小的栈开始其生命周期一般只需要2KB。一个goroutine的栈和操作系统线程一样会保存其活跃或挂起的函数调用的本地变量但是和OS线程不太一样的是一个goroutine的栈大小并不是固定的栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB比传统的固定大小的线程栈要大得多尽管一般情况下大多goroutine都不需要这么大的栈。
** 练习 9.4:** 创建一个流水线程序支持用channel连接任意数量的goroutine在跑爆内存之前可以创建多少流水线阶段一个变量通过整个流水线需要用多久(这个练习题翻译不是很确定。。)
** 练习 9.4:** 创建一个流水线程序支持用channel连接任意数量的goroutine在跑爆内存之前可以创建多少流水线阶段一个变量通过整个流水线需要用多久(这个练习题翻译不是很确定)

View File

@ -2,7 +2,7 @@
OS线程会被操作系统内核调度。每几毫秒一个硬件计时器会中断处理器这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中检查线程列表并决定下一次哪个线程可以被运行并从内存中恢复该线程的寄存器信息然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度所以从一个线程向另一个“移动”需要完整的上下文切换也就是说保存一个用户线程的状态到内存恢复另一个线程的到寄存器然后更新调度器的数据结构。这几步操作很慢因为其局部性很差需要几次内存访问并且会增加运行的cpu周期。
Go的运行时包含了其自己的调度器这个调度器使用了一些技术手段比如m:n调度因为其会在n个操作系统线程上多工(调度)m个goroutine。Go调度器的工作和内核的调度是相似的但是这个调度器只关注单独的Go程序中的goroutine译注按程序独立
Go的运行时包含了其自己的调度器这个调度器使用了一些技术手段比如m:n调度因为其会在n个操作系统线程上多工(调度)m个goroutine。Go调度器的工作和内核的调度是相似的但是这个调度器只关注单独的Go程序中的goroutine译注按程序独立
和操作系统的线程调度不同的是Go调度器并不是用一个硬件定时器而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep或者被channel调用或者mutex操作阻塞时调度器会使其进入休眠并开始执行另一个goroutine直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文所以重新调度一个goroutine比调度一个线程代价要低得多。

View File

@ -1,6 +1,6 @@
### 9.8.3. GOMAXPROCS
Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数所以在一个有8个核心的机器上时调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时是需要一个对应的操作系统线程的但是GOMAXPROCS并不需要将这几种情况计算在内。
Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数所以在一个有8个核心的机器上时调度器一次会在8个OS线程上去调度GO代码。GOMAXPROCS是前面说的m:n调度中的n。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时是需要一个对应的操作系统线程的但是GOMAXPROCS并不需要将这几种情况计算在内。
你可以用GOMAXPROCS的环境变量来显式地控制这个参数或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果这个程序会无限打印0和1。
@ -20,4 +20,4 @@ $ GOMAXPROCS=2 go run hacker-cliché.go
在第一次执行时最多同时只能有一个goroutine被执行。初始情况下只有main goroutine被执行所以会打印很多1。过了一段时间后GO调度器会将其置为休眠并唤醒另一个goroutine这时候就开始打印很多0了在打印的时候goroutine是被调度到操作系统线程上的。在第二次执行时我们使用了两个操作系统线程所以两个goroutine可以一起被执行以同样的频率交替打印0和1。我们必须强调的是goroutine的调度是受很多因子影响的而runtime也是在不断地发展演进的所以这里的你实际得到的结果可能会因为版本的不同而与我们运行的结果有所不同。
** 练习9.6:** 测试一下计算密集型的并发程序(练习8.5那样的)会被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少你的电脑CPU有多少个核心
** 练习9.6:** 测试一下计算密集型的并发程序练习8.5那样的)会被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少你的电脑CPU有多少个核心

View File

@ -1,10 +1,10 @@
### 9.8.4. Goroutine没有ID号
在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份(id)并且这个身份信息可以以一个普通值的形式被很容易地获取到典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易只需要以线程的id作为key的一个map就可以解决问题每一个线程以其id就能从中获取到值且和其它线程互不冲突。
在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份id并且这个身份信息可以以一个普通值的形式被很容易地获取到典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易只需要以线程的id作为key的一个map就可以解决问题每一个线程以其id就能从中获取到值且和其它线程互不冲突。
goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之由于thread-local storage总是会被滥用。比如说一个web server是用一种支持tls的语言实现的而非常普遍的是很多函数会去寻找HTTP请求的信息这代表它们就是去其存储层(这个存储层有可能是tls)查找的。这就像是那些过分依赖全局变量的程序一样会导致一种非健康的“距离外行为”在这种行为下一个函数的行为可能并不仅由自己的参数所决定而是由其所运行在的线程所决定。因此如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。
goroutine没有可以被程序员获取到的身份id的概念。这一点是设计上故意而为之由于thread-local storage总是会被滥用。比如说一个web server是用一种支持tls的语言实现的而非常普遍的是很多函数会去寻找HTTP请求的信息这代表它们就是去其存储层这个存储层有可能是tls查找的。这就像是那些过分依赖全局变量的程序一样会导致一种非健康的“距离外行为”在这种行为下一个函数的行为可能并不仅由自己的参数所决定而是由其所运行在的线程所决定。因此如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。
Go鼓励更为简单的模式这种模式下参数(译注外部显式参数和内部显式参数。tls 中的内容算是"外部"隐式参数)对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。
Go鼓励更为简单的模式这种模式下参数译注外部显式参数和内部显式参数。tls 中的内容算是"外部"隐式参数)对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。
你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中我们会回顾一些之前的实例和工具支持我们写出更大规模的程序如何将一个工程组织成一系列的包如何获取构建测试性能测试剖析写文档并且将这些包分享出去。