Merge pull request #26 from zhliner/revised

Revised
pull/27/head
Xargin 2017-08-24 23:23:19 +08:00 committed by GitHub
commit b056972f41
97 changed files with 284 additions and 286 deletions

View File

@ -44,7 +44,7 @@ line := input.Text()
counts[line] = counts[line] + 1
```
`map`中不含某个键时不用担心,首次读到新行时,等号右边的表达式`counts[line]`的值将被计算为其类型的零值对于int`即0。
`map`中不含某个键时不用担心,首次读到新行时,等号右边的表达式`counts[line]`的值将被计算为其类型的零值,对于`int`即0。
为了打印结果,我们使用了基于`range`的循环,并在`counts`这个`map`上迭代。跟之前类似,每次迭代得到两个结果,键和其在`map`中对应的值。`map`的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。(译注具体可以参见这里http://stackoverflow.com/questions/11853396/google-go-lang-assignment-order)
@ -131,7 +131,7 @@ func countLines(f *os.File, counts map[string]int) {
注意`countLines`函数在其声明前被调用。函数和包级别的变量package-level entities可以任意顺序声明并不影响其被调用。译注最好还是遵循一定的规范
`map`是一个由`make`函数创建的数据结构的引用。`map`作为参数传递给某函数时该函数接收这个引用的一份拷贝copy或译为副本被调用函数对`map`底层数据结构的任何修改,调用者函数都可以通过持有的`map`引用看到。在我们的例子中,`countLines`函数向`counts`插入的值,也会被`main`函数看到。译注类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)
`map`是一个由`make`函数创建的数据结构的引用。`map`作为参数传递给某函数时该函数接收这个引用的一份拷贝copy或译为副本被调用函数对`map`底层数据结构的任何修改,调用者函数都可以通过持有的`map`引用看到。在我们的例子中,`countLines`函数向`counts`插入的值,也会被`main`函数看到。译注类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)
`dup`的前两个版本以"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,`dup3`,就是这么操作的。这个例子引入了`ReadFile`函数(来自于`io/ioutil`包),其读取指定文件的全部内容,`strings.Split`函数把字符串分割成子串的切片。(`Split`的作用与前文提到的`strings.Join`相反。)

View File

@ -112,14 +112,17 @@ func handler(w http.ResponseWriter, r *http.Request) {
```
GET /?q=query HTTP/1.1
Header["Accept-Encoding"] = ["gzip, deflate, sdch"] Header["Accept-Language"] = ["en-US,en;q=0.8"]
Header["Accept-Encoding"] = ["gzip, deflate, sdch"]
Header["Accept-Language"] = ["en-US,en;q=0.8"]
Header["Connection"] = ["keep-alive"]
Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."] Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."] Host = "localhost:8000"
Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."]
Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."]
Host = "localhost:8000"
RemoteAddr = "127.0.0.1:59911"
Form["q"] = ["query"]
```
可以看到这里的ParseForm被嵌套在了if语句中。Go语言允许这样的一个简单的语句结果作为循环的变量声明出现在if语句的最前面这一点对错误处理很有用处。我们还可以像下面这样写当然看起来就长了一些
可以看到这里的ParseForm被嵌套在了if语句中。Go语言允许这样的一个简单的语句结果作为局部的变量声明出现在if语句的最前面这一点对错误处理很有用处。我们还可以像下面这样写当然看起来就长了一些
```go
err := r.ParseForm()

View File

@ -8,7 +8,7 @@
包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式这是为了避免和预定义的类型冲突同样还有go/types是为了避免和type关键字冲突。
要避免包名有其它的含义。例如2.5节中我们的温度转换包最初使用了temp包名虽然并没有持续多久。但这是一个糟糕的尝试因为temp几乎是临时变量的同义词。然后我们有一段时间使用了temperature作为包名然名字并没有表达包的真实用途。最后我们改成了和strconv标准包类似的tempconv包名这个名字比之前的就好多了。
要避免包名有其它的含义。例如2.5节中我们的温度转换包最初使用了temp包名虽然并没有持续多久。但这是一个糟糕的尝试因为temp几乎是临时变量的同义词。然后我们有一段时间使用了temperature作为包名然名字并没有表达包的真实用途。最后我们改成了和strconv标准包类似的tempconv包名这个名字比之前的就好多了。
现在让我们看看如何命名包的成员。由于是通过包的导入名字引入包里面的成员例如fmt.Println同时包含了包名和成员名信息。因此我们一般并不需要关注Println的具体内容因为fmt包名已经包含了这个信息。当设计一个包的时候需要考虑包名和成员名两个部分如何很好地配合。下面有一些例子

View File

@ -68,7 +68,7 @@ $ go run quoteargs.go one "two three" four\ five
因为编译对应不同的操作系统平台和CPU架构`go install`命令会将编译结果安装到GOOS和GOARCH对应的目录。例如在Mac系统golang.org/x/net/html包将被安装到$GOPATH/pkg/darwin_amd64目录下的golang.org/x/net/html.a文件。
针对不同操作系统或CPU的交叉构建也是很简单的。只需要设置好目标对应的GOOS和GOARCH然后运行构建命令即可。下面交叉编译的程序将输出它在编译时操作系统和CPU类型
针对不同操作系统或CPU的交叉构建也是很简单的。只需要设置好目标对应的GOOS和GOARCH然后运行构建命令即可。下面交叉编译的程序将输出它在编译时操作系统和CPU类型
<u><i>gopl.io/ch10/cross</i></u>
```Go
@ -77,7 +77,7 @@ func main() {
}
```
下面以64位和32位环境分别执行程序
下面以64位和32位环境分别编译和执行:
```
$ go build gopl.io/ch10/cross

View File

@ -10,7 +10,7 @@ Go语言中的文档注释一般是完整的句子第一行通常是摘要说
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
```
Fprintf函数格式化的细节在fmt包文档中描述。如果注释后跟着包声明语句那注释对应整个包的文档。包文档对应的注释只能有一个译注其实可以有多个它们会组合成一个包文档注释包注释可以出现在任何一个源文件中。如果包的注释内容比较长一般会放到一个独立的源文件中fmt包注释就有300行之多。这个专门用于保存包文档的源文件通常叫doc.go。
Fprintf函数格式化的细节在fmt包文档中描述。如果注释后跟着包声明语句那注释对应整个包的文档。包文档对应的注释只能有一个译注其实可以有多个它们会组合成一个包文档注释包注释可以出现在任何一个源文件中。如果包的注释内容比较长一般会放到一个独立的源文件中fmt包注释就有300行之多。这个专门用于保存包文档的源文件通常叫doc.go。
好的文档并不需要面面俱到文档本身应该是简洁但不可忽略的。事实上Go语言的风格更喜欢简洁的文档并且文档也是需要像代码一样维护的。对于一组声明语句可以用一个精炼的句子描述如果是显而易见的功能则并不需要注释。

View File

@ -88,7 +88,7 @@ $ go list -f "{{join .Deps \" \"}}" strconv
```
{% endraw %}
下面的命令打印compress子目录下所有包的依赖包列表:
下面的命令打印compress子目录下所有包的导入包列表:
{% raw %}
```

View File

@ -2,9 +2,9 @@
本章剩下的部分将讨论Go语言工具箱的具体功能包括如何下载、格式化、构建、测试和安装Go语言编写的程序。
Go语言的工具箱集合了一系列功能的命令集。它可以看作是一个包管理器类似于Linux中的apt和rpm工具用于包的查询、计算包的依赖关系、从远程版本控制系统下载它们等任务。它也是一个构建系统计算文件的依赖关系然后调用编译器、汇编器和链接器构建程序虽然它故意被设计成没有标准的make命令那么复杂。它也是一个单元测试和基准测试的驱动程序我们将在第11章讨论测试话题。
Go语言的工具箱集合了一系列功能的命令集。它可以看作是一个包管理器类似于Linux中的apt和rpm工具用于包的查询、计算包的依赖关系、从远程版本控制系统下载它们等任务。它也是一个构建系统计算文件的依赖关系然后调用编译器、汇编器和链接器构建程序虽然它故意被设计成没有标准的make命令那么复杂。它也是一个单元测试和基准测试的驱动程序我们将在第11章讨论测试话题。
Go语言工具箱的命令有着类似“瑞士军刀”的风格带着一打的子命令有一些我们经常用到例如get、run、build和fmt等。你可以运行go或go help命令查看内置的帮助文档为了查询方便我们列出了最常用的命令
Go语言工具箱的命令有着类似“瑞士军刀”的风格带着一打的子命令有一些我们经常用到例如get、run、build和fmt等。你可以运行go或go help命令查看内置的帮助文档为了查询方便我们列出了最常用的命令
```
$ go
@ -26,7 +26,7 @@ Use "go help [command]" for more information about a command.
...
```
为了达到零配置的设计目标Go语言的工具箱很多地方都依赖各种约定。例如根据给定的源文件的名称Go语言的工具可以找到源文件对应的包因为每个目录只包含了单一的包并且包的导入路径和工作区的目录结构是对应的。给定一个包的导入路径Go语言的工具可以找到与之对应的存储着实体文件的目录。它还可以根据导入路径找到存储代码仓库的远程服务器URL。
为了达到零配置的设计目标Go语言的工具箱很多地方都依赖各种约定。例如根据给定的源文件的名称Go语言的工具可以找到源文件对应的包因为每个目录只包含了单一的包并且包的导入路径和工作区的目录结构是对应的。给定一个包的导入路径Go语言的工具可以找到与之对应的存储着实体文件的目录。它还可以根据导入路径找到存储代码仓库的远程服务器URL。
{% include "./ch10-07-1.md" %}

View File

@ -2,6 +2,6 @@
现在随便一个小程序的实现都可能包含超过10000个函数。然而作者一般只需要考虑其中很小的一部分和做很少的设计因为绝大部分代码都是由他人编写的它们通过类似包或模块的方式被重用。
Go语言有超过100个的标准包译注可以用`go list std | wc -l`命令查看标准包的具体数目标准库为大多数的程序提供了必要的基础构件。在Go的社区有很多成熟的包被设计、共享、重用和改进目前互联网上已经发布了非常多的Go语言开源包它们可以通过 http://godoc.org 检索。在本章,我们将演示如使用已有的包和创建新的包。
Go语言有超过100个的标准包译注可以用`go list std | wc -l`命令查看标准包的具体数目标准库为大多数的程序提供了必要的基础构件。在Go的社区有很多成熟的包被设计、共享、重用和改进目前互联网上已经发布了非常多的Go语言开源包它们可以通过 http://godoc.org 检索。在本章,我们将演示如使用已有的包和创建新的包。
Go还自带了工具箱里面有很多用来简化工作区和包管理的小工具。在本书开始的时候我们已经见识过如何使用工具箱自带的工具来下载、构建和运行我们的演示程序了。在本章我们将看看这些工具的基本设计理论和尝试更多的功能例如打印工作区中包的文档和查询相关的元数据等。在下一章我们将探讨testing包的单元测试用法。

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

@ -53,7 +53,7 @@ $ go list -f={{.XTestGoFiles}} fmt
例如fmt包的fmt.Scanf函数需要unicode.IsSpace函数提供的功能。但是为了避免太多的依赖fmt包并没有导入包含巨大表格数据的unicode包相反fmt包有一个叫isSpace内部的简易实现。
为了确保fmt.isSpace和unicode.IsSpace函数的行为保持一致fmt包谨慎地包含了一个测试。一个在外部测试包内的白盒测试是无法直接访问到isSpace内部函数的因此fmt通过一个后门导出了isSpace函数。export_test.go文件就是专门用于外部测试包的后门。
为了确保fmt.isSpace和unicode.IsSpace函数的行为保持一致fmt包谨慎地包含了一个测试。一个在外部测试包内的白盒测试是无法直接访问到isSpace内部函数的因此fmt通过一个后门导出了isSpace函数。export_test.go文件就是专门用于外部测试包的后门。
```Go
package fmt

View File

@ -25,7 +25,7 @@ func TestSplit(t *testing.T) {
}
```
从这个意义上说,断言函数犯了过早抽象的错误:仅仅测试两个整数是否相同,而没能根据上下文提供更有意义的错误信息。我们可以根据具体的错误打印一个更有价值的错误信息,就像下面例子那样。只有在测试中出现重复模式才采用抽象。
从这个意义上说,断言函数犯了过早抽象的错误:仅仅测试两个整数是否相同,而没能根据上下文提供更有意义的错误信息。我们可以根据具体的错误打印一个更有价值的错误信息,就像下面例子那样。只有在测试中出现重复模式才采用抽象。
```Go
func TestSplit(t *testing.T) {

View File

@ -2,7 +2,7 @@
如果一个应用程序对于新出现的但有效的输入经常失败说明程序容易出bug不够稳健同样如果一个测试仅仅对程序做了微小变化就失败则称为脆弱。就像一个不够稳健的程序会挫败它的用户一样一个脆弱的测试同样会激怒它的维护者。最脆弱的测试代码会在程序没有任何变化的时候产生不同的结果时好时坏处理它们会耗费大量的时间但是并不会得到任何好处。
当一个测试函数会产生一个复杂的输出如一个很长的字符串、一个精心设计的数据结构或一个文件时,人很容易想预先写下一系列固定的用于对比的标杆数据。但是随着项目的发展,有些输出可能会发生变化,尽管很可能是一个改进的实现导致的。而且不仅仅是输出部分,函数复杂的输入部分可能也跟着变化了,因此测试使用的输入也就不再有效了。
当一个测试函数会产生一个复杂的输出如一个很长的字符串、一个精心设计的数据结构或一个文件时,人很容易想预先写下一系列固定的用于对比的标杆数据。但是随着项目的发展,有些输出可能会发生变化,尽管很可能是一个改进的实现导致的。而且不仅仅是输出部分,函数复杂的输入部分可能也跟着变化了,因此测试使用的输入也就不再有效了。
避免脆弱测试代码的方法是只检测你真正关心的属性。保持测试代码的简洁和内部结构的稳定。特别是对断言部分要有所选择。不要对字符串进行全字匹配,而是针对那些在项目的发展中是比较稳定不变的子串。很多时候值得花力气来编写一个从复杂输出中提取用于断言的必要信息的函数,虽然这可能会带来很多前期的工作,但是它可以帮助迅速及时修复因为项目演化而导致的不合逻辑的失败测试。

View File

@ -209,7 +209,7 @@ ok gopl.io/ch11/word2 0.015s
如果我们真的需要停止测试或许是因为初始化失败或可能是早先的错误导致了后续错误等原因我们可以使用t.Fatal或t.Fatalf停止当前测试函数。它们必须在和测试函数同一个goroutine内调用。
测试失败的信息一般的形式是“f(x) = y, want z”其中f(x)解释了失败的操作和对应的输y是实际的运行结果z是期望的正确的结果。就像前面检查回文字符串的例子实际的函数用于f(x)部分。显示x是表格驱动型测试中比较重要的部分因为同一个断言可能对应不同的表格项执行多次。要避免无用和冗余的信息。在测试类似IsPalindrome返回布尔类型的函数时可以忽略并没有额外信息的z部分。如果x、y或z是y的长度输出一个相关部分的简明总结即可。测试的作者应该要努力帮助程序员诊断测试失败的原因。
测试失败的信息一般的形式是“f(x) = y, want z”其中f(x)解释了失败的操作和对应的输y是实际的运行结果z是期望的正确的结果。就像前面检查回文字符串的例子实际的函数用于f(x)部分。显示x是表格驱动型测试中比较重要的部分因为同一个断言可能对应不同的表格项执行多次。要避免无用和冗余的信息。在测试类似IsPalindrome返回布尔类型的函数时可以忽略并没有额外信息的z部分。如果x、y或z是y的长度输出一个相关部分的简明总结即可。测试的作者应该要努力帮助程序员诊断测试失败的原因。
**练习 11.1:** 为4.3节中的charcount程序编写测试。

View File

@ -63,4 +63,4 @@ Showing top 10 nodes out of 166 (cum >= 60ms)
对于一些更微妙的问题你可能需要使用pprof的图形显示功能。这个需要安装GraphViz工具可以从 http://www.graphviz.org 下载。参数`-web`用于生成函数的有向图标注有CPU的使用和最热点的函数等信息。
这一节我们只是简单看了下Go语言的分析工具。如果想了解更多可以阅读Go官方博客的“Proling Go Programs”一文。
这一节我们只是简单看了下Go语言的数据分析工具。如果想了解更多可以阅读Go官方博客的“Profiling Go Programs”一文。

View File

@ -74,7 +74,7 @@ func display(path string, v reflect.Value) {
让我们针对不同类型分别讨论。
**Slice和数组** 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数Index(i)活动索引i对应的元素返回的也是一个reflect.Value如果索引i超出范围的话将导致panic异常这与数组或slice类型内建的len(a)和a[i]操作类似。display针对序列中的每个元素递归调用自身处理我们通过在递归处理时向path附加“[i]”来表示访问路径。
**Slice和数组** 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数Index(i)获得索引i对应的元素返回的也是一个reflect.Value如果索引i超出范围的话将导致panic异常这与数组或slice类型内建的len(a)和a[i]操作类似。display针对序列中的每个元素递归调用自身处理我们通过在递归处理时向path附加“[i]”来表示访问路径。
虽然reflect.Value类型带有很多方法但是只有少数的方法能对任意值都安全调用。例如Index方法只能对Slice、数组或字符串类型的值调用如果对其它类型调用则会导致panic异常。
@ -218,9 +218,9 @@ c.Value = 42
许多Go语言程序都包含了一些循环的数据。让Display支持这类带环的数据结构需要些技巧需要额外记录迄今访问的路径相应会带来成本。通用的解决方案是采用 unsafe 的语言特性我们将在13.3节看到具体的解决方案。
带环的数据结构很少会对fmt.Sprint函数造成问题因为它很少尝试打印完整的数据结构。例如当它遇到一个指针的时候它只是简单打印指针的数字值。在打印包含自身的slice或map时可能卡住但是这种情况很罕见不值得付出为了处理回环所需的开销。
带环的数据结构很少会对fmt.Sprint函数造成问题因为它很少尝试打印完整的数据结构。例如当它遇到一个指针的时候它只是简单打印指针的数字值。在打印包含自身的slice或map时可能卡住但是这种情况很罕见不值得付出为了处理回环所需的开销。
**练习 12.1** 扩展Displayhans使它可以显示包含以结构体或数组作为map的key类型的值。
**练习 12.1** 扩展Display函数使它可以显示包含以结构体或数组作为map的key类型的值。
**练习 12.2** 增强display函数的稳健性通过记录边界的步数来确保在超出一定限制放弃递归。在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)
**练习 12.2** 增强display函数的稳健性通过记录边界的步数来确保在超出一定限制放弃递归。在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)

View File

@ -25,7 +25,7 @@ fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"
```
每当我们通过指针间接地获取的reflect.Value都是可取地址的即使开始的是一个不可取地址的Value。在反射机制中所有关于是否支持取地址的规则都是类似的。例如slice的索引表达式e[i]将隐式地包含一个指针它就是可取地址的即使开始的e表达式不支持也没有关系。以此类推reflect.ValueOf(e).Index(i)对的值也是可取地址的即使原始的reflect.ValueOf(e)不支持也没有关系。
每当我们通过指针间接地获取的reflect.Value都是可取地址的即使开始的是一个不可取地址的Value。在反射机制中所有关于是否支持取地址的规则都是类似的。例如slice的索引表达式e[i]将隐式地包含一个指针它就是可取地址的即使开始的e表达式不支持也没有关系。以此类推reflect.ValueOf(e).Index(i)对的值也是可取地址的即使原始的reflect.ValueOf(e)不支持也没有关系。
要从变量对应的可取地址的reflect.Value来访问变量需要三个步骤。第一步是调用Addr()方法它返回一个Value里面保存了指向变量的指针。然后是在Value上调用Interface()方法也就是返回一个interface{}里面包含指向变量的指针。最后如果我们知道变量的类型我们可以使用类型的断言机制将得到的interface{}类型的接口强制转为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了:
@ -37,7 +37,7 @@ px := d.Addr().Interface().(*int) // px := &x
fmt.Println(x) // "3"
```
或者不使用指针而是通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新对的值:
或者不使用指针而是通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新对的值:
```Go
d.Set(reflect.ValueOf(4))

View File

@ -4,9 +4,9 @@
第一个原因是基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题在反射中都有与之相对应的误用问题不同的是编译器会在构建时马上报告错误而反射则是在真正运行到的时候才会抛出panic异常可能是写完代码很久之后了而且程序也可能运行了很长的时间。
以前面的readList函数§12.6为例为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险需要非常小心地检查每个reflect.Value的对值的类型、是否可取地址,还有是否可以被修改等。
以前面的readList函数§12.6为例为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险需要非常小心地检查每个reflect.Value的对值的类型、是否可取地址,还有是否可以被修改等。
避免这种因反射而导致的脆弱性的问题的最好方法是将所有的反射相关的使用控制在包的内部如果可能的话避免在包的API中直接暴露reflect.Value类型这样可以限制一些非法输入。如果无法做到这一点在每个有风险的操作前指向额外的类型检查。以标准库中的代码为例当fmt.Printf收到一个非法的操作数它并不会抛出panic异常而是打印相关的错误信息。程序虽然还有BUG但是会更加容易诊断。
避免这种因反射而导致的脆弱性的问题的最好方法是将所有的反射相关的使用控制在包的内部如果可能的话避免在包的API中直接暴露reflect.Value类型这样可以限制一些非法输入。如果无法做到这一点在每个有风险的操作前指向额外的类型检查。以标准库中的代码为例当fmt.Printf收到一个非法的操作数它并不会抛出panic异常而是打印相关的错误信息。程序虽然还有BUG但是会更加容易诊断。
```Go
fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"

View File

@ -2,4 +2,4 @@
Go语言提供了一种机制能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
在本章我们将探讨Go语言的反射特性看看它可以给语言增加哪些表达力以及在两个至关重要的API是如何使用反射机制的一个是fmt包提供的字符串格式功能另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包它们的实现也是依赖反射技术的。然后反射是一个复杂的内省技术不应该随意使用因此尽管上面这些包内部都是用反射技术实现的但是它们自己的API都没有公开反射相关的接口。
在本章我们将探讨Go语言的反射特性看看它可以给语言增加哪些表达力以及在两个至关重要的API是如何使用反射机制的一个是fmt包提供的字符串格式功能另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包它们的实现也是依赖反射技术的。然后反射是一个复杂的内省技术不应该随意使用因此尽管上面这些包内部都是用反射技术实现的但是它们自己的API都没有公开反射相关的接口。

View File

@ -29,8 +29,8 @@ continue for import return var
这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。
如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的译注必须是在函数外部定义的包级名字包级函数名本身也是包级名字那么它将是导出的也就是说可以被外部的包访问例如fmt包的Printf函数就是导出的可以在fmt包外部访问。包本身的名字一般总是用小写字母。
如果一个名字是在函数内部定义那么它就只在函数内部有效。如果是在函数外部定义那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的译注必须是在函数外部定义的包级名字包级函数名本身也是包级名字那么它将是导出的也就是说可以被外部的包访问例如fmt包的Printf函数就是导出的可以在fmt包外部访问。包本身的名字一般总是用小写字母。
名字的长度没有逻辑限制但是Go语言的风格是尽量使用短小的名字对于局部变量尤其是这样你会经常看到i之类的短名字而不是冗长的theLoopIndex命名。通常来说如果一个名字的作用域比较大生命周期也比较长那么用长的名字将会更有意义。
在习惯上Go语言程序员推荐使用 **驼峰式** 命名,当名字有几个单词组成的时优先使用大小写分隔而不是优先用下划线分隔。因此在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法它们可能被称为htmlEscape、HTMLEscape或escapeHTML但不会是escapeHtml。
在习惯上Go语言程序员推荐使用 **驼峰式** 命名,当名字由几个单词组成时优先使用大小写分隔而不是优先用下划线分隔。因此在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法它们可能被称为htmlEscape、HTMLEscape或escapeHTML但不会是escapeHtml。

View File

@ -2,7 +2,7 @@
声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句var、const、type和func分别对应变量、常量、类型和函数实体对象的声明。这一章我们重点讨论变量和类型的声明第三章将讨论常量的声明第五章将讨论函数的声明。
一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件。每个源文件以包的声明语句开始说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包然后是包一级的类型、变量、常量、函数的声明语句包一级的各种类型的声明语句的顺序无关紧要译注函数内部的名字则必须先声明之后才能使用。例如下面的例子中声明了一个常量、一个函数和两个变量
一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件。每个源文件以包的声明语句开始说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包然后是包一级的类型、变量、常量、函数的声明语句包一级的各种类型的声明语句的顺序无关紧要译注函数内部的名字则必须先声明之后才能使用。例如下面的例子中声明了一个常量、一个函数和两个变量
<u><i>gopl.io/ch2/boiling</i></u>
```Go
@ -26,7 +26,7 @@ func main() {
一个函数的声明由一个函数名字、参数列表由函数的调用者提供参数变量的具体值、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值那么返回值列表是省略的。执行函数从函数的第一个语句开始依次顺序执行直到遇到return返回语句如果没有返回语句则是执行到函数末尾然后返回到函数调用者。
我们已经看到过很多函数声明和函数调用的例子了在第五章将深入讨论函数的相关细节这里只简单解释下。下面的fToC函数封装了温度转换的处理逻辑这样它只需要被定义一次就可以在多个地方多次被使用。在这个例子中main函数就调用了两次fToC函数分别使用在局部定义的两个常量作为调用函数的参数。
我们已经看到过很多函数声明和函数调用的例子了在第五章将深入讨论函数的相关细节这里只简单解释下。下面的fToC函数封装了温度转换的处理逻辑这样它只需要被定义一次就可以在多个地方多次被使用。在这个例子中main函数就调用了两次fToC函数分别使用在局部定义的两个常量作为调用函数的参数。
<u><i>gopl.io/ch2/ftoc</i></u>
```Go

View File

@ -8,7 +8,7 @@ freq := rand.Float64() * 3.0
t := 0.0
```
因为简洁和灵活的特点简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型地方或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
因为简洁和灵活的特点简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
```Go
i := 100 // an int
@ -26,7 +26,7 @@ i, j := 0, 1
但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用比如for语句的循环的初始化语句部分。
请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值§2.4.1),后者是将右边各个表达式值赋值给左边对应位置的各个变量:
请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值§2.4.1),后者是将右边各个表达式值赋值给左边对应位置的各个变量:
```Go
i, j = j, i // 交换 i 和 j 的值

View File

@ -55,7 +55,7 @@ incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
```
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,`*p`就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量但是这是一把双刃剑要找到一个变量的所有访问者并不容易我们必须知道变量全部的别名译注这是Go语言的垃圾回收器所做的工作。不仅仅是指针会创建别名很多其他引用类型也会创建别名例如slice、map和chan甚至结构体、数组和接口都会创建所引用变量的别名。
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,`*p`就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量但是这是一把双刃剑要找到一个变量的所有访问者并不容易我们必须知道变量全部的别名译注这是Go语言的垃圾回收器所做的工作。不仅仅是指针会创建别名很多其他引用类型也会创建别名例如slice、map和chan甚至结构体、数组和接口都会创建所引用变量的别名。
指针是实现标准库中flag包的关键技术它使用命令行参数来设置对应变量的值而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点在早些的echo版本中就包含了两个可选的命令行参数`-n`用于忽略行尾的换行符,`-s sep`用于指定分隔字符默认是空格。下面这是第四个版本对应包路径为gopl.io/ch2/echo4。
@ -82,9 +82,9 @@ func main() {
}
```
调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性第一个是命令行标志参数的名字“n”然后是该标志参数的默认值这里是false最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数或者输入`-h`或`-help`参数那么将打印所有标志参数的名字、默认值和描述信息。类似的调用flag.String函数将创建一个对应字符串类型的标志参数变量,同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的`sep`和`n`变量分别是指向对应命令行标志参数变量的指针,因此必须用`*sep`和`*n`形式的指针语法间接引用它们。
调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性第一个是命令行标志参数的名字“n”然后是该标志参数的默认值这里是false最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数或者输入`-h`或`-help`参数那么将打印所有标志参数的名字、默认值和描述信息。类似的调用flag.String函数将创建一个对应字符串类型的标志参数变量同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的`sep`和`n`变量分别是指向对应命令行标志参数变量的指针,因此必须用`*sep`和`*n`形式的指针语法间接引用它们。
当程序运行时必须在使用标志参数对应的变量之前先调用flag.Parse函数用于更新每个标志参数对应变量的值之前是默认值。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,返回值对应对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误默认将打印相关的提示信息然后调用os.Exit(2)终止程序。
当程序运行时必须在使用标志参数对应的变量之前先调用flag.Parse函数用于更新每个标志参数对应变量的值之前是默认值。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问返回值对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误默认将打印相关的提示信息然后调用os.Exit(2)终止程序。
让我们运行一些echo测试用例

View File

@ -1,6 +1,6 @@
### 2.3.3. new函数
另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量初始化为T类型的零值然后返回变量地址返回的指针类型为`*T`。
另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量初始化为T类型的零值然后返回变量地址返回的指针类型为`*T`。
```Go
p := new(int) // p, *int 类型, 指向匿名的 int 变量

