Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Cloud 2018-06-13 15:55:15 +08:00
commit 23c3d56483
82 changed files with 216 additions and 222 deletions

View File

@ -9,6 +9,6 @@
# 译文授权
除特别注明外, 本站内容均采用[知识共享-署名(CC-BY) 3.0协议](http://creativecommons.org/licenses/by/3.0/)授权, 代码遵循[Go项目的BSD协议](http://golang.org/LICENSE)授权.
除特别注明外本站内容均采用[知识共享-署名(CC-BY) 3.0协议](http://creativecommons.org/licenses/by/3.0/)授权,代码遵循[Go项目的BSD协议](http://golang.org/LICENSE)授权。
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="./images/by-nc-sa-4.0-88x31.png"></img></a>

View File

@ -1,6 +1,6 @@
## 附录C译文授权
除特别注明外, 本站内容均采用[知识共享-署名(CC-BY) 3.0协议](http://creativecommons.org/licenses/by/3.0/)授权, 代码遵循[Go项目的BSD协议](http://golang.org/LICENSE)授权.
除特别注明外本站内容均采用[知识共享-署名(CC-BY) 3.0协议](http://creativecommons.org/licenses/by/3.0/)授权,代码遵循[Go项目的BSD协议](http://golang.org/LICENSE)授权。
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="../images/by-nc-sa-4.0-88x31.png"></img></a>

View File

@ -12,7 +12,7 @@ Go语言有时候被描述为“C类似语言”或者是“21世纪的C语
Go语言的另一支祖先带来了Go语言区别其他语言的重要特性灵感来自于贝尔实验室的[Tony Hoare](https://en.wikipedia.org/wiki/Tony_Hoare)于1978年发表的鲜为外界所知的关于并发研究的基础文献 *顺序通信进程* *[communicating sequential processes][CSP]* ,缩写为[CSP][CSP]。在[CSP][CSP]中,程序是一组中间没有共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。不过[Tony Hoare](https://en.wikipedia.org/wiki/Tony_Hoare)的[CSP][CSP]只是一个用于描述并发性基本概念的描述语言,并不是一个可以编写可执行程序的通用编程语言。
接下来Rob Pike和其他人开始不断尝试将[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)引入实际的编程语言中。他们第一次尝试引入[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)特性的编程语言叫[Squeak](http://doc.cat-v.org/bell_labs/squeak/)(老鼠间交流的语言),是一个提供鼠标和键盘事件处理的编程语言,它的管道是静态创建的。然后是改进版的[Newsqueak](http://doc.cat-v.org/bell_labs/squeak/)语言提供了类似C语言语句和表达式的语法和类似[Pascal][Pascal]语言的推导语法。Newsqueak是一个带垃圾回收的纯函数式语言它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建的属于第一类值, 可以保存到变量中。
接下来Rob Pike和其他人开始不断尝试将[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)引入实际的编程语言中。他们第一次尝试引入[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)特性的编程语言叫[Squeak](http://doc.cat-v.org/bell_labs/squeak/)(老鼠间交流的语言),是一个提供鼠标和键盘事件处理的编程语言,它的管道是静态创建的。然后是改进版的[Newsqueak](http://doc.cat-v.org/bell_labs/squeak/)语言提供了类似C语言语句和表达式的语法和类似[Pascal][Pascal]语言的推导语法。Newsqueak是一个带垃圾回收的纯函数式语言它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建的属于第一类值可以保存到变量中。
在Plan9操作系统中这些优秀的想法被吸收到了一个叫[Alef][Alef]的编程语言中。Alef试图将Newsqueak语言改造为系统编程语言但是因为缺少垃圾回收机制而导致并发编程很痛苦。译注在Alef之后还有一个叫[Limbo][Limbo]的编程语言Go语言从其中借鉴了很多特性。 具体请参考Pike的讲稿http://talks.golang.org/2012/concurrency.slide#9

View File

@ -12,4 +12,4 @@ Go语言有足够的类型系统以避免动态语言中那些粗心的类型错
Go语言鼓励当代计算机系统设计的原则特别是局部的重要性。它的内置数据类型和大多数的准库数据结构都经过精心设计而避免显式的初始化或隐式的构造函数因为很少的内存分配和内存初始化代码被隐藏在库代码中了。Go语言的聚合类型结构体和数组可以直接操作它们的元素只需要更少的存储空间、更少的内存写操作而且指针操作比其他间接操作的语言也更有效率。由于现代计算机是一个并行的机器Go语言提供了基于CSP的并发特性支持。Go语言的动态栈使得轻量级线程goroutine的初始栈可以很小因此创建一个goroutine的代价很小创建百万级的goroutine完全是可行的。
Go语言的标准库通常被称为语言自带的电池提供了清晰的构建模块和公共接口包含I/O操作、文本处理、图像、密码学、网络和分布式应用程序等并支持许多标准化的文件格式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释从而最终简化程序的逻辑而且每个Go程序结构都是如此的相似因此Go程序也很容易学习。使用Go语言自带工具构建Go语言项目只需要使用文件名和标识符名称, 一个偶尔的特殊注释来确定所有的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等Go语言源代码本身就包含了构建规范。
Go语言的标准库通常被称为语言自带的电池提供了清晰的构建模块和公共接口包含I/O操作、文本处理、图像、密码学、网络和分布式应用程序等并支持许多标准化的文件格式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释从而最终简化程序的逻辑而且每个Go程序结构都是如此的相似因此Go程序也很容易学习。使用Go语言自带工具构建Go语言项目只需要使用文件名和标识符名称一个偶尔的特殊注释来确定所有的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等Go语言源代码本身就包含了构建规范。

View File

@ -8,15 +8,15 @@
第一章到第五章是基础部分主流命令式编程语言这部分都类似。个别之处Go语言有自己特色的语法和风格但是大多数程序员能很快适应。其余章节是Go语言特有的方法、接口、并发、包、测试和反射等语言特性。
Go语言的面向对象机制与一般语言不同。它没有类层次结构甚至可以说没有类仅仅通过组合而不是继承简单的对象来构建复杂的对象。方法不仅可以定义在结构体上, 而且, 可以定义在任何用户自定义的类型上;并且, 具体类型和抽象类型(接口)之间的关系是隐式的,所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论,接口在第七章讨论。
Go语言的面向对象机制与一般语言不同。它没有类层次结构甚至可以说没有类仅仅通过组合而不是继承简单的对象来构建复杂的对象。方法不仅可以定义在结构体上,而且,可以定义在任何用户自定义的类型上;并且,具体类型和抽象类型(接口)之间的关系是隐式的,所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论,接口在第七章讨论。
第八章讨论了基于顺序通信进程(CSP)概念的并发编程使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。
第八章讨论了基于顺序通信进程CSP概念的并发编程使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。
第十章描述了包机制和包的组织结构。这一章还展示了如何有效地利用Go自带的工具使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。
第十一章讨论了单元测试Go语言的工具和标准库中集成了轻量级的测试功能避免了强大但复杂的测试框架。测试库提供了一些基本构件必要时可以用来构建复杂的测试构件。
第十二章讨论了反射一种程序在运行期间审视自己的能力。反射是一个强大的编程工具不过要谨慎地使用这一章利用反射机制实现一些重要的Go语言库函数, 展示了反射的强大用法。第十三章解释了底层编程的细节在必要时可以使用unsafe包绕过Go语言安全的类型系统。
第十二章讨论了反射一种程序在运行期间审视自己的能力。反射是一个强大的编程工具不过要谨慎地使用这一章利用反射机制实现一些重要的Go语言库函数展示了反射的强大用法。第十三章解释了底层编程的细节在必要时可以使用unsafe包绕过Go语言安全的类型系统。
每一章都有一些练习题你可以用来测试你对Go的理解你也可以探讨书中这些例子的扩展和替代。
@ -38,4 +38,4 @@ $ go version
go version go1.5 linux/amd64
```
如果使用其他的操作系统, 请参考 https://golang.org/doc/install 提供的说明安装。
如果使用其他的操作系统请参考 https://golang.org/doc/install 提供的说明安装。

View File

@ -1,6 +1,6 @@
## 1.1. Hello, World
我们以现已成为传统的“hello world”案例来开始吧, 这个例子首次出现于1978年出版的C语言圣经[《The C Programming Language》](http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html)译注本书作者之一Brian W. Kernighan也是《The C Programming Language》一书的作者。C语言是直接影响Go语言设计的语言之一。这个例子体现了Go语言一些核心理念。
我们以现已成为传统的“hello world”案例来开始吧这个例子首次出现于1978年出版的C语言圣经[《The C Programming Language》](http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html)译注本书作者之一Brian W. Kernighan也是《The C Programming Language》一书的作者。C语言是直接影响Go语言设计的语言之一。这个例子体现了Go语言一些核心理念。
<u><i>gopl.io/ch1/helloworld</i></u>
```go
@ -40,7 +40,7 @@ $ ./helloworld
Hello, 世界
```
本书中, 所有示例代码上都有一行标记,利用这些标记, 可以从[gopl.io](http://gopl.io)网站上本书源码仓库里获取代码:
本书中所有示例代码上都有一行标记,利用这些标记可以从[gopl.io](http://gopl.io)网站上本书源码仓库里获取代码:
```
gopl.io/ch1/helloworld
@ -48,25 +48,26 @@ gopl.io/ch1/helloworld
执行 `go get gopl.io/ch1/helloworld` 命令就会从网上获取代码并放到对应目录中需要先安装Git或Hg之类的版本管理工具并将对应的命令添加到PATH环境变量中。序言已经提及需要先设置好GOPATH环境变量下载的代码会放在`$GOPATH/src/gopl.io/ch1/helloworld`目录。2.6和10.7节有这方面更详细的介绍。
来讨论下程序本身。Go语言的代码通过**包**package组织包类似于其它语言里的库libraries或者模块modules。一个包由位于单个目录下的一个或多个.go源代码文件组成, 目录定义包的作用。每个源文件都以一条`package`声明语句开始,这个例子里就是`package main`, 表示该文件属于哪个包紧跟着一系列导入import的包之后是存储在这个文件里的程序语句。
来讨论下程序本身。Go语言的代码通过**包**package组织包类似于其它语言里的库libraries或者模块modules。一个包由位于单个目录下的一个或多个.go源代码文件组成目录定义包的作用。每个源文件都以一条`package`声明语句开始,这个例子里就是`package main`表示该文件属于哪个包紧跟着一系列导入import的包之后是存储在这个文件里的程序语句。
Go的标准库提供了100多个包以支持常见功能如输入、输出、排序以及文本处理。比如`fmt`包,就含有格式化输出、接收输入的函数。`Println`是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。
`main`包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在`main`里的`main` *函数* 也很特殊它是整个程序执行时的入口译注C系语言差不多都这样。`main`函数所做的事情就是程序做的。当然了,`main`函数一般调用其它包里的函数完成很多工作, 比如, `fmt.Println`
`main`包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在`main`里的`main` *函数* 也很特殊它是整个程序执行时的入口译注C系语言差不多都这样。`main`函数所做的事情就是程序做的。当然了,`main`函数一般调用其它包里的函数完成很多工作(如:`fmt.Println`
必须告诉编译器源文件需要哪些包,这就是跟随在`package`声明后面的`import`声明扮演的角色。hello world例子只用到了一个包大多数程序需要导入多个包。
必须恰当导入需要的包缺少了必要的包或者导入了不需要的包程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包译注Go语言编译过程没有警告信息争议特性之一
`import`声明必须跟在文件的`package`声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字`func`, `var`, `const`, `type`定义)。这些内容的声明顺序并不重要(译注:最好还是定一下规范)。这个例子的程序已经尽可能短了,只声明了一个函数, 其中只调用了一个其他函数。为了节省篇幅,有些时候, 示例程序会省略`package`和`import`声明,但是,这些声明在源代码里有,并且必须得有才能编译。
`import`声明必须跟在文件的`package`声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字`func`、`var`、`const`、`type`定义)。这些内容的声明顺序并不重要(译注:最好还是定一下规范)。这个例子的程序已经尽可能短了,只声明了一个函数其中只调用了一个其他函数。为了节省篇幅,有些时候示例程序会省略`package`和`import`声明,但是,这些声明在源代码里有,并且必须得有才能编译。
一个函数的声明由`func`关键字、函数名、参数列表、返回值列表(这个例子里的`main`函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成。第五章进一步考察函数。
Go语言不需要在语句或者声明的末尾添加分号除非一行上有多条语句。实际上编译器会主动把特定符号后的换行符转换为分号, 因此换行符添加的位置会影响Go代码的正确解析译注比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字`break`、`continue`、`fallthrough`或`return`中的一个、运算符和分隔符`++`、`--`、`)`、`]`或`}`中的一个)。举个例子, 函数的左括号`{`必须和`func`函数声明在同一行上, 且位于末尾,不能独占一行,而在表达式`x + y`中,可在`+`后换行,不能在`+`前换行(译注:以+结尾的话不会被插入分号分隔符但是以x结尾的话则会被分号分隔符从而导致编译错误
Go语言不需要在语句或者声明的末尾添加分号除非一行上有多条语句。实际上编译器会主动把特定符号后的换行符转换为分号因此换行符添加的位置会影响Go代码的正确解析译注比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字`break`、`continue`、`fallthrough`或`return`中的一个、运算符和分隔符`++`、`--`、`)`、`]`或`}`中的一个)。举个例子,函数的左括号`{`必须和`func`函数声明在同一行上,且位于末尾,不能独占一行,而在表达式`x + y`中,可在`+`后换行,不能在`+`前换行(译注:以+结尾的话不会被插入分号分隔符但是以x结尾的话则会被分号分隔符从而导致编译错误
Go语言在代码格式上采取了很强硬的态度。`gofmt`工具把代码格式化为标准格式译注这个格式化工具没有任何可以调整代码格式的参数Go语言就是这么任性并且`go`工具中的`fmt`子命令会对指定包, 否则默认为当前目录, 中所有.go源文件应用`gofmt`命令。本书中的所有代码都被gofmt过。你也应该养成格式化自己的代码的习惯。以法令方式规定标准的代码格式可以避免无尽的无意义的琐碎争执译注也导致了Go语言的TIOBE排名较低因为缺少撕逼的话题。更重要的是这样可以做多种自动源码转换如果放任Go语言代码格式这些转换就不大可能了。
Go语言在代码格式上采取了很强硬的态度。`gofmt`工具把代码格式化为标准格式译注这个格式化工具没有任何可以调整代码格式的参数Go语言就是这么任性并且`go`工具中的`fmt`子命令会对指定包,否则默认为当前目录中所有.go源文件应用`gofmt`命令。本书中的所有代码都被gofmt过。你也应该养成格式化自己的代码的习惯。以法令方式规定标准的代码格式可以避免无尽的无意义的琐碎争执译注也导致了Go语言的TIOBE排名较低因为缺少撕逼的话题。更重要的是这样可以做多种自动源码转换如果放任Go语言代码格式这些转换就不大可能了。
很多文本编辑器都可以配置为保存文件时自动执行`gofmt`,这样你的源代码总会被恰当地格式化。还有个相关的工具,`goimports`,可以根据代码需要,自动地添加或删除`import`声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:
很多文本编辑器都可以配置为保存文件时自动执行`gofmt`,这样你的源代码总会被恰当地格式化。还有个相关的工具,`goimports`,可以根据代码需要, 自动地添加或删除`import`声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:
```
$ go get golang.org/x/tools/cmd/goimports
```

View File

@ -1,14 +1,14 @@
## 1.2. 命令行参数
大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是, 程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会讨论其中几个输入源,首先是命令行参数。
大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会讨论其中几个输入源,首先是命令行参数。
`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:]。
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:]。
下面是Unix里echo命令的一份实现echo把它的命令行参数打印成一行。程序导入了两个包用括号把它们括起来写成列表形式, 而没有分开写成独立的`import`声明。两种形式都合法列表形式习惯上用得多。包导入顺序并不重要gofmt工具格式化时按照字母顺序对包名排序。示例有多个版本时我们会对示例编号, 这样可以明确当前正在讨论的是哪个。)
下面是Unix里echo命令的一份实现echo把它的命令行参数打印成一行。程序导入了两个包用括号把它们括起来写成列表形式而没有分开写成独立的`import`声明。两种形式都合法列表形式习惯上用得多。包导入顺序并不重要gofmt工具格式化时按照字母顺序对包名排序。示例有多个版本时我们会对示例编号这样可以明确当前正在讨论的是哪个。)
<u><i>gopl.io/ch1/echo1</i></u>
```go
@ -46,7 +46,7 @@ sep + os.Args[i]
s += sep + os.Args[i]
```
是一条*赋值语句*, 将s的旧值跟sep与os.Args[i]连接后赋值回s等价于
是一条*赋值语句*将s的旧值跟sep与os.Args[i]连接后赋值回s等价于
```go
s = s + sep + os.Args[i]
@ -56,7 +56,7 @@ s = s + sep + os.Args[i]
echo程序可以每循环一次输出一个参数这个版本却是不断地把新文本追加到末尾来构造字符串。字符串s开始为空即值为""每次循环会添加一些文本第一次迭代之后还会再插入一个空格因此循环结束时每个参数中间都有一个空格。这是一种二次加工quadratic process当参数数量庞大时开销很大但是对于echo这种情形不大可能出现。本章会介绍echo的若干改进版下一章解决低效问题。
循环索引变量i在for循环的第一部分中定义。符号`:=`是*短变量声明*short variable declaration的一部分, 这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。下一章有这方面更多说明。
循环索引变量i在for循环的第一部分中定义。符号`:=`是*短变量声明*short variable declaration的一部分这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。下一章有这方面更多说明。
自增语句`i++`给`i`加1这和`i += 1`以及`i = i + 1`都是等价的。对应的还有`i--`给`i`减1。它们是语句而不像C系的其它语言那样是表达式。所以`j = i++`非法,而且++和--都只能放在变量名后面,因此`--i`也非法。
@ -68,7 +68,7 @@ for initialization; condition; post {
}
```
for循环三个部分不需括号包围。大括号强制要求, 左大括号必须和*post*语句在同一行。
for循环三个部分不需括号包围。大括号强制要求左大括号必须和*post*语句在同一行。
*initialization*语句是可选的,在循环开始前执行。*initalization*如果存在,必须是一条*简单语句*simple statement短变量声明、自增语句、赋值语句或函数调用。`condition`是一个布尔表达式boolean expression其值在每次循环迭代开始时计算。如果为`true`则执行循环体语句。`post`语句在循环体执行结束后执行,之后再次对`condition`求值。`condition`值为`false`时,循环结束。
@ -90,9 +90,9 @@ for {
}
```
这就变成一个无限循环,尽管如此,还可以用其他方式终止循环, 如一条`break`或`return`语句。
这就变成一个无限循环,尽管如此,还可以用其他方式终止循环如一条`break`或`return`语句。
`for`循环的另一种形式, 在某种数据类型的区间range上遍历如字符串或切片。`echo`的第二版本展示了这种形式:
`for`循环的另一种形式在某种数据类型的区间range上遍历如字符串或切片。`echo`的第二版本展示了这种形式:
<u><i>gopl.io/ch1/echo2</i></u>
```go
@ -114,9 +114,9 @@ func main() {
}
```
每次循环迭代,`range`产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但`range`的语法要求, 要处理元素, 必须处理索引。一种思路是把索引赋值给一个临时变量, 如`temp`, 然后忽略它的值但Go语言不允许使用无用的局部变量local variables因为这会导致编译错误。
每次循环迭代,`range`产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但`range`的语法要求,要处理元素,必须处理索引。一种思路是把索引赋值给一个临时变量(如`temp`然后忽略它的值但Go语言不允许使用无用的局部变量local variables因为这会导致编译错误。
Go语言中这种情况的解决方法是用`空标识符`blank identifier即`_`(也就是下划线)。空标识符可用于任何语法需要变量名但程序逻辑不需要的时候, 例如, 在循环里,丢弃不需要的循环索引, 保留元素值。大多数的Go程序员都会像上面这样使用`range`和`_`写`echo`程序因为隐式地而非显式地索引os.Args容易写对。
Go语言中这种情况的解决方法是用`空标识符`blank identifier即`_`(也就是下划线)。空标识符可用于任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。大多数的Go程序员都会像上面这样使用`range`和`_`写`echo`程序因为隐式地而非显式地索引os.Args容易写对。
`echo`的这个版本使用一条短变量声明来声明并初始化`s`和`seps`,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价:
@ -129,7 +129,7 @@ var s string = ""
用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。
如前文所述每次循环迭代字符串s的内容都会更新。`+=`连接原字符串、空格和下个参数,产生新字符串, 并把它赋值给`s`。`s`原来的内容已经不再使用,将在适当时机对它进行垃圾回收。
如前文所述每次循环迭代字符串s的内容都会更新。`+=`连接原字符串、空格和下个参数,产生新字符串并把它赋值给`s`。`s`原来的内容已经不再使用,将在适当时机对它进行垃圾回收。
如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用`strings`包的`Join`函数:

View File

@ -37,7 +37,7 @@ func main() {
(译注:从功能和实现上说,`Go`的`map`类似于`Java`语言中的`HashMap`Python语言中的`dict``Lua`语言中的`table`,通常使用`hash`实现。遗憾的是,对于该词的翻译并不统一,数学界术语为`映射`,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。)
每次`dup`读取一行输入,该行被当做`map`,其对应的值递增。`counts[input.Text()]++`语句等价下面两句:
每次`dup`读取一行输入,该行被当做键存入`map`,其对应的值递增。`counts[input.Text()]++`语句等价下面两句:
```go
line := input.Text()
@ -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,11 +76,11 @@ 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类型这个类型支持把输出结果写到很多目标很快我们就可以看到例子。
内层循环设置两个偏振值。x轴偏振使用sin函数。y轴偏振也是正弦波但其相对x轴的偏振是一个0-3的随机值初始偏振值是一个零值随着动画的每一帧逐渐增加。循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x, y)点来染黑色。
内层循环设置两个偏振值。x轴偏振使用sin函数。y轴偏振也是正弦波但其相对x轴的偏振是一个0-3的随机值初始偏振值是一个零值随着动画的每一帧逐渐增加。循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x,y)点来染黑色。
main函数调用lissajous函数用它来向标准输出流打印信息所以下面这个命令会像图1.1中产生一个GIF动画。

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)这个程序中我们用main函数来接收所有fetch函数传回的字符串可以避免在goroutine异步执行还没有完成时main函数提前退出
当一个goroutine尝试在一个channel上做send或者receive操作时这个goroutine会阻塞在调用处直到另一个goroutine从这个channel里接收或者写入值这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中每一个fetch函数在执行时都会往channel里发送一个值ch <- expression主函数负责接收这些值<-ch这个程序中我们用main函数来接收所有fetch函数传回的字符串可以避免在goroutine异步执行还没有完成时main函数提前退出
**练习 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

@ -1,5 +1,5 @@
# 第一章 入门
本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序希望可以帮你尽快入门, 写出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程序涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然第一章不会解释细枝末节但用这些程序来学习一门新语言还是很有效的。
本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序希望可以帮你尽快入门写出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程序涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然第一章不会解释细枝末节但用这些程序来学习一门新语言还是很有效的。
学习一门新语言时,会有一种自然的倾向, 按照自己熟悉的语言的套路写新语言程序。学习Go语言的过程中请警惕这种想法尽量别这么做。我们会演示怎么写好Go语言程序所以请使用本书的代码作为你自己写程序时的指南。
学习一门新语言时,会有一种自然的倾向按照自己熟悉的语言的套路写新语言程序。学习Go语言的过程中请警惕这种想法尽量别这么做。我们会演示怎么写好Go语言程序所以请使用本书的代码作为你自己写程序时的指南。

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

@ -4,7 +4,7 @@
由于每个目录只包含一个包因此每个对应可执行程序或者叫Unix术语中的命令的包会要求放到一个独立的目录中。这些目录有时候会放在名叫cmd目录的子目录下面例如用于提供Go文档服务的golang.org/x/tools/cmd/godoc命令就是放在cmd子目录§10.7.4)。
每个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径名指定,相对路径必须以`.`或`..`开头。如果没有指定参数,那么默认指定为当前目录对应的包。 下面的命令用于构建同一个包, 虽然它们的写法各不相同:
每个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径名指定,相对路径必须以`.`或`..`开头。如果没有指定参数,那么默认指定为当前目录对应的包。下面的命令用于构建同一个包,虽然它们的写法各不相同:
```
$ cd $GOPATH/src/gopl.io/ch1/helloworld

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

@ -41,7 +41,7 @@ func echo(newline bool, sep string, args []string) error {
}
```
在测试中我们可以用各种参数和标志调用echo函数然后检测它的输出是否正确, 我们通过增加参数来减少echo函数对全局变量的依赖。我们还增加了一个全局名为out的变量来替代直接使用os.Stdout这样测试代码可以根据需要将out修改为不同的对象以便于检查。下面就是echo_test.go文件中的测试代码
在测试中我们可以用各种参数和标志调用echo函数然后检测它的输出是否正确我们通过增加参数来减少echo函数对全局变量的依赖。我们还增加了一个全局名为out的变量来替代直接使用os.Stdout这样测试代码可以根据需要将out修改为不同的对象以便于检查。下面就是echo_test.go文件中的测试代码
```Go
package 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,8 +1,8 @@
## 12.2. reflect.Type和reflect.Value
## 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形式返回其动态类型:
函数 reflect.TypeOf 接受任意的 interface{} 类型,并以 reflect.Type 形式返回其动态类型:
```Go
t := reflect.TypeOf(3) // a reflect.Type
@ -10,22 +10,22 @@ fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"
```
其中 TypeOf(3) 调用将值 3 传给 interface{} 参数. 回到 7.5节 的将一个具体的值转为接口类型会有一个隐式的接口转换操作, 它会创建一个包含两个信息的接口值: 操作数的动态类型(这里是int)和它的动态的值(这里是3).
其中 TypeOf(3) 调用将值 3 传给 interface{} 参数。回到 7.5节 的将一个具体的值转为接口类型会有一个隐式的接口转换操作,它会创建一个包含两个信息的接口值:操作数的动态类型(这里是 int和它的动态的值这里是 3
因为 reflect.TypeOf 返回的是一个动态类型的接口值, 它总是返回具体的类型. 因此, 下面的代码将打印 "*os.File" 而不是 "io.Writer". 稍后, 我们将看到能够表达接口类型的 reflect.Type.
因为 reflect.TypeOf 返回的是一个动态类型的接口值,它总是返回具体的类型。因此,下面的代码将打印 "*os.File" 而不是 "io.Writer"。稍后,我们将看到能够表达接口类型的 reflect.Type。
```Go
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File"
```
要注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的. 因为打印一个接口的动态类型对于调试和日志是有帮助的, fmt.Printf 提供了一个缩写 %T 参数, 内部使用 reflect.TypeOf 来输出:
要注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的。因为打印一个接口的动态类型对于调试和日志是有帮助的, fmt.Printf 提供了一个缩写 %T 参数,内部使用 reflect.TypeOf 来输出:
```Go
fmt.Printf("%T\n", 3) // "int"
```
reflect 包中另一个重要的类型是 Value. 一个 reflect.Value 可以装载任意类型的值. 函数 reflect.ValueOf 接受任意的 interface{} 类型, 并返回一个装载着其动态值的 reflect.Value. 和 reflect.TypeOf 类似, reflect.ValueOf 返回的结果也是具体的类型, 但是 reflect.Value 也可以持有一个接口值.
reflect 包中另一个重要的类型是 Value。一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型,并返回一个装载着其动态值的 reflect.Value。和 reflect.TypeOf 类似reflect.ValueOf 返回的结果也是具体的类型,但是 reflect.Value 也可以持有一个接口值。
```Go
v := reflect.ValueOf(3) // a reflect.Value
@ -34,16 +34,16 @@ fmt.Printf("%v\n", v) // "3"
fmt.Println(v.String()) // NOTE: "<int Value>"
```
和 reflect.Type 类似, reflect.Value 也满足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串, 否则 String 方法只返回其类型. 而使用 fmt 包的 %v 标志参数会对 reflect.Values 特殊处理.
和 reflect.Type 类似reflect.Value 也满足 fmt.Stringer 接口,但是除非 Value 持有的是字符串,否则 String 方法只返回其类型。而使用 fmt 包的 %v 标志参数会对 reflect.Values 特殊处理。
对 Value 调用 Type 方法将返回具体类型所对应的 reflect.Type:
对 Value 调用 Type 方法将返回具体类型所对应的 reflect.Type
```Go
t := v.Type() // a reflect.Type
fmt.Println(t.String()) // "int"
```
reflect.ValueOf 的逆操作是 reflect.Value.Interface 方法. 它返回一个 interface{} 类型,装载着与 reflect.Value 相同的具体值:
reflect.ValueOf 的逆操作是 reflect.Value.Interface 方法。它返回一个 interface{} 类型,装载着与 reflect.Value 相同的具体值:
```Go
v := reflect.ValueOf(3) // a reflect.Value
@ -52,9 +52,9 @@ i := x.(int) // an int
fmt.Printf("%d\n", i) // "3"
```
reflect.Value 和 interface{} 都能装载任意的值. 所不同的是, 一个空的接口隐藏了值内部的表示方式和所有方法, 因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样), 内部值我们没法访问. 相比之下, 一个 Value 则有很多方法来检查其内容, 无论它的具体类型是什么. 让我们再次尝试实现我们的格式化函数 format.Any.
reflect.Value 和 interface{} 都能装载任意的值。所不同的是,一个空的接口隐藏了值内部的表示方式和所有方法,因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样),内部值我们没法访问。相比之下,一个 Value 则有很多方法来检查其内容,无论它的具体类型是什么。让我们再次尝试实现我们的格式化函数 format.Any。
我们使用 reflect.Value 的 Kind 方法来替代之前的类型 switch. 虽然还是有无穷多的类型, 但是它们的kinds类型却是有限的: Bool, String 和 所有数字类型的基础类型; Array 和 Struct 对应的聚合类型; Chan, Func, Ptr, Slice, 和 Map 对应的引用类型; interface 类型; 还有表示空值的 Invalid 类型. (空的 reflect.Value 的 kind 即为 Invalid.)
我们使用 reflect.Value 的 Kind 方法来替代之前的类型 switch。虽然还是有无穷多的类型,但是它们的 kinds 类型却是有限的Bool、String 和 所有数字类型的基础类型Array 和 Struct 对应的聚合类型Chan、Func、Ptr、Slice 和 Map 对应的引用类型interface 类型;还有表示空值的 Invalid 类型。(空的 reflect.Value 的 kind 即为 Invalid。
<u><i>gopl.io/ch12/format</i></u>
```Go
@ -95,7 +95,7 @@ func formatAtom(v reflect.Value) string {
}
```
到目前为止, 我们的函数将每个值视作一个不可分割没有内部结构的物品, 因此它叫 formatAtom. 对于聚合类型(结构体和数组)和接口,只是打印值的类型, 对于引用类型(channels, functions, pointers, slices, 和 maps), 打印类型和十六进制的引用地址. 虽然还不够理想, 但是依然是一个重大的进步, 并且 Kind 只关心底层表示, format.Any 也支持具名类型. 例如:
到目前为止,我们的函数将每个值视作一个不可分割没有内部结构的物品,因此它叫 formatAtom。对于聚合类型结构体和数组和接口只是打印值的类型对于引用类型channels、functions、pointers、slices 和 maps打印类型和十六进制的引用地址。虽然还不够理想但是依然是一个重大的进步并且 Kind 只关心底层表示format.Any 也支持具名类型。例如:
```Go
var x int64 = 1

View File

@ -157,7 +157,7 @@ Display("os.Stderr", os.Stderr)
// (*(*os.Stderr).file).nepipe = 0
```
可以看出反射能够访问到结构体中未导出的成员。需要当心的是这个例子的输出在不同操作系统上可能是不同的并且随着标准库的发展也可能导致结果不同。这也是将这些成员定义为私有成员的原因之一我们甚至可以用Display函数来显示reflect.Value 的内部构造(在这里设置为`*os.File`的类型描述体)。`Display("rV", reflect.ValueOf(os.Stderr))`调用的输出如下,当然不同环境得到的结果可能有差异:
可以看出反射能够访问到结构体中未导出的成员。需要当心的是这个例子的输出在不同操作系统上可能是不同的并且随着标准库的发展也可能导致结果不同。这也是将这些成员定义为私有成员的原因之一我们甚至可以用Display函数来显示reflect.Value 的内部构造(在这里设置为`*os.File`的类型描述体)。`Display("rV", reflect.ValueOf(os.Stderr))`调用的输出如下,当然不同环境得到的结果可能有差异:
```Go
Display rV (reflect.Value):

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

@ -37,4 +37,4 @@ methods.Print(new(strings.Replacer))
// type *strings.Replacer
// func (*strings.Replacer) Replace(string) string
// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)
````
```

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%的内存。
@ -38,11 +38,11 @@ struct{ bool; int16; float64 } // 2 words 3words
关于内存地址对齐算法的细节超出了本书的范围也不是每一个结构体都需要担心这个问题不过有效的包装可以使数据结构更加紧凑译注未来的Go语言编译器应该会默认优化结构体的顺序当然应该也能够指定具体的内存布局相同讨论请参考 [Issue10014](https://github.com/golang/go/issues/10014) ),内存使用率和性能都可能会受益。
`unsafe.Alignof` 函数返回对应参数的类型需要对齐的倍数. 和 Sizeof 类似, Alignof 也是返回一个常量表达式, 对应一个常量. 通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节), 其它的类型对齐到机器字大小.
`unsafe.Alignof` 函数返回对应参数的类型需要对齐的倍数。和 Sizeof 类似, Alignof 也是返回一个常量表达式对应一个常量。通常情况下布尔和数字类型需要对齐到它们本身的大小最多8个字节其它的类型对齐到机器字大小。
`unsafe.Offsetof` 函数的参数必须是一个字段 `x.f`, 然后返回 `f` 字段相对于 `x` 起始地址的偏移量, 包括可能的空洞.
`unsafe.Offsetof` 函数的参数必须是一个字段 `x.f`,然后返回 `f` 字段相对于 `x` 起始地址的偏移量,包括可能的空洞。
图 13.1 显示了一个结构体变量 x 以及其在32位和64位机器上的典型的内存. 灰色区域是空洞.
图 13.1 显示了一个结构体变量 x 以及其在32位和64位机器上的典型的内存。灰色区域是空洞。
```Go
var x struct {

View File

@ -2,7 +2,7 @@
Go程序可能会遇到要访问C语言的某些硬件驱动函数的场景或者是从一个C++语言实现的嵌入式数据库查询记录的场景或者是使用Fortran语言实现的一些线性代数库的场景。C语言作为一个通用语言很多库会选择提供一个C兼容的API然后用其他不同的编程语言实现译者Go语言需要也应该拥抱这些巨大的代码遗产
在本节中我们将构建一个简易的数据压缩程序使用了一个Go语言自带的叫cgo的用于支援C语言函数调用的工具。这类工具一般被称为 *foreign-function interfaces* 简称ffi, 并且在类似工具中cgo也不是唯一的。SWIG http://swig.org 是另一个类似的且被广泛使用的工具SWIG提供了很多复杂特性以支援C++的特性但SWIG并不是我们要讨论的主题。
在本节中我们将构建一个简易的数据压缩程序使用了一个Go语言自带的叫cgo的用于支援C语言函数调用的工具。这类工具一般被称为 *foreign-function interfaces* 简称ffi并且在类似工具中cgo也不是唯一的。SWIGhttp://swig.org是另一个类似的且被广泛使用的工具SWIG提供了很多复杂特性以支援C++的特性但SWIG并不是我们要讨论的主题。
在标准库的`compress/...`子包有很多流行的压缩算法的编码和解码实现包括流行的LZW压缩算法Unix的compress命令用的算法和DEFLATE压缩算法GNU gzip命令用的算法。这些包的API的细节虽然有些差异但是它们都提供了针对 io.Writer类型输出的压缩接口和提供了针对io.Reader类型输入的解压缩接口。例如
@ -37,7 +37,7 @@ bzip2压缩算法是基于优雅的Burrows-Wheeler变换算法运行速度
// pointers to Go variables.
```
要使用libbzip2我们需要先构建一个bz_stream结构体用于保持输入和输出缓存。然后有三个函数BZ2_bzCompressInit用于初始化缓存BZ2_bzCompress用于将输入缓存的数据压缩到输出缓存BZ2_bzCompressEnd用于释放不需要的缓存。目前不要担心包的具体结构, 这个例子的目的就是演示各个部分如何组合在一起的。)
要使用libbzip2我们需要先构建一个bz_stream结构体用于保持输入和输出缓存。然后有三个函数BZ2_bzCompressInit用于初始化缓存BZ2_bzCompress用于将输入缓存的数据压缩到输出缓存BZ2_bzCompressEnd用于释放不需要的缓存。目前不要担心包的具体结构这个例子的目的就是演示各个部分如何组合在一起的。)
我们可以在Go代码中直接调用BZ2_bzCompressInit和BZ2_bzCompressEnd但是对于BZ2_bzCompress我们将定义一个C语言的包装函数用它完成真正的工作。下面是C代码对应一个独立的文件。
@ -204,7 +204,7 @@ $ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
```
我们演示了如何将一个C语言库链接到Go语言程序。相反, 将Go编译为静态库然后链接到C程序或者将Go程序编译为动态库然后在C程序中动态加载也都是可行的译注在Go1.5中Windows系统的Go语言实现并不支持生成C语言动态库或静态库的特性。不过好消息是目前已经有人在尝试解决这个问题具体请访问 [Issue11058](https://github.com/golang/go/issues/11058) 。这里我们只展示的cgo很小的一些方面更多的关于内存管理、指针、回调函数、中断信号处理、字符串、errno处理、终结器以及goroutines和系统线程的关系等有很多细节可以讨论。特别是如何将Go语言的指针传入C函数的规则也是异常复杂的译注简单来说要传入C函数的Go指针指向的数据本身不能包含指针或其他引用类型并且C函数在返回后不能继续持有Go指针并且在C函数返回之前Go指针是被锁定的不能导致对应指针数据被移动或栈的调整部分的原因在13.2节有讨论到但是在Go1.5中还没有被明确译注Go1.6将会明确cgo中的指针使用规则。如果要进一步阅读可以从 https://golang.org/cmd/cgo 开始。
我们演示了如何将一个C语言库链接到Go语言程序。相反将Go编译为静态库然后链接到C程序或者将Go程序编译为动态库然后在C程序中动态加载也都是可行的译注在Go1.5中Windows系统的Go语言实现并不支持生成C语言动态库或静态库的特性。不过好消息是目前已经有人在尝试解决这个问题具体请访问 [Issue11058](https://github.com/golang/go/issues/11058) 。这里我们只展示的cgo很小的一些方面更多的关于内存管理、指针、回调函数、中断信号处理、字符串、errno处理、终结器以及goroutines和系统线程的关系等有很多细节可以讨论。特别是如何将Go语言的指针传入C函数的规则也是异常复杂的译注简单来说要传入C函数的Go指针指向的数据本身不能包含指针或其他引用类型并且C函数在返回后不能继续持有Go指针并且在C函数返回之前Go指针是被锁定的不能导致对应指针数据被移动或栈的调整部分的原因在13.2节有讨论到但是在Go1.5中还没有被明确译注Go1.6将会明确cgo中的指针使用规则。如果要进一步阅读可以从 https://golang.org/cmd/cgo 开始。
**练习 13.3** 使用sync.Mutex以保证bzip2.writer在多个goroutines中被并发调用是安全的。

View File

@ -16,5 +16,4 @@ Go语言的实现刻意隐藏了很多底层细节。我们无法知道一个结
要注意的是unsafe包是一个采用特殊方式实现的包。虽然它可以和普通包一样的导入和使用但它实际上是由编译器实现的。它提供了一些访问语言内部特性的方法特别是内存布局相关的细节。将这些特性封装到一个独立的包中是为在极少数情况下需要使用的时候同时引起人们的注意译注因为看包的名字就知道使用unsafe包是不安全的。此外有一些环境因为安全的因素可能限制这个包的使用。
不过unsafe包被广泛地用于比较低级的包, 例如runtime、os、syscall还有net包等因为它们需要和操作系统密切配合但是对于普通的程序一般是不需要使用unsafe包的。
不过unsafe包被广泛地用于比较低级的包例如runtime、os、syscall还有net包等因为它们需要和操作系统密切配合但是对于普通的程序一般是不需要使用unsafe包的。

View File

@ -32,7 +32,7 @@ q := new(int)
fmt.Println(p == q) // "false"
```
当然也可能有特殊情况如果两个类型都是空的也就是说类型的大小是0例如`struct{}`和 `[0]int`, 有可能有相同的地址依赖具体的语言实现译注请谨慎使用大小为0的类型因为如果类型的大小为0的话可能导致Go语言的自动垃圾回收器有不同的行为具体请查看`runtime.SetFinalizer`函数相关文档)。
当然也可能有特殊情况如果两个类型都是空的也就是说类型的大小是0例如`struct{}`和`[0]int`有可能有相同的地址依赖具体的语言实现译注请谨慎使用大小为0的类型因为如果类型的大小为0的话可能导致Go语言的自动垃圾回收器有不同的行为具体请查看`runtime.SetFinalizer`函数相关文档)。
new函数使用通常相对比较少因为对于结构体来说直接用字面量语法创建新变量的方法会更灵活§4.4.1)。

View File

@ -56,7 +56,7 @@ fmt.Println(i, i+1, i*i) // "127 -128 1"
这里是一元的加法和减法运算符:
```
+ 一元加法 (无效果)
+ 一元加法(无效果)
- 负数
```
@ -68,12 +68,12 @@ Go语言还提供了以下的bit位操作运算符前面4个操作运算符
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空 (AND NOT)
&^ 位清空AND NOT
<< 左移
>> 右移
```
位操作运算符`^`作为二元运算符时是按位异或XOR当用作一元运算符时表示按位取反也就是说它返回一个每个bit位都取反的数。位操作运算符`&^`用于按位置零AND NOT如果对应y中bit位为1的话, 表达式`z = x &^ y`结果z的对应的bit位为0否则z对应的bit位等于x相应的bit位的值。
位操作运算符`^`作为二元运算符时是按位异或XOR当用作一元运算符时表示按位取反也就是说它返回一个每个bit位都取反的数。位操作运算符`&^`用于按位置零AND NOT如果对应y中bit位为1的话表达式`z = x &^ y`结果z的对应的bit位为0否则z对应的bit位等于x相应的bit位的值。
下面的代码演示了如何使用位操作解释uint8类型值的8个独立的bit位。它使用了Printf函数的%b参数打印二进制格式的数字其中%08b中08表示打印至少8个字符宽度不足的前缀部分用0填充。

View File

@ -135,15 +135,15 @@ func f(x, y float64) float64 {
要注意的是corner函数返回了两个结果分别对应每个网格顶点的坐标参数。
要解释这个程序是如何工作的需要一些基本的几何学知识但是我们可以跳过几何学原理因为程序的重点是演示浮点数运算。程序的本质是三个不同的坐标系中映射关系如图3.2所示。第一个是100x100的二维网格对应整数坐标(i,j),从远处的(0, 0)位置开始。我们从远处向前面绘制,因此远处先绘制的多边形有可能被前面后绘制的多边形覆盖。
要解释这个程序是如何工作的需要一些基本的几何学知识但是我们可以跳过几何学原理因为程序的重点是演示浮点数运算。程序的本质是三个不同的坐标系中映射关系如图3.2所示。第一个是100x100的二维网格对应整数坐标(i,j),从远处的(0,0)位置开始。我们从远处向前面绘制,因此远处先绘制的多边形有可能被前面后绘制的多边形覆盖。
第二个坐标系是一个三维的网格浮点坐标(x,y,z)其中x和y是i和j的线性函数通过平移转换为网格单元的中心然后用xyrange系数缩放。高度z是函数f(x,y)的值。
第三个坐标系是一个二维的画布,起点(0,0)在左上角。画布中点的坐标用(sx, sy)表示。我们使用等角投影将三维点
第三个坐标系是一个二维的画布,起点(0,0)在左上角。画布中点的坐标用(sx,sy)表示。我们使用等角投影将三维点(x,y,z)投影到二维的画布中。
![](../images/ch3-02.png)
(x,y,z)投影到二维的画布中。画布中从远处到右边的点对应较大的x值和较大的y值。并且画布中x和y值越大则对应的z值越小。x和y的垂直和水平缩放系数来自30度角的正弦和余弦值。z的缩放系数0.4,是一个任意选择的参数。
画布中从远处到右边的点对应较大的x值和较大的y值。并且画布中x和y值越大则对应的z值越小。x和y的垂直和水平缩放系数来自30度角的正弦和余弦值。z的缩放系数0.4,是一个任意选择的参数。
对于二维网格中的每一个网格单元main函数计算单元的四个顶点在画布中对应多边形ABCD的顶点其中B对应(i,j)顶点位置A、C和D是其它相邻的顶点然后输出SVG的绘制指令。
@ -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

@ -93,4 +93,4 @@ func mandelbrot(z complex128) color.Color {
**练习 3.8** 通过提高精度来生成更多级别的分形。使用四种不同精度类型的数字实现相同的分形complex64、complex128、big.Float和big.Rat。后面两种类型在math/big包声明。Float是有指定限精度的浮点数Rat是无限精度的有理数。它们间的性能和内存使用对比如何当渲染图可见时缩放的级别是多少
**练习 3.9** 编写一个web服务器用于给客户端生成分形的图像。运行客户端通过HTTP参数指定x,y和zoom参数。
**练习 3.9** 编写一个web服务器用于给客户端生成分形的图像。运行客户端通过HTTP参数指定xy和zoom参数。

View File

@ -29,7 +29,7 @@ if b {
}
```
如果需要经常做类似的转换, 包装成一个函数会更方便:
如果需要经常做类似的转换,包装成一个函数会更方便:
```Go
// btoi returns 1 if b is true and 0 if false.
@ -41,7 +41,7 @@ func btoi(b bool) int {
}
```
数字到布尔型的逆转换则非常简单, 不过为了保持对称, 我们也可以包装一个函数:
数字到布尔型的逆转换则非常简单,不过为了保持对称,我们也可以包装一个函数:
```Go
// itob reports whether i is non-zero.

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

@ -8,8 +8,7 @@ func name(parameter-list) (result-list) {
}
```
形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。
在hypot函数中,
形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表那么函数体执行完毕后不会返回任何值。在hypot函数中
```Go
func hypot(x, y float64) float64 {
@ -18,7 +17,7 @@ func hypot(x, y float64) float64 {
fmt.Println(hypot(3,4)) // "5"
```
x和y是形参名,3和4是调用时的传入的实参函数返回了一个float64类型的值。
x和y是形参名3和4是调用时的传入的实参函数返回了一个float64类型的值。
返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为该类型的零值。
如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。

View File

@ -4,7 +4,7 @@
下文的示例代码使用了非标准包 golang.org/x/net/html 解析HTML。golang.org/x/... 目录下存储了一些由Go团队设计、维护对网络编程、国际化文件处理、移动平台、图像处理、加密解密、开发者工具提供支持的扩展包。未将这些扩展包加入到标准库原因有二一是部分包仍在开发中二是对大多数Go语言的开发者而言扩展包提供的功能很少被使用。
例子中调用golang.org/x/net/html的部分api如下所示。html.Parse函数读入一组bytes解析后返回html.Node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text文本,commnets注释类型在下面的例子中我们 只关注< name key='value' >形式的结点。
例子中调用golang.org/x/net/html的部分api如下所示。html.Parse函数读入一组bytes解析后返回html.Node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text文本commnets注释类型在下面的例子中我们 只关注< name key='value' >形式的结点。
<u><i>golang.org/x/net/html</i></u>
```Go
@ -151,12 +151,12 @@ $ ./fetch https://golang.org | ./outline
正如你在上面实验中所见大部分HTML页面只需几层递归就能被处理但仍然有些页面需要深层次的递归。
大部分编程语言使用固定大小的函数调用栈常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度当你用递归处理大量数据时需要避免栈溢出除此之外还会导致安全性问题。与此相反Go语言使用可变栈栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
大部分编程语言使用固定大小的函数调用栈常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度当你用递归处理大量数据时需要避免栈溢出除此之外还会导致安全性问题。与此相反Go语言使用可变栈栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
**练习 5.1** 修改findlinks代码中遍历n.FirstChild链表的部分将循环调用visit改成递归调用。
**练习 5.2** 编写函数记录在HTML树中出现的同名元素的次数。
**练习 5.3** 编写函数输出所有text结点的内容。注意不要访问`<script>``<style>`,
**练习 5.3** 编写函数输出所有text结点的内容。注意不要访问`<script>``<style>`
**练习 5.4** 扩展visit函数使其能够处理其他类型的结点如images、scripts和style sheets。

View File

@ -41,7 +41,7 @@ func findLinks(url string) ([]string, error) {
在findlinks中有4处return语句每一处return都返回了一组值。前三处return将http和html包中的错误信息传递给findlinks的调用者。第一处return直接返回错误信息其他两处通过fmt.Errorf§7.8输出详细的错误信息。如果findlinks成功结束最后的return语句将一组解析获得的连接返回给用户。
在finallinks中我们必须确保resp.Body被关闭释放网络资源。虽然Go的垃圾回收机制会回收不被使用的内存但是这不包括操作系统层面的资源比如打开的文件、网络连接。因此我们必须显式的释放这些资源。
在findlinks中我们必须确保resp.Body被关闭释放网络资源。虽然Go的垃圾回收机制会回收不被使用的内存但是这不包括操作系统层面的资源比如打开的文件、网络连接。因此我们必须显式的释放这些资源。
调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量:
@ -110,8 +110,8 @@ func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
return words, images, err
```
当一个函数有多处return语句以及许多返回值时bare return 可以减少代码的重复但是使得代码难以被理解。举个例子如果你没有仔细的审查代码很难发现前2处return等价于 return 0,0,errGo会将返回值 words和images在函数体的开始处根据它们的类型将其初始化为0最后一处return等价于 return wordsimagenil。基于以上原因不宜过度使用bare return。
当一个函数有多处return语句以及许多返回值时bare return 可以减少代码的重复但是使得代码难以被理解。举个例子如果你没有仔细的审查代码很难发现前2处return等价于 return 0,0,errGo会将返回值 words和images在函数体的开始处根据它们的类型将其初始化为0最后一处return等价于 return words, image, nil。基于以上原因不宜过度使用bare return。
**练习 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

@ -1,6 +1,5 @@
## 5.4. 错误
在Go中有一部分函数总是能成功的运行。比如strings.Contains和strconv.FormatBool函数对各种可能的输入都做了良好的处理使得运行时几乎不会失败除非遇到灾难性的、不可预料的情况比如运行时的内存溢出。导致这种错误的原因很复杂难以处理从错误中恢复的可能性也很低。
还有一部分函数只要输入的参数满足一定条件也能保证运行成功。比如time.Date函数该函数将年月日等参数构造成time.Time对象除非最后一个参数时区是nil。这种情况下会引发panic异常。panic是来自被调用函数的信号表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。
@ -20,14 +19,14 @@ if !ok {
通常导致失败的原因不止一种尤其是对I/O操作而言用户需要了解更多的错误信息。因此额外的返回值不再是简单的布尔类型而是error类型。
内置的error是接口类型。我们将在第七章了解接口类型的含义以及它对错误处理的影响。现在我们只需要明白error类型可能是nil或者non-nil。nil意味着函数运行成功non-nil表示失败。对于non-nil的error类型,我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。
内置的error是接口类型。我们将在第七章了解接口类型的含义以及它对错误处理的影响。现在我们只需要明白error类型可能是nil或者non-nil。nil意味着函数运行成功non-nil表示失败。对于non-nil的error类型我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。
```Go
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

@ -49,9 +49,9 @@
<u><i>gopl.io/ch5/outline2</i></u>
```Go
// forEachNode针对每个结点x,都会调用pre(x)和post(x)。
// forEachNode针对每个结点x都会调用pre(x)和post(x)。
// pre和post都是可选的。
// 遍历孩子结点之前,pre被调用
// 遍历孩子结点之前pre被调用
// 遍历孩子结点之后post被调用
func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
if pre != nil {

View File

@ -242,6 +242,6 @@ http://research.swtch.com/gotour
**练习5.13** 修改crawl使其能保存发现的页面必要时可以创建目录来保存这些页面。只保存来自原始域名下的页面。假设初始页面在golang.org下就不要保存vimeo.com下的页面。
**练习5.14** 使用breadthFirst遍历其他数据结构。比如topoSort例子中的课程依赖关系有向图,个人计算机的文件层次结构(树),你所在城市的公交或地铁线路(无向图)。
**练习5.14** 使用breadthFirst遍历其他数据结构。比如topoSort例子中的课程依赖关系有向图、个人计算机的文件层次结构(树);你所在城市的公交或地铁线路(无向图)。
{% include "./ch5-06-1.md" %}

View File

@ -15,7 +15,7 @@ func sum(vals...int) int {
}
```
sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[] int的切片。sum可以接收任意数量的int型参数
sum函数返回任意个int型参数的和。在函数体中vals被看作是类型为[] int的切片。sum可以接收任意数量的int型参数
```Go
fmt.Println(sum()) // "0"

View File

@ -64,4 +64,4 @@ func soleTitle(doc *html.Node) (title string, err error) {
有些情况下我们无法恢复。某些致命错误会导致Go在运行时终止程序如内存不足。
**练习5.19** 使用panic和recover编写一个不包含return语句但能返回一个非零值的函数。
**练习5.19** 使用panic和recover编写一个不包含return语句但能返回一个非零值的函数。

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节中做的那样:
@ -107,7 +107,7 @@ func (*IntSet) Copy() *IntSet // return a copy of the set
**练习 6.2** 定义一个变参方法(*IntSet).AddAll(...int)这个方法可以添加一组IntSet比如s.AddAll(1,2,3)。
**练习 6.3** (*IntSet).UnionWith会用`|`操作符计算两个集合的并集我们再为IntSet实现另外的几个函数IntersectWith交集元素在A集合B集合均出现,DifferenceWith差集元素出现在A集合未出现在B集合SymmetricDifference并差集元素出现在A但没有出现在B或者出现在B没有出现在A
**练习 6.3** (*IntSet).UnionWith会用`|`操作符计算两个集合的并集我们再为IntSet实现另外的几个函数IntersectWith交集元素在A集合B集合均出现DifferenceWith差集元素出现在A集合未出现在B集合SymmetricDifference并差集元素出现在A但没有出现在B或者出现在B没有出现在A
***练习6.4: ** 实现一个Elems方法返回集合中的所有元素用于做一些range之类的遍历操作。

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

@ -22,7 +22,7 @@ w = rwc // OK: io.ReadWriteCloser has Write method
rwc = w // compile error: io.Writer lacks Close method
```
因为ReadWriter和ReadWriteCloser包含有Writer的方法所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口
因为ReadWriter和ReadWriteCloser包含有Writer的方法所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口
在进一步学习前必须先解释一个类型持有一个方法的表示当中的细节。回想在6.2章中对于每一个命名过的具体类型T它的一些方法的接收者是类型T本身然而另一些则是一个`*T`的指针。还记得在T类型的参数上调用一个`*T`的方法是合法的只要这个参数是一个变量编译器隐式的获取了它的地址。但这仅仅是一个语法糖T类型的值不拥有所有`*T`指针的方法,这样它就可能只实现了更少的接口。
@ -34,7 +34,7 @@ func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
```
但是我们可以在一个IntSet上调用这个方法:
但是我们可以在一个IntSet变量上调用这个方法:
```go
var s IntSet
@ -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
@ -74,7 +74,7 @@ func (f *celsiusFlag) Set(s string) error {
调用fmt.Sscanf函数从输入s中解析一个浮点数value和一个字符串unit。虽然通常必须检查Sscanf的错误返回但是在这个例子中我们不需要因为如果有错误发生就没有switch case会匹配到。
下面的CelsiusFlag函数将所有逻辑都封装在一起。它返回一个内嵌在celsiusFlag变量f中的Celsius指针给调用者。Celsius字段是一个会通过Set方法在标记处理的过程中更新的变量。调用Var方法将标记加入应用的命令行标记集合中有异常复杂命令行接口的全局变量flag.CommandLine.Programs可能有几个这个类型的变量。调用Var方法将一个`*celsiusFlag`参数赋值给一个flag.Value参数,导致编译器去检查`*celsiusFlag`是否有必须的方法。
下面的CelsiusFlag函数将所有逻辑都封装在一起。它返回一个内嵌在celsiusFlag变量f中的Celsius指针给调用者。Celsius字段是一个会通过Set方法在标记处理的过程中更新的变量。调用Var方法将标记加入应用的命令行标记集合中有异常复杂命令行接口的全局变量flag.CommandLine.Programs可能有几个这个类型的变量。调用Var方法将一个`*celsiusFlag`参数赋值给一个flag.Value参数导致编译器去检查`*celsiusFlag`是否有必须的方法。
```go
// CelsiusFlag defines a Celsius flag with the specified name,

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

@ -74,7 +74,7 @@ w = nil
这个重置将它所有的部分都设为nil值把变量w恢复到和它之前定义时相同的状态在图7.1中可以看到。
一个接口值可以持有任意大的动态值。例如表示时间实例的time.Time类型这个类型有几个对外不公开的字段。我们从它上面创建一个接口值,
一个接口值可以持有任意大的动态值。例如表示时间实例的time.Time类型这个类型有几个对外不公开的字段。我们从它上面创建一个接口值
```go
var x interface{} = time.Now()

View File

@ -1,4 +1,5 @@
## 7.6. sort.Interface接口
排序操作和字符串格式化一样是很多程序经常使用的操作。尽管一个最短的快排程序只要15行就可以搞定但是一个健壮的实现需要更多的代码并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。
幸运的是sort包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中排序算法都是和序列数据类型关联同时排序函数和具体类型元素关联。相比之下Go语言的sort.Sort函数不会对具体的序列和它的元素做任何假设。相反它使用了一个接口类型sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定序列的表示经常是一个切片。
@ -124,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
@ -212,10 +213,10 @@ fmt.Println(values) // "[4 3 1 1]"
fmt.Println(sort.IntsAreSorted(values)) // "false"
```
为了使用方便sort包为[]int,[]string和[]float64的正常排序提供了特定版本的函数和类型。对于其他类型例如[]int64或者[]uint尽管路径也很简单还是依赖我们自己实现。
为了使用方便sort包为[]int[]string和[]float64的正常排序提供了特定版本的函数和类型。对于其他类型例如[]int64或者[]uint尽管路径也很简单还是依赖我们自己实现。
**练习 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
@ -44,7 +44,7 @@ $ go build gopl.io/ch7/http1
$ ./http1 &
```
然后用1.5节中的获取程序如果你更喜欢可以使用web浏览器来连接服务器,我们得到下面的输出:
然后用1.5节中的获取程序如果你更喜欢可以使用web浏览器来连接服务器我们得到下面的输出:
```
$ go build gopl.io/ch1/fetch
@ -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

@ -1,6 +1,6 @@
## 8.2. 示例: 并发的Clock服务
网络编程是并发大显身手的一个领域由于服务器是最典型的需要同时处理很多连接的程序这些连接一般来自于彼此独立的客户端。在本小节中我们会讲解go语言的net包这个包提供编写一个网络客户端或者服务器程序的基本组件无论两者间通信是使用TCPUDP或者Unix domain sockets。在第一章中我们使用过的net/http包里的方法也算是net包的一部分。
网络编程是并发大显身手的一个领域由于服务器是最典型的需要同时处理很多连接的程序这些连接一般来自于彼此独立的客户端。在本小节中我们会讲解go语言的net包这个包提供编写一个网络客户端或者服务器程序的基本组件无论两者间通信是使用TCPUDP或者Unix domain sockets。在第一章中我们使用过的net/http包里的方法也算是net包的一部分。
我们的第一个例子是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端:
@ -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程序所需要的所有语言特性信息。在后面两章节中我们会回顾一些之前的实例和工具支持我们写出更大规模的程序如何将一个工程组织成一系列的包如何获取构建测试性能测试剖析写文档并且将这些包分享出去。

View File

@ -11,12 +11,7 @@ Go语言圣经 [《The Go Programming Language》](http://gopl.io) 中文版本
在线预览:
- http://gopl-zh.simple-is-best.tk/
- http://gopl-zh.b0.upaiyun.com/
- http://docs.ruanjiadeng.com/gopl-zh/
- http://shifei.me/gopl-zh/
- http://2goo.info/media/html/gopl-zh-gh-pages/
- http://docs.plhwin.com/gopl-zh/
- https://gopl-zh.shanyy.xyz
- https://docs.hacknode.org/gopl-zh/
- http://books.studygolang.com/gopl-zh/