View File

@ -1,6 +1,6 @@
### 2.3.4. 变量的生命周期
变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的声明周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
变量的生命周期指的是在程序运行期间变量有效存在的时间。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
例如下面是从1.4节的Lissajous程序摘录的代码片段
@ -13,7 +13,7 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
}
```
译注:函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样:
译注:函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样:
```Go
for t := 0.0; t < cycles*2*math.Pi; t += res {

View File

@ -53,7 +53,7 @@ v, ok = x.(T) // type assertion
v, ok = <-ch // channel receive
```
译注map查找§4.3、类型断言§7.10或通道接收§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于值产生一个结果的情形map查找失败时会返回零值类型断言失败时会发送运行时panic异常通道接收失败时会返回零值阻塞不算是失败。例如下面的例子
译注map查找§4.3、类型断言§7.10或通道接收§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于只产生一个结果的情形map查找失败时会返回零值类型断言失败时会发生运行时panic异常通道接收失败时会返回零值阻塞不算是失败。例如下面的例子
```Go
v = m[key] // map查找失败时返回零值

View File

@ -10,7 +10,7 @@
type 类型名字 底层类型
```
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部也可以使用。
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部也可以使用。
译注对于中文汉字Unicode标志都作为小写字母处理因此中文的命名默认不能导出不过国内的用户针对该问题提出了不同的看法根据RobPike的回复在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在 [Issue763](https://github.com/golang/go/issues/5763) 的回复:

View File

@ -4,9 +4,9 @@
不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。
句法块是由花括弧所包含的一系列语句就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块block的概念推广到包括其他声明的群组这些声明在代码中并未显式地使用花括号包裹起来我们称之为词法块。对全局的源代码来说存在一个整体的词法块称为全局词法块对于每个包每个for、if和switch语句也都对应词法块每个switch或select的分支也有独立的法块;当然也包括显式书写的词法块(花括弧包含的语句)。
句法块是由花括弧所包含的一系列语句就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块block的概念推广到包括其他声明的群组这些声明在代码中并未显式地使用花括号包裹起来我们称之为词法块。对全局的源代码来说存在一个整体的词法块称为全局词法块对于每个包每个for、if和switch语句也都对应词法块每个switch或select的分支也有独立的法块;当然也包括显式书写的词法块(花括弧包含的语句)。
声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量比如int、len和true等是在全局作用域的因此可以在整个程序中直接使用。任何在函数外部也就是包级语法域声明的名字可以在同一个包的任何源文件中访问的。对于导入的包例如tempconv导入的fmt包则是对应源文件级的作用域因此只能在当前的文件中访问导入的fmt包当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句比如tempconv.CToF函数中的变量c则是局部作用域的它只能在函数内部甚至只能是局部的某些部分访问。
声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量比如int、len和true等是在全局作用域的因此可以在整个程序中直接使用。任何在函数外部也就是包级语法域声明的名字可以在同一个包的任何源文件中访问的。对于导入的包例如tempconv导入的fmt包则是对应源文件级的作用域因此只能在当前的文件中访问导入的fmt包当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句比如tempconv.CToF函数中的变量c则是局部作用域的它只能在函数内部甚至只能是局部的某些部分访问。
控制流标号就是break、continue或goto语句后面跟着的那种标号则是函数级的作用域。
@ -44,7 +44,7 @@ func main() {
在`x[i]`和`x + 'A' - 'a'`声明语句的初始化的表达式中都引用了外部作用域声明的x变量稍后我们会解释这个。注意后面的表达式与unicode.ToUpper并不等价。
正如上面例子所示并不是所有的词法域都显式地对应到由花括弧包含的语句还有一些隐含的规则。上面的for语句创建了两个词法域花括弧包含的是显式的部分是for的循环体部分词法域另外一个隐式的部分则是循环的初始化部分比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分`i++`),当然也包含循环体词法域。
正如上面例子所示并不是所有的词法域都显式地对应到由花括弧包含的语句还有一些隐含的规则。上面的for语句创建了两个词法域花括弧包含的是显式的部分是for的循环体部分词法域另外一个隐式的部分则是循环的初始化部分比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分`i++`),当然也包含循环体词法域。
下面的例子同样有三个不同的x变量每个声明在不同的词法域一个在函数体词法域一个在for隐式的初始化词法域一个在for循环体词法域只有两个块是显式创建的
@ -71,7 +71,7 @@ if x := f(); x == 0 {
fmt.Println(x, y) // compile error: x and y are not visible here
```
第二个if语句嵌套在第一个内部因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则条件部分为一个隐式词法域然后每个是每个分支的词法域。
第二个if语句嵌套在第一个内部因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则条件部分为一个隐式词法域然后是每个分支的词法域。
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。
@ -85,7 +85,7 @@ f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f
```
变量f的作用域只在if语句内因此后面的语句将无法引入它这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示具体错误信息依赖编译器的实现。
变量f的作用域只在if语句内因此后面的语句将无法引入它这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示具体错误信息依赖编译器的实现。
通常需要在if之前声明变量这样可以确保后面的语句依然可以访问变量
@ -112,7 +112,7 @@ if f, err := os.Open(fname); err != nil {
但这不是Go语言推荐的做法Go语言的习惯是在if中处理错误然后直接返回这样可以确保正常执行的语句不需要代码缩进。
要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这可以本来通过直接调用os.Getwd完成但是将这个从主逻辑中分离出来可能会更好特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息然后调用os.Exit(1)终止程序。
要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这本来可以通过直接调用os.Getwd完成但是将这个从主逻辑中分离出来可能会更好特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息然后调用os.Exit(1)终止程序。
```Go
var cwd string
@ -127,7 +127,7 @@ func init() {
虽然cwd在外部已经声明过但是`:=`语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明因此上面的代码并不会正确更新包级声明的cwd变量。
由于当前的编译器会检测到局部声明的cwd并没有使用然后报告这可能是一个错误但是这种检测并不可靠。因为一些小的代码变更例如增加一个局部cwd的打印语句就可能导致这种检测失效。
由于当前的编译器会检测到局部声明的cwd并没有使用然后报告这可能是一个错误但是这种检测并不可靠。因为一些小的代码变更例如增加一个局部cwd的打印语句就可能导致这种检测失效。
```Go
var cwd string

View File

@ -28,7 +28,7 @@ Unicode字符rune类型是和int32等价的类型通常用于表示一个Unic
对于上表中前两行的运算符,例如+运算符还有一个与赋值相结合的对应运算符+=,可以用于简化赋值语句。
算术运算符`+`、`-`、`*`和`/`可以适用于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算。对于不同编程语言,%取模运算的行为可能并不相同。在Go语言中%取模运算符的符号和被取模数的符号总是一致的,因此`-5%3`和`-5%-3`结果都是-2。除法运算符`/`的行为则依赖于操作数是否全为整数,比如`5.0/4.0`的结果是1.25但是5/4的结果是1因为整数除法会向着0方向截断余数。
算术运算符`+`、`-`、`*`和`/`可以适用于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算。对于不同编程语言,%取模运算的行为可能并不相同。在Go语言中%取模运算符的符号和被取模数的符号总是一致的,因此`-5%3`和`-5%-3`结果都是-2。除法运算符`/`的行为则依赖于操作数是否全为整数,比如`5.0/4.0`的结果是1.25但是5/4的结果是1因为整数除法会向着0方向截断余数。
一个算术运算的结果不管是有符号或者是无符号的如果需要更多的bit位才能正确表示的话就说明计算结果是溢出了。超出的高位的bit位部分将被丢弃。如果原始的数值是有符号类型而且最左边的bit位是1的话那么最终结果可能是负的例如int8的例子
@ -101,11 +101,11 @@ fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}
6.5节给出了一个可以远大于一个字节的整数集的实现。)
在`x<<n``x>>n`移位运算中决定了移位操作bit数部分必须是无符号数被操作的x可以是有符号或无符号数。算术上,一个`x<<n`$2^n$`x>>n`右移运算等价于除以$2^n$。
在`x<<n``x>>n`移位运算中,决定了移位操作bit数部分必须是无符号数被操作的x可以是有符号或无符号数。算术上,一个`x<<n`$2^n$`x>>n`右移运算等价于除以$2^n$。
左移运算用零填充右边空缺的bit位无符号数的右移运算也是用0填充左边空缺的bit位但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。因为这个原因最好用无符号运算这样你可以将整数完全当作一个bit位模式处理。
尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型就像数组的长度那样虽然使用uint无符号类型似乎是一个更合理的选择。事实上内置的len函数返回一个有符号的int我们可以像下面例子那样处理逆序循环。
尽管Go语言提供了无符号数的运算,但即使数值本身不可能出现负数,我们还是倾向于使用有符号的int类型就像数组的长度那样虽然使用uint无符号类型似乎是一个更合理的选择。事实上内置的len函数返回一个有符号的int我们可以像下面例子那样处理逆序循环。
```Go
medals := []string{"gold", "silver", "bronze"}
@ -118,9 +118,9 @@ for i := len(medals) - 1; i >= 0; i-- {
出于这个原因无符号数往往只有在位运算或其它特殊的运算场景才会使用就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。
一般来说,需要一个显式的转换将一个值从一种类型转化另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。
一般来说,需要一个显式的转换将一个值从一种类型转化另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。
在很多场景,会遇到类似下面的代码通用的错误:
在很多场景,会遇到类似下面代码的常见的错误:
```Go
var apples int32 = 1
@ -150,7 +150,7 @@ f = 1.99
fmt.Println(int(f)) // "1"
```
浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。你应该避免对可能会超出目标类型表示范围的数值类型转换,因为截断的行为可能依赖于具体的实现:
浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。你应该避免对可能会超出目标类型表示范围的数值类型转换,因为截断的行为可能依赖于具体的实现:
```Go
f := 1e100 // a float64

View File

@ -135,9 +135,9 @@ 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)的值。
第二个坐标系是一个三维的网格浮点坐标(x,y,z)其中x和y是i和j的线性函数通过平移转换网格单元的中心然后用xyrange系数缩放。高度z是函数f(x,y)的值。
第三个坐标系是一个二维的画布,起点(0,0)在左上角。画布中点的坐标用(sx, sy)表示。我们使用等角投影将三维点

View File

@ -81,7 +81,7 @@ func mandelbrot(z complex128) color.Color {
}
```
用于遍历1024x1024图像每个点的两个嵌套的循环对应-2到+2区间的复数平面。程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。如果超过了通过根据预设置的逃逸迭代次数对应的灰度颜色来代替。如果不是那么该点属于Mandelbrot集合使用黑色颜色标记。最终程序将生成的PNG格式分形图像图像输出到标准输出如图3.3所示。
用于遍历1024x1024图像每个点的两个嵌套的循环对应-2到+2区间的复数平面。程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。如果超过了通过根据预设置的逃逸迭代次数对应的灰度颜色来代替。如果不是那么该点属于Mandelbrot集合使用黑色颜色标记。最终程序将生成的PNG格式分形图像输出到标准输出如图3.3所示。
![](../images/ch3-03.png)
@ -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参数指定x,y和zoom参数。

View File

@ -1,6 +1,6 @@
### 3.5.1. 字符串面值
字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号即可:
字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号即可:
```
"Hello, 世界"
@ -25,9 +25,9 @@
\\ 反斜杠
```
可以通过十六进制或八进制转义在字符串面值包含任意的字节。一个十六进制的转义形式是`\xhh`其中两个h表示十六进制数字大写或小写都可以。一个八进制转义形式是`\ooo`包含三个八进制的o数字0到7但是不能超过`\377`译注对应一个字节的范围十进制为255。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。
可以通过十六进制或八进制转义在字符串面值包含任意的字节。一个十六进制的转义形式是`\xhh`其中两个h表示十六进制数字大写或小写都可以。一个八进制转义形式是`\ooo`包含三个八进制的o数字0到7但是不能超过`\377`译注对应一个字节的范围十进制为255。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。
一个原生的字符串面值形式是\`...\`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写\`字符的,可以用八进制或十六进制转义或+"\`"接字符串常量完成。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的包括那些把回车也放入文本文件的系统译注Windows系统会把回车和换行一起放入文本文件中
一个原生的字符串面值形式是\`...\`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写\`字符的,可以用八进制或十六进制转义或+"\`"接字符串常量完成。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的包括那些把回车也放入文本文件的系统译注Windows系统会把回车和换行一起放入文本文件中
原生字符串面值用于编写正则表达式会很方便因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。

View File

@ -4,6 +4,6 @@
答案就是使用Unicode http://unicode.org 它收集了这个世界上所有的符号系统包括重音符号和其它变音符号制表符和回车符还有很多神秘的符号每个符号都分配一个唯一的Unicode码点Unicode码点对应Go语言中的rune整数类型译注rune是int32等价类型
在第八版本的Unicode标准收集了超过120,000个字符涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢通用的表示一个Unicode码点的数据类型是int32也就是Go语言中rune对应的类型它的同义词rune符文正是这个意思。
在第八版本的Unicode标准收集了超过120,000个字符涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢通用的表示一个Unicode码点的数据类型是int32也就是Go语言中rune对应的类型它的同义词rune符文正是这个意思。
我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4每个Unicode码点都使用同样大小32bit来表示。这种方式比较简单统一但是它会浪费很多存储空间因为大数计算机可读的文本是ASCII字符本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个也就是说用16bit编码方式就能表达常用字符。但是还有其它更好的编码方法吗
我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4每个Unicode码点都使用同样大小32bit来表示。这种方式比较简单统一但是它会浪费很多存储空间因为大数计算机可读的文本是ASCII字符本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个也就是说用16bit编码方式就能表达常用字符。但是还有其它更好的编码方法吗

View File

@ -1,6 +1,6 @@
### 3.5.3. UTF-8
UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码由Go语言之父Ken Thompson和Rob Pike共同发明的现在已经是Unicode的标准。UTF8编码使用1到4个字节来表示每个Unicode码点ASCII部分字符只使用1个字节常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示总共有多少编码个字节。如果第一个字节的高端bit为0则表示对应7bit的ASCII字符ASCII字符每个字符依然是一个字节和传统的ASCII编码兼容。如果第一个字节的高端bit是110则说明需要2个字节后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。
UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码由Go语言之父Ken Thompson和Rob Pike共同发明的现在已经是Unicode的标准。UTF8编码使用1到4个字节来表示每个Unicode码点ASCII部分字符只使用1个字节常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0则表示对应7bit的ASCII字符ASCII字符每个字符依然是一个字节和传统的ASCII编码兼容。如果第一个字节的高端bit是110则说明需要2个字节后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。
```
0xxxxxxx runes 0-127 (ASCII)
@ -11,7 +11,7 @@ UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码
变长的编码无法直接通过索引来访问第n个字符但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑完全兼容ASCII码并且可以自动同步它可以通过向前回朔最多2个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码所以当从左向右解码时不会有任何歧义也并不需要向前查看译注像GBK之类的编码如果不知道起点位置则可能会出现歧义。没有任何字符的编码是其它字符编码的子串或是其它编码序列的字串因此搜索一个字符时只要搜索它的字节编码序列即可不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节可以很好地兼容那些使用NUL作为字符串结尾的编程语言。
Go语言的源文件采用UTF8编码并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数比如区分字母和数或者是字母的大写和小写转换等unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。
Go语言的源文件采用UTF8编码并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数比如区分字母和数或者是字母的大写和小写转换等unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。
有很多Unicode字符很难直接从键盘输入并且还有很多字符有着相似的结构有一些甚至是不可见的字符译注中文和日文就有很多相似但不同的字。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式`\uhhhh`对应16bit的码点值`\Uhhhhhhhh`对应32bit的码点值其中h是一个十六进制数字一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。例如下面的字母串面值都表示相同的值
@ -30,7 +30,7 @@ Unicode转义也可以使用在rune字符中。下面三个字符是等价的
'世' '\u4e16' '\U00004e16'
```
对于小于256码点值可以写在一个十六进制转义字节中例如`\x41`对应字符'A',但是对于更大的码点则必须使用`\u`或`\U`转义形式。因此,`\xe4\xb8\x96`并不是一个合法的rune字符虽然这三个字节对应一个有效的UTF8编码的码点。
对于小于256码点值可以写在一个十六进制转义字节中,例如`\x41`对应字符'A',但是对于更大的码点则必须使用`\u`或`\U`转义形式。因此,`\xe4\xb8\x96`并不是一个合法的rune字符虽然这三个字节对应一个有效的UTF8编码的码点。
得益于UTF8编码优良的设计诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀
@ -63,7 +63,7 @@ func Contains(s, substr string) bool {
对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。上面的函数都来自strings字符串处理包真实的代码包含了一个用哈希技术优化的Contains 实现。)
另一方面如果我们真的关心每个Unicode字符我们可以使用其它处理方式。考虑前面的第一个例子中的字符串混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节以UTF8形式编码但是只对应9个Unicode字符
另一方面如果我们真的关心每个Unicode字符我们可以使用其它处理方式。考虑前面的第一个例子中的字符串它混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节以UTF8形式编码但是只对应9个Unicode字符
```Go
import "unicode/utf8"
@ -113,9 +113,9 @@ for range s {
或者我们可以直接调用utf8.RuneCountInString(s)函数。
正如我们前面提到的文本字符串采用UTF8编码只是一种惯例但是对于循环的真正字符串并不是一个惯例这是正确的。如果用于循环的字符串只是一个普通的二进制数据或者是含有错误编码的UTF8数据将会发什么呢?
正如我们前面提到的文本字符串采用UTF8编码只是一种惯例但是对于循环的真正字符串并不是一个惯例这是正确的。如果用于循环的字符串只是一个普通的二进制数据或者是含有错误编码的UTF8数据将会发什么呢?
每一个UTF8字符解码不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码如果遇到一个错误的UTF8编码输入将生成一个特别的Unicode字符`\uFFFD`,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"<EFBFBD>"。当程序遇到这样的一个字符通常是一个危险信号说明输入并不是一个完美没有错误的UTF8字符串。
每一个UTF8字符解码不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码如果遇到一个错误的UTF8编码输入将生成一个特别的Unicode字符`\uFFFD`,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。当程序遇到这样的一个字符通常是一个危险信号说明输入并不是一个完美没有错误的UTF8字符串。
UTF8字符串作为交换格式是非常方便的但是在程序内部采用rune序列可能更方便因为rune大小一致支持数组索引和方便切割。
@ -147,5 +147,5 @@ fmt.Println(string(0x4eac)) // "京"
如果对应码点的字符是无效的,则用`\uFFFD`无效字符作为替换:
```Go
fmt.Println(string(1234567)) // "<EFBFBD>"
fmt.Println(string(1234567)) // "?"
```

View File

@ -8,7 +8,7 @@ strconv包提供了布尔型、整型数、浮点数和对应字符串的相互
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能它们用于给字符分类。每个函数有一个单一的rune类型的参数然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数它们是ToUpper和ToLower将原始字符串的每个字符都做相应的转换然后返回新的字符串。
下面例子的basename函数灵感于Unix shell的同名工具。在我们实现的版本中basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除:
下面例子的basename函数灵感于Unix shell的同名工具。在我们实现的版本中basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除:
```Go
fmt.Println(basename("a/b/c.go")) // "c"
@ -41,7 +41,7 @@ func basename(s string) string {
}
```
简化版本使用了strings.LastIndex库函数
这个简化版本使用了strings.LastIndex库函数
<u><i>gopl.io/ch3/basename2</i></u>
```Go
@ -57,7 +57,7 @@ func basename(s string) string {
path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名但是在其他一些领域可能会用于文件名例如URL路径组件。相比之下path/filepath包则使用操作系统本身的路径规则例如POSIX系统使用/foo/bar而Microsoft Windows使用`c:\foo\bar`等。
让我们继续另一个字符串的例子。函数的功能是将一个表示整值的字符串每隔三个字符插入一个逗号分隔符例如“12345”处理后成为“12,345”。这个版本只适用于整数类型支持浮点数类型的支持留作练习。
让我们继续另一个字符串的例子。函数的功能是将一个表示整值的字符串每隔三个字符插入一个逗号分隔符例如“12345”处理后成为“12,345”。这个版本只适用于整数类型支持浮点数类型的留作练习。
<u><i>gopl.io/ch3/comma</i></u>
```Go
@ -71,9 +71,9 @@ func comma(s string) string {
}
```
输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话则不需要插入逗分隔符。否则comma函数将在最后三个字符前位置将字符串切割为两个两个子串并插入逗号分隔符,然后通过递归调用自身来出前面的子串。
输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话则不需要插入逗分隔符。否则comma函数将在最后三个字符前位置将字符串切割为两个子串并插入逗号分隔符,然后通过递归调用自身来出前面的子串。
一个字符串是包含只读字节数组一旦创建是不可变的。相比之下一个字节slice的元素则可以自由地修改。
一个字符串是包含只读字节数组一旦创建是不可变的。相比之下一个字节slice的元素则可以自由地修改。
字符串和字节slice之间可以相互转换
@ -83,7 +83,7 @@ b := []byte(s)
s2 := string(b)
```
从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据但总的来说需要确保在变量b被修改的情况下原始的s字符串也不会改变。将一个字节slice转到字符串的string(b)操作则是构造一个字符串拷贝以确保s2字符串是只读的。
从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据但总的来说需要确保在变量b被修改的情况下原始的s字符串也不会改变。将一个字节slice转到字符串的string(b)操作则是构造一个字符串拷贝以确保s2字符串是只读的。
为了避免转换中不必要的内存分配bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数
@ -140,4 +140,4 @@ bytes.Buffer类型有着很多实用的功能我们在第七章讨论接口
**练习 3.11** 完善comma函数以支持浮点数处理和一个可选的正负号的处理。
**练习 3.12** 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。
**练习 3.12** 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。

View File

@ -16,7 +16,7 @@ FormatInt和FormatUint函数可以用不同的进制来格式化数字
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
```
fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多特别是在需要包含附加额外信息的时候
fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多特别是在需要包含附加额外信息的时候:
```Go
s := fmt.Sprintf("x=%b", x) // "x=1111011"

View File

@ -34,7 +34,7 @@ fmt.Println(s[7:]) // "world"
fmt.Println(s[:]) // "hello, world"
```
其中+操作符将两个字符串接构造一个新字符串:
其中+操作符将两个字符串接构造一个新字符串:
```Go
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
@ -63,7 +63,7 @@ fmt.Println(t) // "left foot"
s[0] = 'L' // compile error: cannot assign to s[0]
```
不变性意味如果两个字符串共享相同的底层数据的话也是安全的这使得复制任何长度的字符串代价是低廉的。同样一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。
不变性意味如果两个字符串共享相同的底层数据的话也是安全的这使得复制任何长度的字符串代价是低廉的。同样一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。
{% include "./ch3-05-1.md" %}

View File

@ -1,8 +1,8 @@
### 3.6.2. 无类型常量
Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型例如int或float64或者是类似time.Duration这样命名的基础类型但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型例如int或float64或者是类似time.Duration这样命名的基础类型但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
通过延迟明确常量的具体类型无类型的常量不仅可以提供更高的运算精度而且可以直接用于更多的表达式而不需要显式的类型转换。例如例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围但是它们依然是合法的常量而且可以像下面常量表达式依然有效译注YiB/ZiB是在编译期计算出来的并且结果常量是1024是Go语言int变量能有效表示的
通过延迟明确常量的具体类型无类型的常量不仅可以提供更高的运算精度而且可以直接用于更多的表达式而不需要显式的类型转换。例如例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围但是它们依然是合法的常量而且像下面常量表达式依然有效译注YiB/ZiB是在编译期计算出来的并且结果常量是1024是Go语言int变量能有效表示的
```Go
fmt.Println(YiB/ZiB) // "1024"

View File

@ -1,6 +1,6 @@
## 4.1. 数组
数组是一个由固定长度的特定类型元素组成的序列一个数组可以由零个或多个元素组成。因为数组的长度是固定的因此在Go语言中很少直接使用数组。和数组对应的类型是Slice切片它是可以增长和收缩动态序列slice功能也更灵活但是要理解slice工作原理的话需要先理解数组。
数组是一个由固定长度的特定类型元素组成的序列一个数组可以由零个或多个元素组成。因为数组的长度是固定的因此在Go语言中很少直接使用数组。和数组对应的类型是Slice切片它是可以增长和收缩动态序列slice功能也更灵活但是要理解slice工作原理的话需要先理解数组。
数组的每个元素可以通过索引下标来访问索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。

View File

@ -105,7 +105,7 @@ x = append(x, x...) // append the slice x
fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"
```
通过下面的小修改,我们可以可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。我们将在5.7节详细解释这个特性。
通过下面的小修改我们可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。我们将在5.7节详细解释这个特性。
```Go
func appendInt(x []int, y ...int) []int {

View File

@ -77,7 +77,7 @@ reverse(s)
fmt.Println(s) // "[2 3 4 5 0 1]"
```
要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似它们都是用花括弧包含一系列的初始化元素但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组然后slice的指针指向底层的数组。就像数组字面值一样slice的字面值也可以按顺序指定初始化值序列或者是通过索引和元素值指定或者两种风格的混合语法初始化。
要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似它们都是用花括弧包含一系列的初始化元素但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组然后slice的指针指向底层的数组。就像数组字面值一样slice的字面值也可以按顺序指定初始化值序列或者是通过索引和元素值指定或者两种风格的混合语法初始化。
和数组不同的是slice之间不能比较因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等[]byte但是对于其他类型的slice我们必须自己展开每个元素进行比较

View File

@ -115,7 +115,7 @@ ages["carol"] = 21 // panic: assignment to entry in nil map
在向map存数据前必须先创建map。
通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的那么将得到与key对应的value如果key不存在那么将得到value对应类型的零值正如我们前面看到的ages["bob"]那样。这个规则很实用但是有时候可能需要知道对应的元素是否真的是在map之中。例如如果元素类型是一个数字你可需要区分一个已经存在的0和不存在而返回零值的0可以像下面这样测试
通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的那么将得到与key对应的value如果key不存在那么将得到value对应类型的零值正如我们前面看到的ages["bob"]那样。这个规则很实用但是有时候可能需要知道对应的元素是否真的是在map之中。例如如果元素类型是一个数字你可需要区分一个已经存在的0和不存在而返回零值的0可以像下面这样测试
```Go
age, ok := ages["bob"]
@ -192,7 +192,7 @@ func Count(list []string) int { return m[k(list)] }
使用同样的技术可以处理任何不可比较的key类型而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用例如在比较字符串的时候忽略大小写。同时辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。
这是map的另一个例子下面的程序用于统计输入中每个Unicode码点出现的次数。虽然Unicode全部码点的数量巨大但是出现在特定文档中的字符种类并没有多少使用map可以用比较自然的方式来跟踪那些出现过字符的次数。
这是map的另一个例子下面的程序用于统计输入中每个Unicode码点出现的次数。虽然Unicode全部码点的数量巨大但是出现在特定文档中的字符种类并没有多少使用map可以用比较自然的方式来跟踪那些出现过字符的次数。
<u><i>gopl.io/ch4/charcount</i></u>
```Go

View File

@ -16,7 +16,7 @@ p := Point{1, 2}
anim := gif.GIF{LoopCount: nframes}
```
在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为,提供了成员的名字,所有成员出现的顺序并不重要。
在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。
两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。
@ -64,7 +64,7 @@ func AwardAnnualRaise(e *Employee) {
pp := &Point{1, 2}
```
下面的语句是等价的
下面的语句是等价的
```Go
pp := new(Point)

View File

@ -66,7 +66,7 @@ type Wheel struct {
}
```
于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
```Go
var w Wheel
@ -112,14 +112,14 @@ fmt.Printf("%#v\n", w)
需要注意的是Printf函数中%v参数包含的#副词它表示用和Go语言类似的语法打印值。对于结构体类型来说将包含每个成员的名字。
因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所匿名成员也有可见性的规则约束。在上面的例子中Point和Circle匿名成员都是导出的。即使它们不导出比如改成小写字母开头的point和circle我们依然可以用简短形式访问匿名成员嵌套的成员
因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所匿名成员也有可见性的规则约束。在上面的例子中Point和Circle匿名成员都是导出的。即使它们不导出比如改成小写字母开头的point和circle我们依然可以用简短形式访问匿名成员嵌套的成员
```Go
w.X = 8 // equivalent to w.circle.point.X = 8
```
但是在包外部因为circle和point没有导出不能访问它们的成员因此简短的匿名成员访问语法也是禁止的。
但是在包外部因为circle和point没有导出不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。
到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?
答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心我们将在6.3节中专门讨论。
答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心我们将在6.3节中专门讨论。

View File

@ -1,6 +1,6 @@
## 4.4. 结构体
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。
下面两个语句声明了一个叫Employee的命名的结构体类型并且声明了一个Employee类型的变量dilbert
@ -76,7 +76,7 @@ type Employee struct {
结构体类型往往是冗长的因为它的每个成员可能都会占一行。虽然我们每次都可以重写整个结构体成员但是重复会令人厌烦。因此完整的结构体写法通常只在类型声明语句的地方出现就像Employee类型声明语句那样。
一个命名为S的结构体类型将不能再包含S类型的成员因为一个聚合的值不能包含它自身。该限制同样适于数组。但是S类型的结构体可以包含`*S`指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:
一个命名为S的结构体类型将不能再包含S类型的成员因为一个聚合的值不能包含它自身。该限制同样适于数组。但是S类型的结构体可以包含`*S`指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:
<u><i>gopl.io/ch4/treesort</i></u>
```Go

View File

@ -8,7 +8,7 @@ JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和
基本的JSON类型有数字十进制或科学记数法、布尔值true或false、字符串其中字符串是以双引号包含的Unicode字符序列支持和Go语言类似的反斜杠转义特性不过JSON使用的是`\Uhhhh`转义数字来表示一个UTF-16编码译注UTF-16和UTF-8一样是一种变长的编码有些Unicode码点较大的字符需要用4个字节表示而且UTF-16还有大端和小端的问题而不是Go语言的rune类型。
这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的值序列写在一个方括号中并以逗号分隔一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射写成系列的name:value对形式用花括号包含并以逗号分隔JSON的对象类型可以用于编码Go语言的map类型key类型是字符串和结构体。例如
这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的值序列写在一个方括号中并以逗号分隔一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射写成系列的name:value对形式用花括号包含并以逗号分隔JSON的对象类型可以用于编码Go语言的map类型key类型是字符串和结构体。例如
```
boolean true
@ -42,7 +42,7 @@ var movies = []Movie{
}
```
这样的数据结构特别适合JSON格式并且在两之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组marshaling。编组通过调用json.Marshal函数完成
这样的数据结构特别适合JSON格式并且在两之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组marshaling。编组通过调用json.Marshal函数完成
```Go
data, err := json.Marshal(movies)
@ -105,16 +105,16 @@ fmt.Printf("%s\n", data)
在编码时默认使用Go语言结构体的成员名字作为JSON的对象通过reflect反射技术我们将在12.6节讨论)。只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。
细心的读者可能已经注意到其中Year名字的成员在编码后变成了released还有Color成员编码后变成了小写字母开头的color。这是因为构体成员Tag所导致的。一个构体成员Tag是和在编译阶段关联到该成员的元信息字符串
细心的读者可能已经注意到其中Year名字的成员在编码后变成了released还有Color成员编码后变成了小写字母开头的color。这是因为构体成员Tag所导致的。一个构体成员Tag是和在编译阶段关联到该成员的元信息字符串
```
Year int `json:"released"`
Color bool `json:"color,omitempty"`
```
结构体的成员Tag可以是任意的字符串面值但是通常是一系列用空格分隔的key:"value"键值对序列;因为值中含双引号字符因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为并且encoding/...下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项表示当Go语言结构体成员为空或零值时不生成JSON对象这里false为零值。果然Casablanca是一个黑白电影并没有输出Color成员。
结构体的成员Tag可以是任意的字符串面值但是通常是一系列用空格分隔的key:"value"键值对序列;因为值中含双引号字符因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为并且encoding/...下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项表示当Go语言结构体成员为空或零值时不生成JSON对象这里false为零值。果然Casablanca是一个黑白电影并没有输出Color成员。
编码的逆操作是解码对应将JSON数据解码为Go语言的数据结构Go语言中一般叫unmarshaling通过json.Unmarshal函数完成。下面的代码将JSON格式的电影数据解码为一个结构体slice结构体中只有Title成员。通过定义合适的Go语言数据结构我们可以选择性地解码JSON中感兴趣的成员。当Unmarshal函数调用返回slice将被只含有Title信息值填充其它JSON成员将被忽略。
编码的逆操作是解码对应将JSON数据解码为Go语言的数据结构Go语言中一般叫unmarshaling通过json.Unmarshal函数完成。下面的代码将JSON格式的电影数据解码为一个结构体slice结构体中只有Title成员。通过定义合适的Go语言数据结构我们可以选择性地解码JSON中感兴趣的成员。当Unmarshal函数调用返回slice将被只含有Title信息值填充其它JSON成员将被忽略。
```Go
var titles []struct{ Title string }

View File

@ -1,6 +1,6 @@
# 第四章 复合数据类型
在第三章我们讨论了基本数据类型它们可以用于构建程序中数据结构是Go语言世界的原子。在本章,我们将讨论复合数据类型,它是以不同的方式组合基本类型可以构造出来的复合数据类型。我们主要讨论四种类型——数组、slice、map和结构体——同时在本章的最后我们将演示如何使用结构体来解码和编码到对应JSON格式的数据并且通过结合使用模板来生成HTML页面。
在第三章我们讨论了基本数据类型,它们可以用于构建程序中数据结构是Go语言世界的原子。在本章我们将讨论复合数据类型它是以不同的方式组合基本类型构造出来的复合数据类型。我们主要讨论四种类型——数组、slice、map和结构体——同时在本章的最后我们将演示如何使用结构体来解码和编码到对应JSON格式的数据并且通过结合使用模板来生成HTML页面。
数组和结构体是聚合类型它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。数组和结构体都是有固定内存大小的数据结构。相比之下slice和map则是动态的数据结构它们将根据需要动态增长。

View File

@ -18,7 +18,7 @@ func hypot(x, y float64) float64 {
fmt.Println(hypot(3,4)) // "5"
```
x和y是形参名,3和4是调用时的传入的实函数返回了一个float64类型的值。
x和y是形参名,3和4是调用时的传入的实函数返回了一个float64类型的值。
返回值也可以像形式参数一样被命名。在这种情况下每个返回值被声明成一个局部变量并根据该返回值的类型将其初始化为0。
如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。
@ -43,7 +43,7 @@ fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"
```
函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。
函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。
每一次函数调用都必须按照声明顺序为所有参数提供实参参数值。在函数调用时Go语言没有默认参数值也没有任何方法可以通过参数名指定形参因此形参和返回值的变量名对于函数调用者而言没有意义。

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

View File

@ -55,7 +55,7 @@ links, err := findLinks(url)
links, _ := findLinks(url) // errors ignored
```
一个函数内部可以将另一个有多返回值的函数作为返回值下面的例子展示了与findLinks有相同功能的函数两者的区别在于下面的例子先输出参数
一个函数内部可以将另一个有多返回值的函数调用作为返回值下面的例子展示了与findLinks有相同功能的函数两者的区别在于下面的例子先输出参数
```Go
func findLinksLog(url string) ([]string, error) {
@ -64,7 +64,7 @@ func findLinksLog(url string) ([]string, error) {
}
```
当你调用接受多参数的函数时可以将一个返回多参数的函数作为该函数的参数。虽然这很少出现在实际生产代码中但这个特性在debug时很方便我们只需要一条语句就可以输出所有的返回值。下面的代码是等价的
当你调用接受多参数的函数时,可以将一个返回多参数的函数调用作为该函数的参数。虽然这很少出现在实际生产代码中但这个特性在debug时很方便我们只需要一条语句就可以输出所有的返回值。下面的代码是等价的
```Go
log.Println(findLinks(url))
@ -82,7 +82,7 @@ func HourMinSec(t time.Time) (hour, minute, second int)
虽然良好的命名很重要但你也不必为每一个返回值都取一个适当的名字。比如按照惯例函数的最后一个bool类型的返回值表示函数是否运行成功error类型的返回值代表函数的错误信息对于这些类似的惯例我们不必思考合适的命名它们都无需解释。
如果一个函数将所有的返回值都显示的变量名那么该函数的return语句可以省略操作数。这称之为bare return。
如果一个函数所有的返回值都有显式的变量名那么该函数的return语句可以省略操作数。这称之为bare return。
```Go
// CountWordsAndImages does an HTTP GET request for the HTML
@ -96,7 +96,7 @@ func CountWordsAndImages(url string) (words, images int, err error) {
resp.Body.Close()
if err != nil {
err = fmt.Errorf("parsing HTML: %s", err)
return
return
}
words, images = countWordsAndImages(doc)
return

View File

@ -1,6 +1,6 @@
### 5.4.1. 错误处理策略
当一次函数调用返回错误时,调用者有应该选择何时的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。
当一次函数调用返回错误时,调用者应该选择合适的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。
首先也是最常用的方式是传播错误。这意味着函数中某个子程序的失败会变成该函数的失败。下面我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败findLinks会直接将这个HTTP错误返回给调用者
@ -21,7 +21,7 @@ if err != nil {
}
```
fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数前缀添加额外的上下文信息到原始错误信息。当错误最终由main函数处理时错误信息应提供清晰的从原因到后果的因果链就像美国宇航局事故调查时做的那样
fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数添加额外的前缀上下文信息到原始错误信息。当错误最终由main函数处理时错误信息应提供清晰的从原因到后果的因果链就像美国宇航局事故调查时做的那样
```
genesis: crashed: no parachute: G-switch failed: bad relay orientation
@ -31,9 +31,9 @@ genesis: crashed: no parachute: G-switch failed: bad relay orientation
编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。
OS包为例OS包确保文件操作如os.Open、Read、Write、Close返回的每个错误的描述不仅仅包含错误的原因如无权限文件目录不存在也包含文件名这样调用者在构造新的错误信息时无需再添加这些信息。
os包为例os包确保文件操作如os.Open、Read、Write、Close返回的每个错误的描述不仅仅包含错误的原因如无权限文件目录不存在也包含文件名这样调用者在构造新的错误信息时无需再添加这些信息。
一般而言被调函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者调用者需要添加一些错误信息中不包含的信息比如添加url到html.Parse返回的错误中。
一般而言,被调函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者调用者需要添加一些错误信息中不包含的信息比如添加url到html.Parse返回的错误中。
让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
@ -118,6 +118,6 @@ if err != nil {
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
```
尽管os.RemoveAll会失败但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此虽然程序没有处理错误但程序的逻辑不会因此受到影响。我们应该在每次函数调用后都养成考虑错误处理的习惯当你决定忽略某个错误时你应该在清晰的记录下你的意图。
尽管os.RemoveAll会失败但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此虽然程序没有处理错误但程序的逻辑不会因此受到影响。我们应该在每次函数调用后都养成考虑错误处理的习惯当你决定忽略某个错误时你应该清晰地写下你的意图。
在Go中错误处理有一套独特的编码风格。检查某个子函数是否失败后我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回那么成功时的逻辑代码不应放在else语句块中而应直接放在函数体中。Go中大部分函数的代码结构几乎相同首先是一系列的初始检查防止错误发生之后是函数的实际逻辑。

View File

@ -3,7 +3,7 @@
在Go中有一部分函数总是能成功的运行。比如strings.Contains和strconv.FormatBool函数对各种可能的输入都做了良好的处理使得运行时几乎不会失败除非遇到灾难性的、不可预料的情况比如运行时的内存溢出。导致这种错误的原因很复杂难以处理从错误中恢复的可能性也很低。
还有一部分函数只要输入的参数满足一定条件也能保证运行成功。比如time.Date函数该函数将年月日等参数构造成time.Time对象除非最后一个参数时区是nil。这种情况下会引发panic异常。panic是来自被调函数的信号表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。
还有一部分函数只要输入的参数满足一定条件也能保证运行成功。比如time.Date函数该函数将年月日等参数构造成time.Time对象除非最后一个参数时区是nil。这种情况下会引发panic异常。panic是来自被调函数的信号表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。
对于大部分函数而言永远无法确保能否成功运行。这是因为错误的原因超出了程序员的控制。举个例子任何进行I/O操作的函数都会面临出现错误的可能只有没有经验的程序员才会相信读写操作不会失败即使是简单的读写。因此当本该可信的操作出乎意料的失败后我们必须弄清楚导致失败的原因。
@ -31,9 +31,9 @@ fmt.Printf("%v", err)
在Go中函数运行失败时会返回错误信息这些错误信息被认为是一种预期的值而非异常exception这使得Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制但这些机制仅被使用在处理那些未被预料到的错误即bug而不是那些在健壮程序中应该被避免的程序错误。对于Go的异常机制我们将在5.9介绍。
Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言将这个错误以异常的形式抛出会混乱对错误的描述这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后这个错误会将堆栈根据信息返回给终端用户,这些信息复杂且无用,无法帮助定位错误。
Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言将这个错误以异常的形式抛出会混乱对错误的描述这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后这个错误会将堆栈跟踪信息返回给终端用户,这些信息复杂且无用,无法帮助定位错误。
正因此Go使用控制流机制如if和return处理异常,这使得编码人员能更多的关注错误处理。
正因此Go使用控制流机制如if和return处理错误,这使得编码人员能更多的关注错误处理。
{% include "./ch5-04-1.md" %}

View File

@ -45,7 +45,7 @@
fmt.Println(strings.Map(add1, "Admix")) // "Benjy"
```
5.2节的findLinks函数使用了辅助函数visit,遍历和操作了HTML页面的所有结点。使用函数值我们可以将遍历结点的逻辑和操作结点的逻辑分离使得我们可以复用遍历的逻辑从而对结点进行不同的操作。
5.2节的findLinks函数使用了辅助函数visit遍历和操作了HTML页面的所有结点。使用函数值我们可以将遍历结点的逻辑和操作结点的逻辑分离使得我们可以复用遍历的逻辑从而对结点进行不同的操作。
<u><i>gopl.io/ch5/outline2</i></u>
```Go
@ -84,7 +84,7 @@ func endElement(n *html.Node) {
}
```
上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。`%*s`中的`*`会在字符串之前填充一些空格。在例子中,每次输出会先填充`depth*2`数量的空格,再输出""最后再输出HTML标签。
上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。`%*s`中的`*`会在字符串之前填充一些空格。在例子中每次输出会先填充`depth*2`数量的空格,再输出""最后再输出HTML标签。
如果我们像下面这样调用forEachNode

View File

@ -2,7 +2,7 @@
本节将介绍Go词法作用域的一个陷阱。请务必仔细的阅读弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。
考虑这样一个问题你被要求首先创建一些目录再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单我们忽略了所有的异常处理。
考虑这样一个问题你被要求首先创建一些目录再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单我们忽略了所有的异常处理。
```Go
var rmdirs []func()

View File

@ -1,6 +1,6 @@
## 5.6. 匿名函数
拥有函数名的函数只能在包级语法块中被声明通过函数字面量function literal我们可绕过这一限制在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似区别在于func关键字后没有函数名。函数值字面量是一种表达式它的值被为匿名函数anonymous function
拥有函数名的函数只能在包级语法块中被声明通过函数字面量function literal我们可绕过这一限制在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似区别在于func关键字后没有函数名。函数值字面量是一种表达式它的值被为匿名函数anonymous function
函数字面量允许我们在使用函数时再定义它。通过这种技巧我们可以改写之前对strings.Map的调用
@ -30,7 +30,7 @@ func main() {
}
```
函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用匿名函数时该函数都会先使x的值加1再返回x的平方。第二次调用squares时会生成第二个x变量并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。
函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用匿名函数时该函数都会先使x的值加1再返回x的平方。第二次调用squares时会生成第二个x变量并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。
squares的例子证明函数值不仅仅是一串代码还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量这意味着匿名函数和squares中存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包closures技术实现函数值Go程序员也把函数值叫做闭包。

View File

@ -1,6 +1,6 @@
## 5.7. 可变参数
参数数量可变的函数称为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个必备参数,之后接收任意个数的后续参数。
参数数量可变的函数称为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个必备参数,之后接收任意个数的后续参数。
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。
@ -23,7 +23,7 @@ fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
```
在上面的代码中调用者隐式的创建一个数组并将原始参数复制到数组中再把数组的一个切片作为参数传给被调函数。如果原始参数已经是切片类型我们该如何传递给sum只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。
在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数。如果原始参数已经是切片类型我们该如何传递给sum只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。
```Go
values := []int{1, 2, 3, 4}

View File

@ -103,11 +103,9 @@ func lookup(key string) int {
<u><i>gopl.io/ch5/trace</i></u>
```Go
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the
extra parentheses
defer trace("bigSlowOperation")() // don't forget the extra parentheses
// ...lots of work…
time.Sleep(10 * time.Second) // simulate slow
operation by sleeping
time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
@ -169,8 +167,7 @@ for _, filename := range filenames {
if err != nil {
return err
}
defer f.Close() // NOTE: risky; could run out of file
descriptors
defer f.Close() // NOTE: risky; could run out of file descriptors
// ...process f…
}
```

View File

@ -76,7 +76,7 @@ defer 2
defer 3
```
当f(0)被调用时发生panic异常之前被延迟执行的3个fmt.Printf被调用。程序中断执行后panic信息和堆栈信息会被输出下面是简化的输出
当f(0)被调用时发生panic异常之前被延迟执行的3个fmt.Printf被调用。程序中断执行后panic信息和堆栈信息会被输出下面是简化的输出
```
panic: runtime error: integer divide by zero

View File

@ -85,4 +85,4 @@ fmt.Println(geometry.PathDistance(perim)) // "12", standalone function
fmt.Println(perim.Distance()) // "12", method of geometry.Path
```
**译注:** 如果我们要用方法去计算perim的distance还需要去写全geometry的包名和其函数名但是因为Path这个变量定义了一个可以直接用的Distance方法所以我们可以直接写perim.Distance()。相当于可以少打很多字作者应该是这个意思。因为在Go里包外调用函数需要带上包名还是挺麻烦的。
**译注:** 如果我们要用方法去计算perim的distance还需要去写全geometry的包名和其函数名但是因为Path这个类型定义了一个可以直接用的Distance方法所以我们可以直接写perim.Distance()。相当于可以少打很多字作者应该是这个意思。因为在Go里包外调用函数需要带上包名还是挺麻烦的。

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
@ -61,6 +61,6 @@ fmt.Println(m.Get("item")) // ""
m.Add("item", "3") // panic: assignment to entry in nil map
```
对Get的最后一次调用中nil接收器的行为即是一个空map的行为。我们可以等价地将这个操作写成Value(nil).Get("item")但是如果你直接写nil.Get("item")的话是无法通过编译的因为nil的字面量编译器无法判断其准类型。所以相比之下最后的那行m.Add的调用就会产生一个panic因为他尝试更新一个空map。
对Get的最后一次调用中nil接收器的行为即是一个空map的行为。我们可以等价地将这个操作写成Value(nil).Get("item")但是如果你直接写nil.Get("item")的话是无法通过编译的因为nil的字面量编译器无法判断其准类型。所以相比之下最后的那行m.Add的调用就会产生一个panic因为他尝试更新一个空map。
由于url.Values是一个map类型并且间接引用了其key/value对因此url.Values.Add对这个map里的元素做任何的更新、删除操作对调用方都是可见的。实际上就像在普通函数中一样虽然可以通过引用来操作内部值但在方法想要修改引用本身是不会影响原始值的比如把他置为nil或者让这个引用指向了其它的对象调用方都不会受影响。译注因为传入的是存储了内存地址的变量你改变这个变量是影响不了原始的变量的想想C语言是差不多的
由于url.Values是一个map类型并且间接引用了其key/value对因此url.Values.Add对这个map里的元素做任何的更新、删除操作对调用方都是可见的。实际上就像在普通函数中一样虽然可以通过引用来操作内部值但在方法想要修改引用本身是不会影响原始值的,比如把他置为nil或者让这个引用指向了其它的对象调用方都不会受影响。译注因为传入的是存储了内存地址的变量你改变这个变量本身是影响不了原始的变量的想想C语言是差不多的

View File

@ -13,7 +13,7 @@ func (p *Point) ScaleBy(factor float64) {
在现实的程序里一般会约定如果Point这个类有一个指针作为接收器的方法那么所有Point的方法都必须有一个指针接收器即使是那些并不需要这个指针接收器的函数。我们在这里打破了这个约定只是为了展示一下两种方法的异同而已。
只有类型(Point)和指向他们的指针`(*Point)`,才是可能会出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:
只有类型(Point)和指向他们的指针`(*Point)`,才可能是出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:
```go
type P *int
@ -65,7 +65,7 @@ pptr.Distance(q)
这里的几个例子可能让你有些困惑,所以我们总结一下:在每一个合法的方法调用表达式中,也就是下面三种情况里的任意一种情况都是可以的:
不论接收器的实际参数和其接收器的形式参数相同比如两者都是类型T或者都是类型`*T`
不论接收器的实际参数和其形式参数相同比如两者都是类型T或者都是类型`*T`
```go
Point{1, 2}.Distance(q) // Point
@ -84,11 +84,11 @@ p.ScaleBy(2) // implicit (&p)
pptr.Distance(q) // implicit (*pptr)
```
如果命名类型T(译注用type xxx定义的类型)的所有方法都是用T类型自己来做接收器(而不是`*T`)那么拷贝这种类型的实例就是安全的调用他的任何一个方法也就会产生一个值的拷贝。比如time.Duration的这个类型在调用其方法时就会被全部拷贝一份包括在作为参数传入函数的时候。但是如果一个方法使用指针作为接收器你需要避免对其进行拷贝因为这样可能会破坏掉该类型内部的不变性。比如你对bytes.Buffer对象进行了拷贝那么可能会引起原始对象和拷贝对象只是别名而已但实际上其指向的对象是一致的。紧接着对拷贝后的变量进行修改可能会有让你意外的结果。
如果命名类型T译注用type xxx定义的类型的所有方法都是用T类型自己来做接收器而不是`*T`那么拷贝这种类型的实例就是安全的调用他的任何一个方法也就会产生一个值的拷贝。比如time.Duration的这个类型在调用其方法时就会被全部拷贝一份包括在作为参数传入函数的时候。但是如果一个方法使用指针作为接收器你需要避免对其进行拷贝因为这样可能会破坏掉该类型内部的不变性。比如你对bytes.Buffer对象进行了拷贝那么可能会引起原始对象和拷贝对象只是别名而已实际上它们指向的对象是一样的。紧接着对拷贝后的变量进行修改可能会有让你意外的结果。
**译注:** 作者这里说的比较绕,其实有两点:
1. 不管你的method的receiver是指针类型还是非指针类型都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
2. 在声明一个method的receiver该是指针还是非指针类型时你需要考虑两方面的内部第一方面是这个对象本身是不是特别大如果声明为非指针变量时调用会产生一次拷贝第二方面是如果你用指针类型作为receiver那么你一定要注意这种指针类型指向的始终是一块内存地址就算你对其进行了拷贝。熟悉C或者C的人这里应该很快能明白。
2. 在声明一个method的receiver该是指针还是非指针类型时你需要考虑两方面的因素第一方面是这个对象本身是不是特别大如果声明为非指针变量时调用会产生一次拷贝第二方面是如果你用指针类型作为receiver那么你一定要注意这种指针类型指向的始终是一块内存地址就算你对其进行了拷贝。熟悉C或者C++的人这里应该很快能明白。
{% include "./ch6-02-1.md" %}

View File

@ -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节中做的那样:
@ -94,7 +94,7 @@ fmt.Println(x.String()) // "{1 9 42 144}"
fmt.Println(x) // "{[4398046511618 0 65536]}"
```
在第一个Println中我们打印一个`*IntSet`的指针这个类型的指针确实有自定义的String方法。第二Println我们直接调用了x变量的String()方法这种情况下编译器会隐式地在x前插入&操作符,这样相当我们还是调用的IntSet指针的String方法。在第三个Println中因为IntSet类型没有String方法所以Println方法会直接以原始的方式理解并打印。所以在这种情况下&符号是不能忘的。在我们这种场景下你把String方法绑定到IntSet对象上而不是IntSet指针上可能会更合适一些不过这也需要具体问题具体分析。
在第一个Println中我们打印一个`*IntSet`的指针这个类型的指针确实有自定义的String方法。第二Println我们直接调用了x变量的String()方法这种情况下编译器会隐式地在x前插入&操作符,这样相当我们还是调用的IntSet指针的String方法。在第三个Println中因为IntSet类型没有String方法所以Println方法会直接以原始的方式理解并打印。所以在这种情况下&符号是不能忘的。在我们这种场景下你把String方法绑定到IntSet对象上而不是IntSet指针上可能会更合适一些不过这也需要具体问题具体分析。
**练习6.1:** 为bit数组实现下面这些方法
@ -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

@ -12,13 +12,13 @@ type IntSet struct {
}
```
当然我们也可以把IntSet定义为一个slice类型尽管这样我们就需要把代码中所有方法里用到的s.words用`*s`替换掉了:
当然我们也可以把IntSet定义为一个slice类型这样我们就需要把代码中所有方法里用到的s.words用`*s`替换掉了:
```go
type IntSet []uint64
```
尽管这个版本的IntSet在本质上是一样的他也可以允许其它包中可以直接读取并编辑这个slice。换句话说相对`*s`这个表达式会出现在所有的包中s.words只需要在定义IntSet的包中出现(译注:所以还是推荐后者吧的意思)。
尽管这个版本的IntSet在本质上是一样的但它也允许其它包中可以直接读取并编辑这个slice。换句话说相对`*s`这个表达式会出现在所有的包中s.words只需要在定义IntSet的包中出现(译注:所以还是推荐后者吧的意思)。
这种基于名字的手段使得在语言中最小的封装单元是package而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性无论你的代码是写在一个函数还是一个方法里。

View File

@ -2,9 +2,9 @@
目前为止,我们看到的类型都是具体的类型。一个具体的类型可以准确的描述它所代表的值,并且展示出对类型本身的一些操作方式:就像数字类型的算术操作,切片类型的取下标、添加元素和范围获取操作。具体的类型还可以通过它的内置方法提供额外的行为操作。总的来说,当你拿到一个具体的类型时你就知道它的本身是什么和你可以用它来做什么。
在Go语言中还存在着另外一种类型接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合它们只会展示出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
在Go语言中还存在着另外一种类型接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
在本书中我们一直使用两个相似的函数来进行字符串的格式化fmt.Printf它会把结果写到标准输出和fmt.Sprintf它会把结果以字符串的形式返回。得益于使用接口我们不必可悲的因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上这两个函数都使用了另一个函数fmt.Fprintf来进行封装。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。
在本书中我们一直使用两个相似的函数来进行字符串的格式化fmt.Printf它会把结果写到标准输出和fmt.Sprintf它会把结果以字符串的形式返回。得益于使用接口我们不必可悲的因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上这两个函数都使用了另一个函数fmt.Fprintf来进行封装。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。
``` go
package fmt
@ -23,7 +23,7 @@ func Sprintf(format string, args ...interface{}) string {
Fprintf的前缀F表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是`*os.File`类型在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区然而它
并不是一个文件类型尽管它在某种意义上和文件类型相似。
即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型这是一个接口类型定义如下
即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型这是一个接口类型定义如下:
``` go
package io
@ -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
@ -84,12 +84,12 @@ type Stringer interface {
我们会在7.10节解释fmt包怎么发现哪些值是满足这个接口类型的。
**练习 7.1** 使用来自ByteCounter的思路实现一个针对单词和行数的计数器。你会发现bufio.ScanWords非常的有用。
**练习 7.1** 使用来自ByteCounter的思路实现一个针对单词和行数的计数器。你会发现bufio.ScanWords非常的有用。
**练习 7.2** 写一个带有如下函数签名的函数CountingWriter传入一个io.Writer接口类型返回一个新的Writer类型把原来的Writer封装在里面和一个表示写入新的Writer字节数的int64类型指针
**练习 7.2** 写一个带有如下函数签名的函数CountingWriter传入一个io.Writer接口类型返回一个把原来的Writer封装在里面的新的Writer类型和一个表示新的写入字节数的int64类型指针。
```go
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

@ -2,7 +2,7 @@
接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。
io.Writer类型是用最广泛的接口之一,因为它提供了所有类型写入bytes的抽象包括文件类型内存缓冲区网络链接HTTP客户端压缩工具哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型Closer可以是任意可以关闭的值例如一个文件或是网络链接。到现在你可能注意到了很多Go语言中单方法接口的命名习惯
io.Writer类型是用最广泛的接口之一,因为它提供了所有类型写入bytes的抽象包括文件类型内存缓冲区网络链接HTTP客户端压缩工具哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型Closer可以是任意可以关闭的值例如一个文件或是网络链接。到现在你可能注意到了很多Go语言中单方法接口的命名习惯
```go
package io
@ -14,7 +14,7 @@ type Closer interface {
}
```
往下看,我们发现有些新的接口类型通过组合已有的接口来定义。下面是两个例子:
往下看,我们发现有些新的接口类型通过组合已有的接口来定义。下面是两个例子:
```go
type ReadWriter interface {
@ -27,7 +27,7 @@ type ReadWriteCloser interface {
Closer
}
```
上面用到的语法和结构内嵌相似,我们可以用这种方式以一个简写命名一个接口,而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁我们可以像下面这样不使用内嵌来声明io.Writer接口。
上面用到的语法和结构内嵌相似我们可以用这种方式以一个简写命名一个接口而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁我们可以像下面这样不使用内嵌来声明io.ReadWriter接口。
```go
type ReadWriter interface {
@ -36,7 +36,7 @@ type ReadWriter interface {
}
```
或者甚至使用种混合的风格:
或者甚至使用种混合的风格:
```go
type ReadWriter interface {
@ -45,9 +45,9 @@ type ReadWriter interface {
}
```
上面3种定义方式都是一样的效果。方法顺序变化也没有影响,唯一重要的就是这个集合里面的方法。
上面3种定义方式都是一样的效果。方法顺序变化也没有影响,唯一重要的就是这个集合里面的方法。
**练习 7.4** strings.NewReader函数通过读取一个string参数返回一个满足io.Reader接口类型的值和其它值。实现一个简单版本的NewReader用它来构造一个接收字符串输入的HTML解析器§5.2
**练习 7.4** strings.NewReader函数通过读取一个string参数返回一个满足io.Reader接口类型的值和其它值。实现一个简单版本的NewReader用它来构造一个接收字符串输入的HTML解析器§5.2
**练习 7.5** io包里面的LimitReader函数接收一个io.Reader接口类型的r和字节数n并且返回另一个从r中读取字节但是当读完n个字节后就表示读到文件结束的Reader。实现这个LimitReader函数

View File

@ -24,7 +24,7 @@ rwc = w // compile error: io.Writer lacks Close method
因为ReadWriter和ReadWriteCloser包含所有Writer的方法所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口
在进一步学习前,必须先解释表示一个类型持有一个方法当中的细节。回想在6.2章中对于每一个命名过的具体类型T它一些方法的接收者是类型T本身然而另一些则是一个`*T`的指针。还记得在T类型的参数上调用一个`*T`的方法是合法的只要这个参数是一个变量编译器隐式的获取了它的地址。但这仅仅是一个语法糖T类型的值不拥有所有`*T`指针的方法,这样它就可能只实现更少的接口。
在进一步学习前,必须先解释一个类型持有一个方法的表示当中的细节。回想在6.2章中对于每一个命名过的具体类型T一些方法的接收者是类型T本身然而另一些则是一个`*T`的指针。还记得在T类型的参数上调用一个`*T`的方法是合法的只要这个参数是一个变量编译器隐式的获取了它的地址。但这仅仅是一个语法糖T类型的值不拥有所有`*T`指针的方法,这样它就可能只实现更少的接口。
举个例子可能会更清晰一点。在第6.5章中IntSet类型的String方法的接收者是一个指针类型所以我们不能在一个不能寻址的IntSet值上调用这个方法
@ -50,7 +50,7 @@ var _ fmt.Stringer = s // compile error: IntSet lacks String method
12.8章包含了一个打印出任意值的所有方法的程序然后可以使用godoc -analysis=type tool(§10.7.4)展示每个类型的方法和具体类型和接口之间的关系
就像信封封装和隐藏信件来一样,接口类型封装和隐藏具体类型和它的值。即使具体类型有其它的方法也只有接口类型暴露出来的方法会被调用到:
就像信封封装和隐藏信件来一样,接口类型封装和隐藏具体类型和它的值。即使具体类型有其它的方法也只有接口类型暴露出来的方法会被调用到:
```go
os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
@ -62,7 +62,7 @@ w.Write([]byte("hello")) // OK: io.Writer has Write method
w.Close() // compile error: io.Writer lacks Close method
```
一个有更多方法的接口类型比如io.ReadWriter和少一些方法的接口类型,例如io.Reader进行对比更多方法的接口类型会告诉我们更多关于它的值持有的信息并且对实现它的类型要求更加严格。那么关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?
一个有更多方法的接口类型比如io.ReadWriter和少一些方法的接口类型例如io.Reader进行对比更多方法的接口类型会告诉我们更多关于它的值持有的信息并且对实现它的类型要求更加严格。那么关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?
这看上去好像没有用但实际上interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。
@ -75,11 +75,11 @@ any = map[string]int{"one": 1}
any = new(bytes.Buffer)
```
尽管不是很明显,从本书最早的例子中我们就已经在使用空接口类型。它允许像fmt.Println或者5.7章中的errorf函数接受任何类型的参数。
尽管不是很明显从本书最早的例子中我们就已经在使用空接口类型。它允许像fmt.Println或者5.7章中的errorf函数接受任何类型的参数。
对于创建的一个interface{}值持有一个booleanfloatstringmappointer或者任意其它的类型我们当然不能直接对它持有的值做操作因为interface{}没有任何方法。我们会在7.10章中学到一种用类型断言来获取interface{}中值的方法。
因为接口实现只依赖于判断两个类型的方法,所以没有必要定义一个具体类型和它实现的接口之间的关系。也就是说,尝试文档化和断言这种关系几乎没有用,所以并没有通过程序强制定义。下面的定义在编译期断言一个`*bytes.Buffer`的值实现了io.Writer接口类型:
因为接口实现只依赖于判断两个类型的方法,所以没有必要定义一个具体类型和它实现的接口之间的关系。也就是说,尝试文档化和断言这种关系几乎没有用,所以并没有通过程序强制定义。下面的定义在编译期断言一个`*bytes.Buffer`的值实现了io.Writer接口类型:
```go
// *bytes.Buffer must satisfy io.Writer
@ -139,7 +139,7 @@ type Video interface {
}
```
这些接口不止是一种有用的方式来分组相关的具体类型和表示他们之间的共同特。我们后面可能会发现其它的分组。举例如果我们发现我们需要以同样的方式处理Audio和Video我们可以定义一个Streamer接口来代表它们之间相同的部分而不必对已经存在的类型做改变。
这些接口不止是一种有用的方式来分组相关的具体类型和表示他们之间的共同特。我们后面可能会发现其它的分组。举例如果我们发现我们需要以同样的方式处理Audio和Video我们可以定义一个Streamer接口来代表它们之间相同的部分而不必对已经存在的类型做改变。
```go
type Streamer interface {

View File

@ -39,9 +39,9 @@ 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因此可以避免一开始就将一个不完的值赋值给这个接口:
问题在于尽管一个nil的\*bytes.Buffer指针有实现这个接口的方法它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(\*bytes.Buffer).Write方法的接收者非空的隐含先觉条件所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer因此可以避免一开始就将一个不完的值赋值给这个接口:
```go
var buf io.Writer

View File

@ -20,7 +20,7 @@ var w io.Writer
![](../images/ch7-01.png)
一个接口值基于它的动态类型被描述为空或非空所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判接口值是否为空。调用一个空接口值上的任意方法都会产生panic:
一个接口值基于它的动态类型被描述为空或非空所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判接口值是否为空。调用一个空接口值上的任意方法都会产生panic:
```go
w.Write([]byte("hello")) // panic: nil pointer dereference
@ -32,7 +32,7 @@ w.Write([]byte("hello")) // panic: nil pointer dereference
w = os.Stdout
```
这个赋值过程调用了一个具体类型到接口类型的隐式转换这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为`*os.Stdout`指针的类型描述符它的动态值持有os.Stdout的拷贝这是一个代表处理标准输出的os.File类型变量的指针图7.2)。
这个赋值过程调用了一个具体类型到接口类型的隐式转换这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为`*os.File`指针的类型描述符它的动态值持有os.Stdout的拷贝这是一个代表处理标准输出的os.File类型变量的指针图7.2)。
![](../images/ch7-02.png)
@ -72,7 +72,7 @@ w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers
w = nil
```
这个重置将它所有的部分都设为nil值把变量w恢复到和它之前定义时相同的状态在图7.1中可以看到。
这个重置将它所有的部分都设为nil值把变量w恢复到和它之前定义时相同的状态在图7.1中可以看到。
一个接口值可以持有任意大的动态值。例如表示时间实例的time.Time类型这个类型有几个对外不公开的字段。我们从它上面创建一个接口值,
@ -84,7 +84,7 @@ var x interface{} = time.Now()
![](../images/ch7-04.png)
接口值可以使用==和!来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的操作相等。因为接口值是可比较的所以它们可以用在map的键或者作为switch语句的操作数。
接口值可以使用==和!来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的所以它们可以用在map的键或者作为switch语句的操作数。
然而如果两个接口值的动态类型相同但是这个动态类型是不可比较的比如切片将它们进行比较就会失败并且panic:

View File

@ -34,11 +34,11 @@ sort.Sort(StringSlice(names))
对字符串切片的排序是很常用的需要所以sort包提供了StringSlice类型也提供了Strings函数能让上面这些调用简化成sort.Strings(names)。
这里用到的技术很容易适用到其它排序序列中,例如我们可以忽略大些或者含有特殊的字符。本书使用Go程序对索引词和页码进行排序也用到了这个技术对罗马数字做了额外逻辑处理。对于更复杂的排序我们使用相同的方法但是会用更复杂的数据结构和更复杂地实现sort.Interface的方法。
这里用到的技术很容易适用到其它排序序列中,例如我们可以忽略大小写或者含有的特殊字符。本书使用Go程序对索引词和页码进行排序也用到了这个技术对罗马数字做了额外逻辑处理。对于更复杂的排序我们使用相同的方法但是会用更复杂的数据结构和更复杂地实现sort.Interface的方法。
我们会运行上面的例子来对一个表格中的音乐播放列表进行排序。每个track都是单独的一行每一列都是这个track的属性像艺术家标题和运行时间。想象一个图形用户界面来呈现这个表格并且点击一个属性的顶部会使这个列表按照这个属性进行排序再一次点击相同属性的顶部会进行逆向排序。让我们看下每个点击会发生什么响应。
下面的变量tracks包了一个播放列表。One of the authors apologizes for the other authors musical tastes.每个元素都不是Track本身而是指向它的指针。尽管我们在下面的代码中直接存储Tracks也可以工作sort函数会交换很多对元素所以如果每个元素都是指针会更快而不是全部Track类型指针是一个机器字码长度而Track类型可能是八个或更多。
下面的变量tracks包了一个播放列表。One of the authors apologizes for the other authors musical tastes.每个元素都不是Track本身而是指向它的指针。尽管我们在下面的代码中直接存储Tracks也可以工作sort函数会交换很多对元素所以如果每个元素都是指针而不是Track类型会更快指针是一个机器字码长度而Track类型可能是八个或更多。
<u><i>gopl.io/ch7/sorting</i></u>
```go
@ -66,7 +66,7 @@ func length(s string) time.Duration {
}
```
printTracks函数将播放列表打印成一个表格。一个图形化的展示可能会更好点但是这个小程序使用text/tabwriter包来生成一个列整齐对齐和隔开的表格,像下面展示的这样。注意到`*tabwriter.Writer`是满足io.Writer接口的。它会收集每一片写向它的数据它的Flush方法会格式化整个表格并且将它写向os.Stdout标准输出
printTracks函数将播放列表打印成一个表格。一个图形化的展示可能会更好点但是这个小程序使用text/tabwriter包来生成一个列整齐对齐和隔开的表格像下面展示的这样。注意到`*tabwriter.Writer`是满足io.Writer接口的。它会收集每一片写向它的数据它的Flush方法会格式化整个表格并且将它写向os.Stdout标准输出
```go
func printTracks(tracks []*Track) {
@ -81,7 +81,7 @@ func printTracks(tracks []*Track) {
}
```
为了能按照Artist字段对播放列表进行排序我们会像对StringSlice那样定义一个新的带有必须LenLess和Swap方法的切片类型。
为了能按照Artist字段对播放列表进行排序我们会像对StringSlice那样定义一个新的带有必须LenLess和Swap方法的切片类型。
```go
type byArtist []*Track
@ -124,7 +124,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
@ -136,7 +136,7 @@ func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }
```
reverse的另外两个方法Len和Swap隐式地由原有内嵌的sort.Interface提供。因为reverse是一个不公开的类型所以导出函数Reverse函数返回一个包含原有sort.Interface值的reverse类型实例。
reverse的另外两个方法Len和Swap隐式地由原有内嵌的sort.Interface提供。因为reverse是一个不公开的类型所以导出函数Reverse返回一个包含原有sort.Interface值的reverse类型实例。
为了可以按照不同的列进行排序我们必须定义一个新的类型例如byYear
@ -166,9 +166,9 @@ type customSort struct {
less func(x, y *Track) bool
}
func (x customSort) Len() int
func (x customSort) Len() int { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }
```
让我们定义一个多层的排序函数它主要的排序键是标题第二个键是年第三个键是运行时间Length。下面是该排序的调用其中这个排序使用了匿名排序函数
@ -199,7 +199,7 @@ Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
```
尽管对长度为n的序列排序需要 O(n log n)次比较操作检查一个序列是否已经有序至少需要n1次比较。sort包中的IsSorted函数帮我们做这样的检查。像sort.Sort一样它也使用sort.Interface对这个序列和它的排序函数进行抽象但是它从不会调用Swap方法这段代码示范了IntsAreSorted和Ints函数和IntSlice类型的使用:
尽管对长度为n的序列排序需要 O(n log n)次比较操作检查一个序列是否已经有序至少需要n-1次比较。sort包中的IsSorted函数帮我们做这样的检查。像sort.Sort一样它也使用sort.Interface对这个序列和它的排序函数进行抽象但是它从不会调用Swap方法这段代码示范了IntsAreSorted和Ints函数在IntSlice类型上的使用:
```go
values := []int{3, 1, 4, 1}

View File

@ -15,7 +15,7 @@ func ListenAndServe(address string, h Handler) error
ListenAndServe函数需要一个例如“localhost:8000”的服务器地址和一个所有请求都可以分派的Handler接口实例。它会一直运行直到这个服务因为一个错误而失败或者启动失败它的返回值一定是一个非空的错误。
想象一个电子商务网站,为了销售它的数据库将它物品的价格映射成美元。下面这个程序可能是能想到的最简单的实现了。它将库存清单模型化为一个命名为database的map类型我们给这个类型一个ServeHttp方法这样它可以满足http.Handler接口。这个handler会遍历整个map并输出物品信息。
想象一个电子商务网站,为了销售,将数据库中物品的价格映射成美元。下面这个程序可能是能想到的最简单的实现了。它将库存清单模型化为一个命名为database的map类型我们给这个类型一个ServeHttp方法这样它可以满足http.Handler接口。这个handler会遍历整个map并输出物品信息。
<u><i>gopl.io/ch7/http1</i></u>
```go
@ -53,7 +53,7 @@ shoes: $50.00
socks: $5.00
```
目前为止这个服务器不考虑URL只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的URL每一个都会触发一个不同的行为。让我们使用/list来调用已经存在的这个行为并且增加另一个/price调用表明单个货品的价格像这样/price?item=socks来指定一个请求参数。
目前为止这个服务器不考虑URL只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的URL每一个都会触发一个不同的行为。让我们使用/list来调用已经存在的这个行为并且增加另一个/price调用表明单个货品的价格像这样/price?item=socks来指定一个请求参数。
<u><i>gopl.io/ch7/http2</i></u>
```go
@ -112,7 +112,7 @@ no such page: /help
对于更复杂的应用一些ServeMux可以通过组合来处理更加错综复杂的路由需求。Go语言目前没有一个权威的web框架就像Ruby语言有Rails和python有Django。这并不是说这样的框架不存在而是Go语言标准库中的构建模块就已经非常灵活以至于这些框架都是不必要的。此外尽管在一个项目早期使用框架是非常方便的但是它们带来额外的复杂度会使长期的维护更加困难。
在下面的程序中我们创建一个ServeMux并且使用它将URL和相应处理/list和/price操作的handler联系起来这些操作逻辑都已经被分到不同的方法中。然后我门在调用ListenAndServe函数中使用ServeMux为主要的handler。
在下面的程序中我们创建一个ServeMux并且使用它将URL和相应处理/list和/price操作的handler联系起来这些操作逻辑都已经被分到不同的方法中。然后我门在调用ListenAndServe函数中使用ServeMux为主要的handler。
<u><i>gopl.io/ch7/http3</i></u>
```go
@ -144,13 +144,13 @@ 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)
```
也就是说db.list的调用会援引一个接收者是db的database.list方法。所以db.list是一个实现了handler类似行为的函数但是因为它没有方法所以它不满足http.Handler接口并且不能直接传给mux.Handle。
也就是说db.list的调用会援引一个接收者是db的database.list方法。所以db.list是一个实现了handler类似行为的函数但是因为它没有方法(理解:该方法没有它自己的方法)所以它不满足http.Handler接口并且不能直接传给mux.Handle。
语句http.HandlerFunc(db.list)是一个转换而非一个函数调用因为http.HandlerFunc是一个类型。它有如下的定义
@ -165,7 +165,7 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
}
```
HandlerFunc显示了在Go语言接口机制中一些不同寻常的特点。这是一个实现了接口http.Handler方法的函数类型。ServeHTTP方法的行为调用了它本身的函数。因此HandlerFunc是一个让函数值满足一个接口的适配器这里函数和这个接口仅有的方法有相同的函数签名。实际上这个技巧让一个单一的类型例如database以多种方式满足http.Handler接口一种通过它的list方法一种通过它的price方法等等。
HandlerFunc显示了在Go语言接口机制中一些不同寻常的特点。这是一个实现了接口http.Handler方法的函数类型。ServeHTTP方法的行为调用了它的函数本身。因此HandlerFunc是一个让函数值满足一个接口的适配器这里函数和这个接口仅有的方法有相同的函数签名。实际上这个技巧让一个单一的类型例如database以多种方式满足http.Handler接口一种通过它的list方法一种通过它的price方法等等。
因为handler通过这种方式注册非常普遍ServeMux有一个方便的HandleFunc方法它帮我们简化handler注册代码成这样
@ -175,7 +175,7 @@ mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)
```
从上面的代码很容易看出应该怎么构建一个程序它有两个不同的web服务器监听不同的端口的并且定义不同的URL将它们指派到不同的handler。我们只要构建另外一个ServeMux并且调用一次ListenAndServe可能并行的。但是在大多数程序中一个web服务器就足够了。此外在一个应用程序的多个文件中定义HTTP handler也是非常典型的如果它们必须全部都显示的注册到这个应用的ServeMux实例上会比较麻烦。
从上面的代码很容易看出应该怎么构建一个程序由两个不同的web服务器监听不同的端口并且定义不同的URL将它们指派到不同的handler。我们只要构建另外一个ServeMux并且调用一次ListenAndServe可能并行的。但是在大多数程序中一个web服务器就足够了。此外在一个应用程序的多个文件中定义HTTP handler也是非常典型的如果它们必须全部都显式地注册到这个应用的ServeMux实例上会比较麻烦。
所以为了方便net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数。现在为了使用DefaultServeMux作为服务器的主handler我们不需要将它传给ListenAndServe函数nil值就可以工作。
@ -191,8 +191,8 @@ func main() {
}
```
最后一个重要的提示就像我们在1.7节中提到的web服务器在一个新的协程中调用每一个handler所以当handler获取其它协程或者这个handler本身的其它请求也可以访问的变量时一定要使用预防措施比如锁机制。我们后面的两章中讲到并发相关的知识。
最后一个重要的提示就像我们在1.7节中提到的web服务器在一个新的协程中调用每一个handler所以当handler获取其它协程或者这个handler本身的其它请求也可以访问到变量时,一定要使用预防措施,比如锁机制。我们后面的两章中将讲到并发相关的知识。
**练习 7.11** 增加额外的handler让客端可以创建,读取,更新和删除数据库记录。例如,一个形如 `/update?item=socks&price=6` 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)
**练习 7.11** 增加额外的handler让客端可以创建,读取,更新和删除数据库记录。例如,一个形如 `/update?item=socks&price=6` 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)
**练习 7.12** 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包(§4.6)可能会对你有帮助。

View File

@ -50,7 +50,7 @@ type call struct {
type Env map[Var]float64
```
我们也需要每个表式去定义一个Eval方法这个方法会根据给定的environment变量返回表达式的值。因为每个表达式都必须提供这个方法我们将它加入到Expr接口中。这个包只会对外公开ExprEnv和Var类型。调用方不需要获取其它的表达式类型就可以使用这个求值器。
我们也需要每个表式去定义一个Eval方法这个方法会根据给定的environment变量返回表达式的值。因为每个表达式都必须提供这个方法我们将它加入到Expr接口中。这个包只会对外公开ExprEnv和Var类型。调用方不需要获取其它的表达式类型就可以使用这个求值器。
```go
type Expr interface {
@ -71,7 +71,7 @@ func (l literal) Eval(_ Env) float64 {
}
```
unary和binary的Eval方法会递归的计算它的运算对象然后将运算符op作用到它们上。我们不将被零或无穷数除作为一个错误因为它们都会产生一个固定的结果无限。最后call的这个方法会计算对于powsin或者sqrt函数的参数值然后调用对应在math包中的函数。
unary和binary的Eval方法会递归的计算它的运算对象然后将运算符op作用到它们上。我们不将被零或无穷数除作为一个错误因为它们都会产生一个固定的结果——无限。最后call的这个方法会计算对于powsin或者sqrt函数的参数值然后调用对应在math包中的函数。
```go
func (u unary) Eval(env Env) float64 {
@ -111,7 +111,7 @@ func (c call) Eval(env Env) float64 {
}
```
一些方法会失败。例如一个call表达式可能未知的函数或者错误的参数个数。用一个无效的运算符如!或者<去构建一个unary或者binary表达式也是可能会发生的尽管下面提到的Parse函数不会这样做。这些错误会让Eval方法panic。其它的错误像计算一个没有在environment变量中出现过的Var只会让Eval方法返回一个错误的结果。所有的这些错误都可以通过在计算前检查Expr来发现。这是我们接下来要讲的Check方法的工作但是让我们先测试Eval方法。
一些方法会失败。例如一个call表达式可能未知的函数或者错误的参数个数。用一个无效的运算符如!或者<去构建一个unary或者binary表达式也是可能会发生的尽管下面提到的Parse函数不会这样做。这些错误会让Eval方法panic。其它的错误像计算一个没有在environment变量中出现过的Var只会让Eval方法返回一个错误的结果。所有的这些错误都可以通过在计算前检查Expr来发现。这是我们接下来要讲的Check方法的工作但是让我们先测试Eval方法。
下面的TestEval函数是对evaluator的一个测试。它使用了我们会在第11章讲解的testing包但是现在知道调用t.Errof会报告一个错误就足够了。这个函数循环遍历一个表格中的输入这个表格中定义了三个表达式和针对每个表达式不同的环境变量。第一个表达式根据给定圆的面积A计算它的半径第二个表达式通过两个变量x和y计算两个立方体的体积之和第三个表达式将华氏温度F转换成摄氏度。
@ -159,7 +159,7 @@ go test(§11.1) 命令会运行一个包的测试用例:
$ go test -v gopl.io/ch7/eval
```
这个-v标识可以让我们看到测试用例打印的输出正常情况下像这个一样成功的测试用例会阻止打印结果的输出。这里是测试用例里fmt.Printf语句的输出
这个-v标识可以让我们看到测试用例打印的输出正常情况下像这样一个成功的测试用例会阻止打印结果的输出。这里是测试用例里fmt.Printf语句的输出
```
sqrt(A / pi)
@ -177,7 +177,7 @@ pow(x, 3) + pow(y, 3)
幸运的是目前为止所有的输入都是适合的格式,但是我们的运气不可能一直都有。甚至在解释型语言中,为了静态错误检查语法是非常常见的;静态错误就是不用运行程序就可以检测出来的错误。通过将静态检查和动态的部分分开,我们可以快速的检查错误并且对于多次检查只执行一次而不是每次表达式计算的时候都进行检查。
让我们往Expr接口中增加另一个方法。Check方法一个表达式语义树检查出静态错误。我们马上会说明它的vars参数。
让我们往Expr接口中增加另一个方法。Check方法一个表达式语义树检查出静态错误。我们马上会说明它的vars参数。
```go
type Expr interface {
@ -248,9 +248,9 @@ log(10) unknown function "log"
sqrt(1, 2) call to sqrt has 2 args, want 1
```
Check方法的参数是一个Var类型的集合这个集合聚集从表达式中找到的变量名。为了保证成功的计算这些变量中的每一个都必须出现在环境变量中。从逻辑上讲这个集合就是调用Check方法返回的结果但是因为这个方法是递归调用的所以对于Check方法填充结果到一个作为参数传入的集合中会更加的方便。调用方在初始调用时必须提供一个空的集合。
Check方法的参数是一个Var类型的集合这个集合聚集从表达式中找到的变量名。为了保证成功的计算这些变量中的每一个都必须出现在环境变量中。从逻辑上讲这个集合就是调用Check方法返回的结果但是因为这个方法是递归调用的所以对于Check方法填充结果到一个作为参数传入的集合中会更加的方便。调用方在初始调用时必须提供一个空的集合。
在第3.2节中,我们绘制了一个在编译才确定的函数f(x,y)。现在我们可以解析检查和计算在字符串中的表达式我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。我们可以使用集合vars来检查表达式是否是一个只有两个变量,x和y的函数——实际上是3个因为我们为了方便会提供半径大小r。并且我们会在计算前使用Check方法拒绝有格式问题的表达式这样我们就不会在下面函数的40000个计算过程100x100个栅格每一个有4个角重复这些检查。
在第3.2节中,我们绘制了一个在编译才确定的函数f(x,y)。现在我们可以解析检查和计算在字符串中的表达式我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。我们可以使用集合vars来检查表达式是否是一个只有两个变量x和y的函数——实际上是3个因为我们为了方便会提供半径大小r。并且我们会在计算前使用Check方法拒绝有格式问题的表达式这样我们就不会在下面函数的40000个计算过程100x100个栅格每一个有4个角重复这些检查。
这个ParseAndCheck函数混合了解析和检查步骤的过程

View File

@ -11,9 +11,9 @@ f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
```
第二种如果相反断言的类型T是一个接口类型然后类型断言检查是否x的动态类型满足T。如果这个检查成功了动态值没有获取到这个结果仍然是一个有相同类型和值部分的接口值,但是结果有类型T。换句话说对一个接口类型的类型断言改变了类型的表述方式改变了可以获取的方法集合通常更大但是它保了接口值内部的动态类型和值的部分。
第二种,如果相反断言的类型T是一个接口类型然后类型断言检查是否x的动态类型满足T。如果这个检查成功了动态值没有获取到这个结果仍然是一个有相同动态类型和值部分的接口值,但是结果为类型T。换句话说对一个接口类型的类型断言改变了类型的表述方式改变了可以获取的方法集合通常更大但是它保了接口值内部的动态类型和值的部分。
在下面的第一个类型断言后w和rw都持有os.Stdout因此它们每个有一个动态类型`*os.File`但是变量w是一个io.Writer类型只对外公开出文件的Write方法然而rw变量也只公开它的Read方法。
在下面的第一个类型断言后w和rw都持有os.Stdout,因此它们都有一个动态类型`*os.File`但是变量w是一个io.Writer类型只对外公开了文件的Write方法而rw变量还公开了它的Read方法。
```go
var w io.Writer
@ -23,14 +23,14 @@ w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
```
如果断言操作的对象是一个nil接口值那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型更少的方法集合做断言因为它表现的就像赋值操作一样除了对于nil接口值的情况。
如果断言操作的对象是一个nil接口值那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型更少的方法集合做断言因为它表现的就像赋值操作一样除了对于nil接口值的情况。
```go
w = rw // io.ReadWriter is assignable to io.Writer
w = rw.(io.Writer) // fails only if rw == nil
```
经常地我们对一个接口值的动态类型是不确定的并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中例如如下的定义这个操作不会在失败的时候发生panic但是代地返回一个额外的第二个结果,这个结果是一个标识成功的布尔值:
经常地,对一个接口值的动态类型我们是不确定的并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中例如如下的定义这个操作不会在失败的时候发生panic但是代地返回一个额外的第二个结果,这个结果是一个标识成功与否的布尔值:
```go
var w io.Writer = os.Stdout
@ -38,7 +38,7 @@ f, ok := w.(*os.File) // success: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
```
第二个结果常规地赋值给一个命名为ok的变量。如果这个操作失败了那么ok就是false值第一个结果等于被断言类型的零值在这个例子中就是一个nil的`*bytes.Buffer`类型。
第二个结果常赋值给一个命名为ok的变量。如果这个操作失败了那么ok就是false值第一个结果等于被断言类型的零值在这个例子中就是一个nil的`*bytes.Buffer`类型。
这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁
@ -48,7 +48,7 @@ if f, ok := w.(*os.File); ok {
}
```
当类型断言的操作对象是一个变量,你有时会看见原来的变量名重用而不是声明一个新的本地变量,这个重用的变量会覆盖原来的值,如下面这样:
当类型断言的操作对象是一个变量,你有时会看见原来的变量名重用而不是声明一个新的本地变量这个重用的变量原来的值会被覆盖理解其实是声明了一个同名的新的本地变量外层原来的w不会被改变,如下面这样:
```go
if w, ok := w.(*os.File); ok {

View File

@ -1,6 +1,6 @@
## 7.11. 基于类型断言区别错误类型
思考在os包中文件操作返回的错误集合。I/O可以因为任何数量的原因失败但是有三种经常的错误必须进行不同的处理文件已经存在对于创建操作找不到文件对于读取操作和权限拒绝。os包中提供了三个帮助函数来对给定的错误值表示的失败进行分类:
思考在os包中文件操作返回的错误集合。I/O可以因为任何数量的原因失败但是有三种经常的错误必须进行不同的处理文件已经存在对于创建操作找不到文件对于读取操作和权限拒绝。os包中提供了三个帮助函数来对给定的错误值表示的失败进行分类
```go
package os
@ -19,9 +19,9 @@ func IsNotExist(err error) bool {
}
```
但是处理I/O错误的逻辑可能一个和另一个平台非常的不同所以这种方案并不健壮并且对相同的失败可能会报出各种不同的错误消息。在测试的过程中通过检查错误消息的子字符串来保证特定的函数以期望的方式失败是非常有用的但对于线上的代码是不够的。
但是处理I/O错误的逻辑可能一个和另一个平台非常的不同所以这种方案并不健壮并且对相同的失败可能会报出各种不同的错误消息。在测试的过程中,通过检查错误消息的子字符串来保证特定的函数以期望的方式失败是非常有用的,但对于线上的代码是不够的。
一个更可靠的方式是使用一个专门的类型来描述结构化的错误。os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败像Open或者Delete操作,并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作像Symlink和Rename。这下面是os.PathError
一个更可靠的方式是使用一个专门的类型来描述结构化的错误。os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败像Open或者Delete操作并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作像Symlink和Rename。这下面是os.PathError
```go
package os
@ -38,7 +38,7 @@ func (e *PathError) Error() string {
}
```
大多数调用方都不知道PathError并且通过调用错误本身的Error方法来统一处理所有的错误。尽管PathError的Error方法简单地把这些字段连接起来生成错误消息PathError的结构保护了内部的错误组件。调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开具体的类型比字符串可以提供更多的细节。
大多数调用方都不知道PathError并且通过调用错误本身的Error方法来统一处理所有的错误。尽管PathError的Error方法简单地把这些字段连接起来生成错误消息PathError的结构保护了内部的错误组件。调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开具体的类型可以比字符串提供更多的细节。
```go
_, err := os.Open("/no/such/file")

View File

@ -44,9 +44,9 @@ func writeHeader(w io.Writer, contentType string) error {
}
```
为了避免重复定义我们将这个检查移入到一个实用工具函数writeString中但是它太有用了以致标准库将它作为io.WriteString函数提供。这是向一个io.Writer接口写入字符串的推荐方法。
为了避免重复定义我们将这个检查移入到一个实用工具函数writeString中但是它太有用了以致标准库将它作为io.WriteString函数提供。这是向一个io.Writer接口写入字符串的推荐方法。
这个例子的神奇之处在于没有定义了WriteString方法的标准接口和没有指定它是一个需要行为的标准接口。而且一个具体类型只会通过它的方法决定它是否满足stringWriter接口而不是任何它和这个接口类型表明的关系。它的意思就是上面的技术依赖于一个假设;这个假设就是,如果一个类型满足下面的这个接口然后WriteString(s)方法必须和Write([]byte(s))有相同的效果。
这个例子的神奇之处在于没有定义了WriteString方法的标准接口也没有指定它是一个所需行为的标准接口。一个具体类型只会通过它的方法决定它是否满足stringWriter接口而不是任何它和这个接口类型所表达的关系。它的意思就是上面的技术依赖于一个假设,这个假设就是:如果一个类型满足下面的这个接口然后WriteString(s)方法必须和Write([]byte(s))有相同的效果。
```go
interface {
@ -55,9 +55,9 @@ interface {
}
```
尽管io.WriteString记录了它的假设,但是调用它的函数极少有可能会去记录它们也做了同样的假设。定义一个特定类型的方法隐式地获取了对特定行为的协约。对于Go语言的新手特别是那些来自有强类型语言使用背景的新手可能会发现它缺乏显式的意图令人感到混乱但是在实战的过程中这几乎不是一个问题。除了空接口interface{},接口类型很少意外巧合地被实现。
尽管io.WriteString实施了这个假设,但是调用它的函数极少可能会去实施类似的假设。定义一个特定类型的方法隐式地获取了对特定行为的协约。对于Go语言的新手特别是那些来自有强类型语言使用背景的新手可能会发现它缺乏显式的意图令人感到混乱但是在实战的过程中这几乎不是一个问题。除了空接口interface{}接口类型很少意外巧合地被实现。
上面的writeString函数使用一个类型断言来知一个普遍接口类型的值是否满足一个更加具体的接口类型;并且如果满足,它会使用这个更具体接口的行为。这个技术可以被很好的使用不论这个被询问的接口是一个标准如io.ReadWriter或者用户定义的如stringWriter。
上面的writeString函数使用一个类型断言来知一个普遍接口类型的值是否满足一个更加具体的接口类型;并且如果满足,它会使用这个更具体接口的行为。这个技术可以被很好的使用不论这个被询问的接口是一个标准如io.ReadWriter或者用户定义的如stringWriter接口
这也是fmt.Fprintf函数怎么从其它所有值中区分满足error或者fmt.Stringer接口的值。在fmt.Fprintf内部有一个将单个操作对象转换成一个字符串的步骤像下面这样
@ -75,6 +75,6 @@ func formatOneValue(x interface{}) string {
}
```
如果x满足这两个接口类型中的一个具体满足的接口决定对值的格式化方式。如果都不满足默认的case或多或少会统一地使用反射来处理所有的其它类型我们可以在第12章知道具体是怎么实现的。
如果x满足这两个接口类型中的一个具体满足的接口决定对值的格式化方式。如果都不满足默认的case或多或少会统一地使用反射来处理所有的其它类型我们可以在第12章知道具体是怎么实现的。
再一次的它假设任何有String方法的类型满足fmt.Stringer中约定的行为这个行为会返回一个适合打印的字符串。
再一次的它假设任何有String方法的类型满足fmt.Stringer中约定的行为这个行为会返回一个适合打印的字符串。

View File

@ -1,8 +1,8 @@
## 7.13. 类型开关
## 7.13. 类型分支
接口被以两种不同的方式使用。在第一个方式中以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handler和error为典型一个接口的方法表达了实现这个接口的具体类型间的相似性但是隐藏了代的细节和这些具体类型本身的操作。重点在于方法上,而不是具体的类型上。
接口被以两种不同的方式使用。在第一个方式中以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handler和error为典型一个接口的方法表达了实现这个接口的具体类型间的相似性但是隐藏了代的细节和这些具体类型本身的操作。重点在于方法上,而不是具体的类型上。
第二个方式利用一个接口值可以持有各种具体类型值的能力并且将这个接口认为是这些类型的union联合。类型断言用来动态地区别这些类型并且对每一种情况都不一样。在这个方式中,重点在于具体的类型满足这个接口,而不在于接口的方法如果它确实有一些的话并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions可辨识联合
第二个方式利用一个接口值可以持有各种具体类型值的能力,将这个接口认为是这些类型的联合。类型断言用来动态地区别这些类型,使得对每一种情况都不一样。在这个方式中重点在于具体的类型满足这个接口而不在于接口的方法如果它确实有一些的话并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions可辨识联合
如果你熟悉面向对象编程你可能会将这两种方式当作是subtype polymorphism子类型多态和 ad hoc polymorphism非参数多态但是你不需要去记住这些术语。对于本章剩下的部分我们将会呈现一些第二种方式的例子。
@ -19,7 +19,7 @@ func listTracks(db sql.DB, artist string, minYear, maxYear int) {
}
```
Exec方法使用SQL字面量替换在查询字符串中的每个'?'SQL字面量表示相应参数的值它有可能是一个布尔值一个数字一个字符串或者nil空值。用这种方式构造查询可以帮助避免SQL注入攻击这种攻击就是对手可以通过利用输入内容中不正确的引来控制查询语句。在Exec函数内部我们可能会找到像下面这样的一个函数它会将每一个参数值转换成它的SQL字面量符号。
Exec方法使用SQL字面量替换在查询字符串中的每个'?'SQL字面量表示相应参数的值它有可能是一个布尔值一个数字一个字符串或者nil空值。用这种方式构造查询可以帮助避免SQL注入攻击这种攻击就是对手可以通过利用输入内容中不正确的引来控制查询语句。在Exec函数内部我们可能会找到像下面这样的一个函数它会将每一个参数值转换成它的SQL字面量符号。
```go
func sqlQuote(x interface{}) string {
@ -42,9 +42,9 @@ func sqlQuote(x interface{}) string {
}
```
switch语句可以简化if-else链如果这个if-else链对一连串值做相等测试。一个相似的type switch类型开关可以简化类型断言的if-else链。
switch语句可以简化if-else链如果这个if-else链对一连串值做相等测试。一个相似的type switch类型分支可以简化类型断言的if-else链。
它最简单的形式中一个类型开关像普通的switch语句一样它的运算对象是x.(type)它使用了关键词字面量type并且每个case有一到多个类型。一个类型开关基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型开关可能会有这些case
最简单的形式中一个类型分支像普通的switch语句一样它的运算对象是x.(type)——它使用了关键词字面量type——并且每个case有一到多个类型。一个类型分支基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型分支可能会有这些case
```go
switch x.(type) {
@ -58,15 +58,15 @@ switch x.(type) {
和(§1.8)中的普通switch语句一样每一个case会被顺序的进行考虑并且当一个匹配找到时这个case中的内容会被执行。当一个或多个case类型是接口时case的顺序就会变得很重要因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。
注意到在原来的函数中对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型类型开关语句有一个扩展的形式它可以将提取的值绑定到一个在每个case范围内的新变量。
注意到在原来的函数中对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型类型分支语句有一个扩展的形式它可以将提取的值绑定到一个在每个case范围内都有效的新变量。
```go
switch x := x.(type) { /* ... */ }
```
这里我们已经将新的变量也命名为x和类型断言一样重用变量名是很常见的。和一个switch语句相似地一个类型开关隐式的创建了一个语言因此新变量x的定义不会和外面块中的x变量冲突。每一个case也会隐式的创建一个单独的语言块。
这里我们已经将新的变量也命名为x和类型断言一样重用变量名是很常见的。和一个switch语句相似地一个类型分支隐式的创建了一个词法因此新变量x的定义不会和外面块中的x变量冲突。每一个case也会隐式的创建一个单独的词法块。
使用类型开关的扩展形式来重写sqlQuote函数会让这个函数更加的清晰
使用类型分支的扩展形式来重写sqlQuote函数会让这个函数更加的清晰
```go
func sqlQuote(x interface{}) string {
@ -88,6 +88,6 @@ func sqlQuote(x interface{}) string {
}
```
在这个版本的函数中在每个单一类型的case内部变量x和这个case的类型相同。例如变量x在bool的case中是bool类型和string的case中是string类型。在所有其它的情况中变量x是switch运算对象的类型接口在这个例子中运算对象是一个interface{}。当多个case需要相同的操作时比如int和uint的情况类型开关可以很容易的合并这些情况。
在这个版本的函数中在每个单一类型的case内部变量x和这个case的类型相同。例如变量x在bool的case中是bool类型和string的case中是string类型。在所有其它的情况中变量x是switch运算对象的类型接口在这个例子中运算对象是一个interface{}。当多个case需要相同的操作时比如int和uint的情况类型分支可以很容易的合并这些情况。
尽管sqlQuote接受一个任意类型的参数但是这个函数只会在它的参数匹配类型开关中的一个case时运行到结束其它情况的它会panic出“unexpected type”消息。虽然x的类型是interface{}但是我们把它认为是一个intuintboolstring和nil值的discriminated union可识别联合
尽管sqlQuote接受一个任意类型的参数但是这个函数只会在它的参数匹配类型分支中的一个case时运行到结束其它情况的它会panic出“unexpected type”消息。虽然x的类型是interface{}但是我们把它认为是一个intuintboolstring和nil值的discriminated union可识别联合

View File

@ -1,6 +1,6 @@
## 7.14. 示例: 基于标记的XML解码
第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。encoding/xml包提供了一个相似的API。当我们想构造一个文档树的表示时使用encoding/xml包会很方便但是对于很多程序并不是必须的。encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。在基于标记的样式中解析器消费输入产生一个标记流四个主要的标记类型StartElementEndElementCharData和Comment每一个都是encoding/xml包中的具体类型。每一个对(\*xml.Decoder).Token的调用都返回一个标记。
第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。encoding/xml包提供了一个相似的API。当我们想构造一个文档树的表示时使用encoding/xml包会很方便但是对于很多程序并不是必须的。encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。在基于标记的样式中解析器消费输入产生一个标记流四个主要的标记类型StartElementEndElementCharData和Comment每一个都是encoding/xml包中的具体类型。每一个对(\*xml.Decoder).Token的调用都返回一个标记。
这里显示的是和这个API相关的部分
@ -33,9 +33,9 @@ func NewDecoder(io.Reader) *Decoder
func (*Decoder) Token() (Token, error) // returns next Token in sequence
```
这个没有方法的Token接口也是一个可识别联合的例子。传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节这样就可以创造出新的实现在这个实现中每个具体类型都被统一地对待。相反,满足可识别联合的具体类型的集合被设计确定和暴露而不是隐藏。可识别的联合类型几乎没有方法操作它们的函数使用一个类型开关的case集合来进行表述这个case集合中每一个case中有不同的逻辑。
这个没有方法的Token接口也是一个可识别联合的例子。传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节这样就可以创造出新的实现在这个实现中每个具体类型都被统一地对待。相反,满足可识别联合的具体类型的集合被设计为确定和暴露而不是隐藏。可识别联合的类型几乎没有方法操作它们的函数使用一个类型分支的case集合来进行表述这个case集合中每一个case都有不同的逻辑。
下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。使用上面的API它可以在输入上一次完成它的工作而从来不要具体化这个文档树。
下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。使用上面的API它可以在输入上一次完成它的工作而从来不要实例化这个文档树。
<u><i>gopl.io/ch7/xmlselect</i></u>
```go
@ -89,7 +89,7 @@ func containsAll(x, y []string) bool {
}
```
每次main函数中的循环遇到一个StartElement时它把这个元素的名称压到一个栈里并且每次遇到EndElement时它将名称从这个栈中推出。这个API保证了StartElement和EndElement的序列可以被完全的匹配甚至在一个糟糕的文档格式中。注释会被忽略。当xmlselect遇到一个CharData时只有当栈中有序地包含所有通过命令行参数传入的元素名称时它才会输出相应的文本。
main函数中的循环遇到一个StartElement时它把这个元素的名称压到一个栈里并且每次遇到EndElement时它将名称从这个栈中推出。这个API保证了StartElement和EndElement的序列可以被完全的匹配甚至在一个糟糕的文档格式中。注释会被忽略。当xmlselect遇到一个CharData时只有当栈中有序地包含所有通过命令行参数传入的元素名称时它才会输出相应的文本。
下面的命令打印出任意出现在两层div元素下的h2元素的文本。它的输入是XML的说明文档并且它自己就是XML文档格式的。
@ -108,15 +108,15 @@ html body div div h2: B Definitions for Character Normalization
...
```
**练习 7.17** 扩展xmlselect程序以便让元素不仅可以通过名称选择也可以通过它们CSS样式上属性进行选择;例如一个像这样
**练习 7.17** 扩展xmlselect程序以便让元素不仅可以通过名称选择也可以通过它们CSS风格的属性进行选择。例如一个像这样
``` html
<div id="page" class="wide">
```
的元素可以通过匹配id或者class同时还有它的名称来进行选择。
的元素可以通过匹配id或者class同时还有它的名称来进行选择。
**练习 7.18** 使用基于标记的解码API编写一个可以读取任意XML文档和构造这个文档所代表的普通节点树的程序。节点有两种类型CharData节点表示文本字符串和 Element节点表示被命名的元素和它们的属性。每一个元素节点有一个节点的切片。
**练习 7.18** 使用基于标记的解码API编写一个可以读取任意XML文档并构造这个文档所代表的通用节点树的程序。节点有两种类型CharData节点表示文本字符串和 Element节点表示被命名的元素和它们的属性。每一个元素节点有一个节点的切片。
你可能发现下面的定义会对你有帮助。

View File

@ -4,6 +4,6 @@
当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好方式。
因为在Go语言中只有当两个或更多的类型实现一个接口时才使用接口它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法经常和io.Writer或 fmt.Stringer一样只有一个的更小的接口。当新的类型出现时,小的接口更容易满足。对于接口设计的一个好的标准就是 ask only for what you need只考虑你需要的东西
因为在Go语言中只有当两个或更多的类型实现一个接口时才使用接口它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口经常和io.Writer或 fmt.Stringer一样只有一个。当新的类型出现时小的接口更容易满足。对于接口设计的一个好的标准就是 ask only for what you need只考虑你需要的东西
我们完成了对methods和接口的学习过程。Go语言对面向对象风格的编程支持良好但这并不意味着你只能使用这一风格。不是任何事物都需要被当做一个对象独立的函数有它们自己的用处未封装的数据类型也是这样。观察一下在本书前五章的例子中像input.Scan这样的方法被调用不超过二十次与之相反的是普遍的函数调用如fmt.Printf。
我们完成了对方法和接口的学习过程。Go语言对面向对象风格的编程支持良好但这并不意味着你只能使用这一风格。不是任何事物都需要被当做一个对象独立的函数有它们自己的用处未封装的数据类型也是这样。观察一下在本书前五章的例子中像input.Scan这样的方法被调用不超过二十次与之相反的是普遍调用的函数如fmt.Printf。

View File

@ -49,7 +49,7 @@ 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命令),这个工具可以用来执行网络连接操作。
@ -147,7 +147,7 @@ $ ./netcat1
$ killall clock2
```
**练习 8.1** 修改clock2来支持传入参数作为端口号然后写一个clockwall的程序这个程序可以同时与多个clock服务器通信从多服务器中读取时间并且在一个表格中一次显示所有服务传回的结果类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话让这些服务器跑在不同的机器上面或者在同一台机器上跑多个不同的实例这些实例监听不同的端口假装自己在不同的时区。像下面这样
**练习 8.1** 修改clock2来支持传入参数作为端口号然后写一个clockwall的程序这个程序可以同时与多个clock服务器通信从多服务器中读取时间,并且在一个表格中一次显示所有服务传回的结果,类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话,让这些服务器跑在不同的机器上面;或者在同一台机器上跑多个不同的实例,这些实例监听不同的端口,假装自己在不同的时区。像下面这样:
```
$ TZ=US/Eastern ./clock2 -port 8010 &
@ -156,4 +156,4 @@ $ TZ=Europe/London ./clock2 -port 8030 &
$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030
```
**练习 8.2** 实现一个并发FTP服务器。服务器应该解析客户端来的一些命令比如cd命令来切换目录ls来列出目录内文件get和send来传输文件close来关闭连接。你可以用标准的ftp命令来作为客户端或者也可以自己实现一个。
**练习 8.2** 实现一个并发FTP服务器。服务器应该解析客户端来的一些命令比如cd命令来切换目录ls来列出目录内文件get和send来传输文件close来关闭连接。你可以用标准的ftp命令来作为客户端或者也可以自己实现一个。

View File

@ -29,7 +29,7 @@ func main() {
}
```
当用户关闭了标准输入主goroutine中的mustCopy函数调用将返回然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件end-of-le结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”“从关闭的连接读”类似的错误因此我们临时移除了错误日志语句在练习8.3将会提供一个更好的解决方案。需要注意的是go语句调用了一个函数字面量这Go语言中启动goroutine常用的形式。
当用户关闭了标准输入主goroutine中的mustCopy函数调用将返回然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件end-of-file结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”“从关闭的连接读”类似的错误因此我们临时移除了错误日志语句在练习8.3将会提供一个更好的解决方案。需要注意的是go语句调用了一个函数字面量这Go语言中启动goroutine常用的形式。
在后台goroutine返回之前它先打印一个日志信息然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此总是可以在程序退出前正确输出“done”消息。

View File

@ -93,7 +93,7 @@ func main() {
}
```
其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭当它没有被引用时将会被Go语言的垃圾自动回收器回收。不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件都需要在不使用的使用调用对应的Close方法来关闭文件。
其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭当它没有被引用时将会被Go语言的垃圾自动回收器回收。不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件都需要在不使用的时候调用对应的Close方法来关闭文件。
试图重复关闭一个channel将导致panic异常试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制我们将在8.9节讨论。

View File

@ -69,15 +69,15 @@ func request(hostname string) (response string) { /* ... */ }
如果我们使用了无缓存的channel那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况称为goroutines泄漏这将是一个BUG。和垃圾变量不同泄漏的goroutines并不会被自动回收因此确保每个不再需要的goroutine能正常退出是重要的。
关于无缓存或带缓存channels之间的选择或者是带缓存channels的容量大小的选择都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作但是对于带缓存channel这些操作是解耦的。同样即使我们知道将要发送到一个channel的信息的数量上限创建一个对应容量大小的带缓存channel也是不现实的因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓将导致程序死锁。
关于无缓存或带缓存channels之间的选择或者是带缓存channels的容量大小的选择都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作但是对于带缓存channel这些操作是解耦的。同样即使我们知道将要发送到一个channel的信息的数量上限创建一个对应容量大小的带缓存channel也是不现实的因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓将导致程序死锁。
Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师一个烘焙一个上糖衣还有一个将每个蛋糕传递到它下一个厨师生产线。在狭小的厨房空间环境每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它这类似于在一个无缓存的channel上进行沟通。
Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师一个烘焙一个上糖衣还有一个将每个蛋糕传递到它下一个厨师生产线。在狭小的厨房空间环境每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它这类似于在一个无缓存的channel上进行沟通。
如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕制作中这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近那么其中大部分的传输工作将是迅速的个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动例如一个厨师可以短暂地休息然后再加快赶上进度而不影响其他人。
如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕制作中这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近那么其中大部分的传输工作将是迅速的个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动例如一个厨师可以短暂地休息然后再加快赶上进度而不影响其他人。
另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。
生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如如果第二阶段是需要精心制作的复杂操作一个厨师可能无法跟上第一个厨师的进度或者是无法满足第三阶段厨师的需求。要解决这个问题我们可以雇佣另一个厨师来帮助完成第二阶段的工作他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。
生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如如果第二阶段是需要精心制作的复杂操作一个厨师可能无法跟上第一个厨师的进度或者是无法满足第三阶段厨师的需求。要解决这个问题我们可以雇佣另一个厨师来帮助完成第二阶段的工作他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。
我们没有太多的空间展示全部细节但是gopl.io/ch8/cake包模拟了这个蛋糕店可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试§11.4

View File

@ -26,7 +26,7 @@ func makeThumbnails(filenames []string) {
}
```
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注embarrassingly parallel直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受并发带来的好处,能够随着并行的规模线性地扩展。
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注embarrassingly parallel直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受并发带来的好处,能够随着并行的规模线性地扩展。
下面让我们并行地执行这些操作从而将文件IO的延迟隐藏掉并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误之后再进行处理。

View File

@ -1,6 +1,6 @@
## 8.6. 示例: 并发的Web爬虫
在5.6节中我们做了一个简单的web爬虫用bfs(广度优先)算法来抓取整个网站。在本节中,我们会让这个这个爬虫并行化这样每一个彼此独立的抓取命令可以并行进行IO最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。
在5.6节中我们做了一个简单的web爬虫用bfs(广度优先)算法来抓取整个网站。在本节中我们会让这个爬虫并行化这样每一个彼此独立的抓取命令可以并行进行IO最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。
<u><i>gopl.io/ch8/crawl1</i></u>
```go
@ -58,7 +58,7 @@ 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{}来作为其元素。
@ -111,7 +111,7 @@ func main() {
}
```
这个版本中,计器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时我们都会对n进行++操作在向worklist中发送初始的命令行参数之前我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止这时候说明没活可干了。
这个版本中,计器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时我们都会对n进行++操作在向worklist中发送初始的命令行参数之前我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止这时候说明没活可干了。
现在这个并发爬虫会比5.6节中的深度优先搜索版快上20倍而且不会出什么错并且在其完成任务时也会正确地终止。
@ -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

@ -15,7 +15,7 @@ func main() {
}
```
现在我们让这个程序支持在倒计时中用户按下return键时直接中断发射流程。首先我们启动一个goroutine这个goroutine会尝试从标准输入中入一个单独的byte并且如果成功了会向名为abort的channel发送一个值。
现在我们让这个程序支持在倒计时中用户按下return键时直接中断发射流程。首先我们启动一个goroutine这个goroutine会尝试从标准输入中入一个单独的byte并且如果成功了会向名为abort的channel发送一个值。
<u><i>gopl.io/ch8/countdown2</i></u>
```go
@ -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 {
@ -41,11 +41,11 @@ default:
}
```
上面是select语句的一般形式。和switch语句稍微有点相似也会有几个case和最后的default选择支。每一个case代表一个通信操作(在某个channel上进行发送或者接收)并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的)就像上面的第一个case或者包含在一个简短的变量声明中像第二个case里一样第二种形式让你能够引用接收到的值。
上面是select语句的一般形式。和switch语句稍微有点相似也会有几个case和最后的default选择分支。每一个case代表一个通信操作在某个channel上进行发送或者接收并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的)就像上面的第一个case或者包含在一个简短的变量声明中像第二个case里一样第二种形式让你能够引用接收到的值。
select会等待case中有能够执行的case时去执行。当条件满足时select才会去通信并执行case之后的语句这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。
让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待到两个事件中的一个到达无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入那么火箭就会发射。
让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待到两个事件中的一个到达无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入那么火箭就会发射。
```go
func main() {

View File

@ -28,7 +28,7 @@ func dirents(dir string) []os.FileInfo {
}
```
ioutil.ReadDir函数会返回一个os.FileInfo类型的sliceos.FileInfo类型也是os.Stat这个函数的返回值。对每一个子目录而言walkDir会递归地调用其自身并且会对每一个文件也递归调用。walkDir函数会向fileSizes这个channel发送一条消息。这条消息包含了文件的字节大小。
ioutil.ReadDir函数会返回一个os.FileInfo类型的sliceos.FileInfo类型也是os.Stat这个函数的返回值。对每一个子目录而言walkDir会递归地调用其自身同时也在递归里获取每一个文件的信息。walkDir函数会向fileSizes这个channel发送一条消息。这条消息包含了文件的字节大小。
下面的主函数用了两个goroutine。后台的goroutine调用walkDir来遍历命令行给出的每一个路径并最终关闭fileSizes这个channel。主goroutine会对其从channel中接收到的文件大小进行累加并输出其和。

View File

@ -8,7 +8,7 @@ Go语言并没有提供在一个goroutine中终止另一个goroutine的方法
回忆一下我们关闭了一个channel并且被消费掉了所有已发送的值操作channel之后的代码可以立即被执行并且会产生零值。我们可以将这个机制扩展一下来作为我们的广播机制不要向channel发送值而是用关闭一个channel来进行广播。
只要一些小修改我们就可以把退出逻辑加入到前一节的du程序。首先我们创建一个退出的channel这个channel不会向其中发送任何值但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数cancelled这个函数在被调用的时候会轮询退出状态。
只要一些小修改我们就可以把退出逻辑加入到前一节的du程序。首先我们创建一个退出的channel不需要向这个channel发送任何值但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数cancelled这个函数在被调用的时候会轮询退出状态。
<u><i>gopl.io/ch8/du4</i></u>
```go

View File

@ -55,9 +55,9 @@ func broadcaster() {
}
```
broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时会更新clients集合当该事件是离开行为时它会关闭客户端的消息发channel。broadcaster也会监听全局的消息channel所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时就会将其广播至所有连接到服务端的客户端。
broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时会更新clients集合当该事件是离开行为时它会关闭客户端的消息发channel。broadcaster也会监听全局的消息channel所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时就会将其广播至所有连接到服务端的客户端。
现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本并通过全局的消息channel来将这些文本发送出去并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。
现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本并通过全局的消息channel来将这些文本发送出去并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。
```go
func handleConn(conn net.Conn) {
@ -87,7 +87,7 @@ func clientWriter(conn net.Conn, ch <-chan string) {
}
```
另外handleConn为每一个客户端创建了一个clientWriter的goroutine来接收向客户端发出消息channel中发送的广播消息,并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。
另外handleConn为每一个客户端创建了一个clientWriter的goroutine用来接收向客户端发送消息的channel中的广播消息并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。
下面演示的是当服务器有两个活动的客户端连接并且在两个窗口中运行的情况使用netcat来聊天
@ -99,8 +99,7 @@ $ ./netcat3
You are 127.0.0.1:64208 $ ./netcat3
127.0.0.1:64211 has arrived You are 127.0.0.1:64211
Hi!
127.0.0.1:64208: Hi!
127.0.0.1:64208: Hi!
127.0.0.1:64208: Hi! 127.0.0.1:64208: Hi!
Hi yourself.
127.0.0.1:64211: Hi yourself. 127.0.0.1:64211: Hi yourself.
^C
@ -113,12 +112,12 @@ 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事件通知当前所有的客户端。为了达成这个目的你需要有一个客户端的集合并且在entering和leaving的channel中记录客户端的名字。
**练习 8.13** 使聊天服务器能够断开空闲的客户端连接比如最近五分钟之后没有发送任何消息的那些客户端。提示可以在其它goroutine中调用conn.Close()来解除Read调用就像input.Scanner()所做的那样。
**练习 8.14** 修改聊天服务器的网络协议这样每一个客户端就可以在entering时可以提供它们的名字。将消息前缀由之前的网络地址改为这个名字。
**练习 8.14** 修改聊天服务器的网络协议这样每一个客户端就可以在entering时提供他们的名字。将消息前缀由之前的网络地址改为这个名字。
**练习 8.15** 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息而不是等待这个客户端一直到其准备好写。或者为每一个客户端的消息发出channel建立缓冲区这样大部分的消息便不会被丢掉broadcaster应该用一个非阻塞的send向这个channel中发消息。
**练习 8.15** 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息而不是等待这个客户端一直到其准备好读写。或者为每一个客户端的消息发送channel建立缓冲区这样大部分的消息便不会被丢掉broadcaster应该用一个非阻塞的send向这个channel中发消息。

View File

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

View File

@ -1,12 +1,12 @@
## 9.1. 竞争条件
在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话)以此类推。在有两个或更多goroutine的程序中每一个goroutine内的语句也是按照既定的顺序去执行的但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序x是在y之前还是之后还是同时发生是没法判断的。当我们能够没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话就说明x和y这两个事件是并发的。
在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话)以此类推。在有两个或更多goroutine的程序中每一个goroutine内的语句也是按照既定的顺序去执行的但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话就说明x和y这两个事件是并发的。
考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么类型便是并发安全的。
在一个程序中有非并发安全的类型的情况下我们依然可以使这个程序并发安全。确实并发安全的类型是例外而不是规则所以只有当文档中明确地说明了其是并发安全的情况下你才可以并发地去访问它。我们会避免并发访问大多数的类型无论是将变量局限在单一的一个goroutine内还是用互斥条件维持更高级别的不变性都是为了这个目的。我们会在本章中说明这些术语。
在一个程序中有非并发安全的类型的情况下我们依然可以使这个程序并发安全。确实并发安全的类型是例外而不是规则所以只有当文档中明确地说明了其是并发安全的情况下你才可以并发地去访问它。我们会避免并发访问大多数的类型无论是将变量局限在单一的一个goroutine内还是用互斥条件维持更高级别的不变性都是为了这个目的。我们会在本章中说明这些术语。
相反,导出包级别的函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine所以修改这些变量“必须”使用互斥条件。
相反,包级别的导出函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine所以修改这些变量“必须”使用互斥条件。
一个函数在并发调用时没法工作的原因太多了,比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。
@ -114,7 +114,7 @@ func Icon(name string) image.Image { return icons[name] }
第二种避免数据竞争的方法是避免从多个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会监控(monitor)clients map的全部访问。
由于其它的goroutine不能够直接访问变量它们只能使用一个channel来发送请求给指定的goroutine来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信使用通信来共享数据”。一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的monitor监控goroutine。例如broadcaster goroutine会监控clients map的全部访问。
下面是一个重写了的银行的例子这个例子中balance变量被限制在了monitor goroutine中名为teller

View File

@ -102,7 +102,7 @@ func Withdraw(amount int) bool {
上面这个例子中Deposit会调用mu.Lock()第二次去获取互斥锁但因为mutex已经锁上了而无法被重入(译注go里没有重入锁关于重入锁的概念请参考java)--也就是说没法对一个已经锁上的mutex来再次上锁--这会导致程序死锁没法继续执行下去Withdraw会永远阻塞下去。
关于Go的互斥量不能重入这一点我们有很充分的理由。互斥量的目的是为了确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”。但实际上对于mutex保护的变量来说不变性还包括其它方面。当一个goroutine获得了一个互斥锁时它会断定这种不变性能够被保持。其获取并保持锁期间可能会去更新共享变量这样不变性只是短暂地被破坏。然而当其释放锁之后它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量但这种方式没法保证这些变量额外的不变性。(译注:这段翻译有点晕)
关于Go的互斥量不能重入这一点我们有很充分的理由。互斥量的目的是为了确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”。但实际上这里对于mutex保护的变量来说不变性还包括其它方面。当一个goroutine获得了一个互斥锁时它会断定这种不变性能够被保持。其获取并保持锁期间可能会去更新共享变量这样不变性只是短暂地被破坏。然而当其释放锁之后它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量但这种方式没法保证这些变量额外的不变性。(译注:这段翻译有点晕)
一个通用的解决方案是将一个函数分离为多个函数比如我们把Deposit分离成两个一个不导出的函数deposit这个函数假设锁总是会被保持并去做实际的操作另一个是导出的函数Deposit这个函数会调用deposit但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式
@ -134,6 +134,6 @@ func Balance() int {
func deposit(amount int) { balance += amount }
```
当然这里的存款deposit函数很小实际上取款withdraw函数不需要理会对它的调用尽管如此这里的表达还是表明了规则。
当然这里的存款deposit函数很小实际上取款Withdraw函数不需要理会对它的调用尽管如此这里的表达还是表明了规则。
封装(§6.6), 用限制一个程序中的意外交互的方式可以使我们获得数据结构的不变性。因为某种原因封装还帮我们获得了并发的不变性。当你使用mutex时确保mutex和其保护的变量没有被导出(在go里也就是小写且不要被大写字母开头的函数访问啦)无论这些变量是包级的变量还是一个struct的字段。

View File

@ -1,6 +1,6 @@
## 9.4. 内存同步
你可能比较纠结为什么Balance方法需要用到互斥条件无论是基于channel还是基于互斥量。毕竟和存款不一样它只由一个简单的操作组成所以不会碰到其它goroutine在其执行"期间"执行其它的逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二(更重要)的是"同步"不仅仅是一堆goroutine执行顺序的问题同样也会涉及到内存的问题。
你可能比较纠结为什么Balance方法需要用到互斥条件无论是基于channel还是基于互斥量。毕竟和存款不一样它只由一个简单的操作组成所以不会碰到其它goroutine在其执行“期间”执行其它逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二更重要的是“同步”不仅仅是一堆goroutine执行顺序的问题同样也会涉及到内存的问题。
在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率对内存的写入一般会在每一个处理器中缓冲并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。
@ -27,17 +27,16 @@ x:1 y:1
y:1 x:1
```
第四行可以被解释为执行顺序A1,B1,A2,B2或者B1,A1,A2,B2的执行结果。
然而实际的运行时还是有些情况让我们有点惊讶:
第四行可以被解释为执行顺序A1,B1,A2,B2或者B1,A1,A2,B2的执行结果。然而实际运行时还是有些情况让我们有点惊讶
```
x:0 y:0
y:0 x:0
```
但是根据所使用的编译器CPU或者其它很多影响因子这两种情况也是有可能发生的。那么这两种情况要怎么解释呢
根据所使用的编译器CPU或者其它很多影响因子这两种情况也是有可能发生的。那么这两种情况要怎么解释呢
在一个独立的goroutine中每一个语句的执行顺序是可以被保证的也就是说goroutine是顺序连贯的。但是在不使用channel且不使用mutex这样的显式同步操作时我们就没法保证事件在不同的goroutine中看到的执行顺序是一致的了。尽管goroutine A中一定需要观察到x=1执行成功之后才会去读取y但它没法确保自己观察得到goroutine B中对y的写入所以A还可能会打印出y的一个旧版的值。
在一个独立的goroutine中每一个语句的执行顺序是可以被保证的也就是说goroutine内顺序是连贯的。但是在不使用channel且不使用mutex这样的显式同步操作时我们就没法保证事件在不同的goroutine中看到的执行顺序是一致的了。尽管goroutine A中一定需要观察到x=1执行成功之后才会去读取y但它没法确保自己观察得到goroutine B中对y的写入所以A还可能会打印出y的一个旧版的值。
尽管去理解并发的一种尝试是去将其运行理解为不同goroutine语句的交错执行但看看上面的例子这已经不是现代的编译器和cpu的工作方式了。因为赋值和打印指向不同的变量编译器可能会断定两条语句的顺序不会影响执行结果并且会交换两个语句的执行顺序。如果两个goroutine在不同的CPU上执行每一个核心有自己的缓存这样一个goroutine的写入对于其它goroutine的Print在主存同步之前就是不可见的了。

View File

@ -1,6 +1,6 @@
## 9.5. sync.Once初始化
如果初始化成本比较大的话,那么将初始化延迟到需要的时候再去做就是一个比较好的选择。如果在程序启动的时候就去做这类初始化的话会增加程序的启动时间并且因为执行的时候可能也并不需要这些变量所以实际上有一些浪费。让我们在本章早一些时候看到的icons变量
如果初始化成本比较大的话,那么将初始化延迟到需要的时候再去做就是一个比较好的选择。如果在程序启动的时候就去做这类初始化的话会增加程序的启动时间并且因为执行的时候可能也并不需要这些变量所以实际上有一些浪费。让我们来看在本章早一些时候的icons变量
```go
var icons map[string]image.Image
@ -29,7 +29,7 @@ func Icon(name string) image.Image {
如果一个变量只被一个单独的goroutine所访问的话我们可以使用上面的这种模板但这种模板在Icon被并发调用时并不安全。就像前面银行的那个Deposit(存款)函数一样Icon函数也是由多个步骤组成的首先测试icons是否为空然后load这些icons之后将icons更新为一个非空的值。直觉会告诉我们最差的情况是loadIcons函数被多次访问会带来数据竞争。当第一个goroutine在忙着loading这些icons的时候另一个goroutine进入了Icon函数发现变量是nil然后也会调用loadIcons函数。
不过这种直觉是错误的。(我们希望现在你从现在开始能够构建自己对并发的直觉,也就是说对并发的直觉总是不能被信任的!)回忆一下9.4节。因为缺少显式的同步编译器和CPU是可以随意地去更改访问内存的指令顺序以任意方式只要保证每一个goroutine自己的执行顺序一致。其中一种可能loadIcons的语句重排是下面这样。它会在填写icons变量的值之前先用一个空map来初始化icons变量。
不过这种直觉是错误的。(我们希望你从现在开始能够构建自己对并发的直觉,也就是说对并发的直觉总是不能被信任的!回忆一下9.4节。因为缺少显式的同步编译器和CPU是可以随意地去更改访问内存的指令顺序以任意方式只要保证每一个goroutine自己的执行顺序一致。其中一种可能loadIcons的语句重排是下面这样。它会在填写icons变量的值之前先用一个空map来初始化icons变量。
```go
func loadIcons() {

View File

@ -6,6 +6,6 @@
竞争检查器会检查这些事件会寻找在哪一个goroutine中出现了这样的case例如其读或者写了一个共享变量这个共享变量是被另一个goroutine在没有进行干预同步操作便直接写入的。这种情况也就表明了是对一个共享变量的并发访问即数据竞争。这个工具会打印一份报告内容包含变量身份读取和写入的goroutine中活跃的函数的调用栈。这些信息在定位问题时通常很有用。9.7节中会有一个竞争检查器的实战样例。
竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你包。
竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你包。
由于需要额外的记录因此构建时加了竞争检测的程序跑起来会慢一些且需要更大的内存即使是这样这些代价对于很多生产环境的工作来说还是可以接受的。对于一些偶发的竞争条件来说让竞争检查器来干活可以节省无数日夜的debugging。(译注多少服务端C和C程序员为此竞折腰)
由于需要额外的记录因此构建时加了竞争检测的程序跑起来会慢一些且需要更大的内存即使是这样这些代价对于很多生产环境的工作来说还是可以接受的。对于一些偶发的竞争条件来说让竞争检查器来干活可以节省无数日夜的debugging。(译注多少服务端C和C++程序员为此竞折腰)

View File

@ -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

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都不需要这么大的栈。
相反一个goroutine会以一个很小的栈开始其生命周期一般只需要2KB。一个goroutine的栈和操作系统线程一样会保存其活跃或挂起的函数调用的本地变量但是和OS线程不太一样的是一个goroutine的栈大小并不是固定的栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB比传统的固定大小的线程栈要大得多尽管一般情况下大多goroutine都不需要这么大的栈。
** 练习 9.4:** 创建一个流水线程序支持用channel连接任意数量的goroutine在跑爆内存之前可以创建多少流水线阶段一个变量通过整个流水线需要用多久(这个练习题翻译不是很确定。。)

View File

@ -1,9 +1,9 @@
### 9.8.2. Goroutine调度
OS线程会被操作系统内核调度。每几毫秒一个硬件计时器会中断处理器这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并保存内存中它的寄存器内容检查线程列表并决定下一次哪个线程可以被运行并从内存中恢复该线程的寄存器信息然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度所以从一个线程向另一个“移动”需要完整的上下文切换也就是说保存一个用户线程的状态到内存恢复另一个线程的到寄存器然后更新调度器的数据结构。这几步操作很慢因为其局部性很差需要几次内存访问并且会增加运行的cpu周期。
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比调度一个线程代价要低得多。
和操作系统的线程调度不同的是Go调度器并不是用一个硬件定时器而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep或者被channel调用或者mutex操作阻塞时调度器会使其进入休眠并开始执行另一个goroutine直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文所以重新调度一个goroutine比调度一个线程代价要低得多。
** 练习 9.5: ** 写一个有两个goroutine的程序两个goroutine会向两个无buffer channel反复地发送ping-pong消息。这样的程序每秒可以支持多少次通信

View File

@ -1,6 +1,6 @@
### 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线程之类的——那么函数的行为就会变得神秘莫测。

View File

@ -61,7 +61,7 @@ Go语言尤其适合编写网络服务相关基础设施同时也适合开发
和机器学习等诸多领域。目前Go语言已经成为受欢迎的作为无类型的脚本语言的替代者
因为Go编写的程序通常比脚本语言运行的更快也更安全而且很少会发生意外的类型错误。
Go语言还是一个开源的项目可以免费获编译器、库、配套工具的源代码。
Go语言还是一个开源的项目可以免费获编译器、库、配套工具的源代码。
Go语言的贡献者来自一个活跃的全球社区。Go语言可以运行在类[UNIX](http://doc.cat-v.org/unix/)系统——
比如[Linux](http://www.linux.org/)、[FreeBSD](https://www.freebsd.org/)、[OpenBSD](http://www.openbsd.org/)、[Mac OSX](http://www.apple.com/cn/osx/)——和[Plan9](http://plan9.bell-labs.com/plan9/)系统和[Microsoft Windows](https://www.microsoft.com/zh-cn/windows/)操作系统之上。
Go语言编写的程序无需修改就可以运行在上面这些环境。