diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5282307 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.c linguist-language=Go diff --git a/.gitignore b/.gitignore index 5ed3d85..1e773c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,5 @@ -# Node rules: -## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt +/_book +/book -## Dependency directory -## Commenting this out is preferred by some people, see -## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git -node_modules - -# Book build output -_book - -# eBook build output -*.epub -*.mobi -*.pdf - -version.md -gopl-zh-* -_book.zip +*.out* +_zz* diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile index 70cdcc1..5bdc5c2 100644 --- a/Makefile +++ b/Makefile @@ -2,40 +2,11 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -# install gitbook -# npm install gitbook-cli -g - -# https://github.com/GitbookIO -# https://github.com/GitbookIO/gitbook -# https://github.com/GitbookIO/plugin-katex -# https://github.com/wastemobile/gitbook +# install mkbook # http://www.imagemagick.org/ default: - go run update_version.go - gitbook build - go run fix-data-revision.go - go run builder.go + mdbook serve -zh2tw: - go run zh2tw.go . .md$$ - -tw2zh: - go run zh2tw.go . .md$$ tw2zh - -loop: - go run zh2tw.go . .md$$ tw2zh - go run zh2tw.go . .md$$ zh2tw - -cover: - composite cover_patch.png cover_bgd.png cover.jpg - convert -resize 1800x2360! cover.jpg cover.jpg - convert -resize 200x262! cover.jpg cover_small.jpg - convert -resize 400x524! cover.jpg cover_middle.jpg - convert -quality 75% cover.jpg cover.jpg - convert -quality 75% cover_small.jpg cover_small.jpg - convert -quality 75% cover_middle.jpg cover_middle.jpg - convert -strip cover.jpg cover.jpg - convert -strip cover_small.jpg cover_small.jpg - convert -strip cover_middle.jpg cover_middle.jpg +clean: diff --git a/README.md b/README.md index eaf3218..970a47a 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ Go语言圣经 [《The Go Programming Language》](http://gopl.io) 中文版本 ![](cover_middle.jpg) -- 项目主页:http://github.com/golang-china/gopl-zh -- 原版官网:http://gopl.io +- 项目主页:[https://github.com/gopl-zh](https://github.com/gopl-zh) +- 项目主页(旧):[http://github.com/golang-china/gopl-zh](http://github.com/golang-china/gopl-zh) +- 原版官网:[http://gopl.io](http://gopl.io) 译者信息: @@ -42,16 +43,6 @@ Go语言圣经 [《The Go Programming Language》](http://gopl.io) 中文版本 **注意,在线预览不是最新版,最新以仓库里的内容为准** - -### 从源文件构建 - -先安装NodeJS和GitBook命令行工具(`npm install gitbook-cli -g`命令)。 - -1. 运行`go get github.com/golang-china/gopl-zh`,获取 [源文件](https://github.com/golang-china/gopl-zh/archive/master.zip)。 -2. 切换到 `gopl-zh` 目录,运行 `gitbook install`,安装GitBook插件。 -3. 运行`make`,生成`_book`目录。 -4. 打开`_book/index.html`文件。 - # 版权声明 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/)。 diff --git a/SUMMARY.md b/SUMMARY.md index 3ef9266..04613f6 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,11 +1,8 @@ # Summary -* [前言](preface.md) - * [Go语言起源](ch0/ch0-01.md) - * [Go语言项目](ch0/ch0-02.md) - * [本书的组织](ch0/ch0-03.md) - * [更多的信息](ch0/ch0-04.md) - * [致谢](ch0/ch0-05.md) +[Go语言圣经](index.md) +[前言](preface.md) + * [入门](ch1/ch1.md) * [Hello, World](ch1/ch1-01.md) * [命令行参数](ch1/ch1-02.md) diff --git a/book.json b/book.json deleted file mode 100644 index 2cae50e..0000000 --- a/book.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "gitbook": "3.x", - "title": "Go语言圣经", - "description": "中文版", - "language": "zh-hans", - "structure": { - "readme": "preface.md" - }, - "plugins": [ - "katex", - "-search", - "-lunr" - ] -} diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..e36aa55 --- /dev/null +++ b/book.toml @@ -0,0 +1,20 @@ +# https://giscus.app +# https://github.com/badboy/mdbook-mermaid + +[book] +title = "Go语言圣经" +authors = ["译者:", "chai2010", "Xargin", "CrazySssst", "foreversmart"] +description = "中文版" +language = "zh" +multilingual = false +src = "." + +[build] +build-dir = "book" + +[output.html] +additional-css = ["style.css"] +additional-js = ["js/custom.js", "js/bigPicture.js"] +git-repository-url = "https://github.com/gopl-zh/gopl-zh.github.com" +edit-url-template = "https://github.com/gopl-zh/gopl-zh.github.com/edit/master/{path}" +git-repository-icon = "fa-github" diff --git a/cch.png b/cch.png new file mode 100644 index 0000000..d5f83e9 Binary files /dev/null and b/cch.png differ diff --git a/ch0/ch0-01.md b/ch0/ch0-01.md index 8629d1a..812c40f 100644 --- a/ch0/ch0-01.md +++ b/ch0/ch0-01.md @@ -17,5 +17,3 @@ Go语言的另一支祖先,带来了Go语言区别其他语言的重要特性 在Plan9操作系统中,这些优秀的想法被吸收到了一个叫[Alef][Alef]的编程语言中。Alef试图将Newsqueak语言改造为系统编程语言,但是因为缺少垃圾回收机制而导致并发编程很痛苦。(译注:在Alef之后还有一个叫[Limbo][Limbo]的编程语言,Go语言从其中借鉴了很多特性。 具体请参考Pike的讲稿:http://talks.golang.org/2012/concurrency.slide#9 ) Go语言的其他的一些特性零散地来自于其他一些编程语言;比如iota语法是从[APL][APL]语言借鉴,词法作用域与嵌套函数来自于[Scheme][Scheme]语言(和其他很多语言)。当然,我们也可以从Go中发现很多创新的设计。比如Go语言的切片为动态数组提供了有效的随机存取的性能,这可能会让人联想到链表的底层的共享机制。还有Go语言新发明的defer语句。 - -{% include "../links.md" %} diff --git a/ch1/ch1.md b/ch1/ch1.md index 1796f28..daa4ca2 100644 --- a/ch1/ch1.md +++ b/ch1/ch1.md @@ -1,4 +1,4 @@ -# 第一章 入门 +# 第1章 入门 本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序,希望可以帮你尽快入门,写出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程序,涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然,第一章不会解释细枝末节,但用这些程序来学习一门新语言还是很有效的。 diff --git a/ch10/ch10-07-6.md b/ch10/ch10-07-6.md index 082a31e..1891c79 100644 --- a/ch10/ch10-07-6.md +++ b/ch10/ch10-07-6.md @@ -73,24 +73,20 @@ $ go list -json hash 命令行参数`-f`则允许用户使用text/template包(§4.6)的模板语言定义输出文本的格式。下面的命令将打印strconv包的依赖的包,然后用join模板函数将结果链接为一行,连接时每个结果之间用一个空格分隔: -{% raw %} ``` $ go list -f '{{join .Deps " "}}' strconv errors math runtime unicode/utf8 unsafe ``` -{% endraw %} 译注:上面的命令在Windows的命令行运行会遇到`template: main:1: unclosed action`的错误。产生这个错误的原因是因为命令行对命令中的`" "`参数进行了转义处理。可以按照下面的方法解决转义字符串的问题: -{% raw %} ``` $ go list -f "{{join .Deps \" \"}}" strconv ``` -{% endraw %} 下面的命令打印compress子目录下所有包的导入包列表: -{% raw %} + ``` $ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/... compress/bzip2 -> bufio io sort @@ -103,7 +99,7 @@ compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io 译注:Windows下有同样有问题,要避免转义字符串的干扰: -{% raw %} + ``` $ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/... ``` diff --git a/ch10/ch10-07.md b/ch10/ch10-07.md index 4ee3358..734d48e 100644 --- a/ch10/ch10-07.md +++ b/ch10/ch10-07.md @@ -28,14 +28,413 @@ Use "go help [command]" for more information about a command. 为了达到零配置的设计目标,Go语言的工具箱很多地方都依赖各种约定。例如,根据给定的源文件的名称,Go语言的工具可以找到源文件对应的包,因为每个目录只包含了单一的包,并且包的导入路径和工作区的目录结构是对应的。给定一个包的导入路径,Go语言的工具可以找到与之对应的存储着实体文件的目录。它还可以根据导入路径找到存储代码的仓库的远程服务器URL。 -{% include "./ch10-07-1.md" %} +### 10.7.1. 工作区结构 -{% include "./ch10-07-2.md" %} +对于大多数的Go语言用户,只需要配置一个名叫GOPATH的环境变量,用来指定当前工作目录即可。当需要切换到不同工作区的时候,只要更新GOPATH就可以了。例如,我们在编写本书时将GOPATH设置为`$HOME/gobook`: -{% include "./ch10-07-3.md" %} +``` +$ export GOPATH=$HOME/gobook +$ go get gopl.io/... +``` -{% include "./ch10-07-4.md" %} +当你用前面介绍的命令下载本书全部的例子源码之后,你的当前工作区的目录结构应该是这样的: -{% include "./ch10-07-5.md" %} +``` +GOPATH/ + src/ + gopl.io/ + .git/ + ch1/ + helloworld/ + main.go + dup/ + main.go + ... + golang.org/x/net/ + .git/ + html/ + parse.go + node.go + ... + bin/ + helloworld + dup + pkg/ + darwin_amd64/ + ... +``` -{% include "./ch10-07-6.md" %} +GOPATH对应的工作区目录有三个子目录。其中src子目录用于存储源代码。每个包被保存在与$GOPATH/src的相对路径为包导入路径的子目录中,例如gopl.io/ch1/helloworld相对应的路径目录。我们看到,一个GOPATH工作区的src目录中可能有多个独立的版本控制系统,例如gopl.io和golang.org分别对应不同的Git仓库。其中pkg子目录用于保存编译后的包的目标文件,bin子目录用于保存编译后的可执行程序,例如helloworld可执行程序。 + +第二个环境变量GOROOT用来指定Go的安装目录,还有它自带的标准库包的位置。GOROOT的目录结构和GOPATH类似,因此存放fmt包的源代码对应目录应该为$GOROOT/src/fmt。用户一般不需要设置GOROOT,默认情况下Go语言安装工具会将其设置为安装的目录路径。 + +其中`go env`命令用于查看Go语言工具涉及的所有环境变量的值,包括未设置环境变量的默认值。GOOS环境变量用于指定目标操作系统(例如android、linux、darwin或windows),GOARCH环境变量用于指定处理器的类型,例如amd64、386或arm等。虽然GOPATH环境变量是唯一必须要设置的,但是其它环境变量也会偶尔用到。 + +``` +$ go env +GOPATH="/home/gopher/gobook" +GOROOT="/usr/local/go" +GOARCH="amd64" +GOOS="darwin" +... +``` + +### 10.7.2. 下载包 + +使用Go语言工具箱的go命令,不仅可以根据包导入路径找到本地工作区的包,甚至可以从互联网上找到和更新包。 + +使用命令`go get`可以下载一个单一的包或者用`...`下载整个子目录里面的每个包。Go语言工具箱的go命令同时计算并下载所依赖的每个包,这也是前一个例子中golang.org/x/net/html自动出现在本地工作区目录的原因。 + +一旦`go get`命令下载了包,然后就是安装包或包对应的可执行的程序。我们将在下一节再关注它的细节,现在只是展示整个下载过程是如何的简单。第一个命令是获取golint工具,它用于检测Go源代码的编程风格是否有问题。第二个命令是用golint命令对2.6.2节的gopl.io/ch2/popcount包代码进行编码风格检查。它友好地报告了忘记了包的文档: + +``` +$ go get github.com/golang/lint/golint +$ $GOPATH/bin/golint gopl.io/ch2/popcount +src/gopl.io/ch2/popcount/main.go:1:1: + package comment should be of the form "Package popcount ..." +``` + +`go get`命令支持当前流行的托管网站GitHub、Bitbucket和Launchpad,可以直接向它们的版本控制系统请求代码。对于其它的网站,你可能需要指定版本控制系统的具体路径和协议,例如 Git或Mercurial。运行`go help importpath`获取相关的信息。 + +`go get`命令获取的代码是真实的本地存储仓库,而不仅仅只是复制源文件,因此你依然可以使用版本管理工具比较本地代码的变更或者切换到其它的版本。例如golang.org/x/net包目录对应一个Git仓库: + +``` +$ cd $GOPATH/src/golang.org/x/net +$ git remote -v +origin https://go.googlesource.com/net (fetch) +origin https://go.googlesource.com/net (push) +``` + +需要注意的是导入路径含有的网站域名和本地Git仓库对应远程服务地址并不相同,真实的Git地址是go.googlesource.com。这其实是Go语言工具的一个特性,可以让包用一个自定义的导入路径,但是真实的代码却是由更通用的服务提供,例如googlesource.com或github.com。因为页面 https://golang.org/x/net/html 包含了如下的元数据,它告诉Go语言的工具当前包真实的Git仓库托管地址: + +``` +$ go build gopl.io/ch1/fetch +$ ./fetch https://golang.org/x/net/html | grep go-import + +``` + +如果指定`-u`命令行标志参数,`go get`命令将确保所有的包和依赖的包的版本都是最新的,然后重新编译和安装它们。如果不包含该标志参数的话,而且如果包已经在本地存在,那么代码将不会被自动更新。 + +`go get -u`命令只是简单地保证每个包是最新版本,如果是第一次下载包则是比较方便的;但是对于发布程序则可能是不合适的,因为本地程序可能需要对依赖的包做精确的版本依赖管理。通常的解决方案是使用vendor的目录用于存储依赖包的固定版本的源代码,对本地依赖的包的版本更新也是谨慎和持续可控的。在Go1.5之前,一般需要修改包的导入路径,所以复制后golang.org/x/net/html导入路径可能会变为gopl.io/vendor/golang.org/x/net/html。最新的Go语言命令已经支持vendor特性,但限于篇幅这里并不讨论vendor的具体细节。不过可以通过`go help gopath`命令查看Vendor的帮助文档。 + +(译注:墙内用户在上面这些命令的基础上,还需要学习用翻墙来go get。) + +**练习 10.3:** 从 http://gopl.io/ch1/helloworld?go-get=1 获取内容,查看本书的代码的真实托管的网址(`go get`请求HTML页面时包含了`go-get`参数,以区别普通的浏览器请求)。 + +### 10.7.3. 构建包 + +`go build`命令编译命令行参数指定的每个包。如果包是一个库,则忽略输出结果;这可以用于检测包是可以正确编译的。如果包的名字是main,`go build`将调用链接器在当前目录创建一个可执行程序;以导入路径的最后一段作为可执行程序的名字。 + +由于每个目录只包含一个包,因此每个对应可执行程序或者叫Unix术语中的命令的包,会要求放到一个独立的目录中。这些目录有时候会放在名叫cmd目录的子目录下面,例如用于提供Go文档服务的golang.org/x/tools/cmd/godoc命令就是放在cmd子目录(§10.7.4)。 + +每个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径名指定,相对路径必须以`.`或`..`开头。如果没有指定参数,那么默认指定为当前目录对应的包。下面的命令用于构建同一个包,虽然它们的写法各不相同: + +``` +$ cd $GOPATH/src/gopl.io/ch1/helloworld +$ go build +``` + +或者: + +``` +$ cd anywhere +$ go build gopl.io/ch1/helloworld +``` + +或者: + +``` +$ cd $GOPATH +$ go build ./src/gopl.io/ch1/helloworld +``` + +但不能这样: + +``` +$ cd $GOPATH +$ go build src/gopl.io/ch1/helloworld +Error: cannot find package "src/gopl.io/ch1/helloworld". +``` + +也可以指定包的源文件列表,这一般只用于构建一些小程序或做一些临时性的实验。如果是main包,将会以第一个Go源文件的基础文件名作为最终的可执行程序的名字。 + +``` +$ cat quoteargs.go +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Printf("%q\n", os.Args[1:]) +} +$ go build quoteargs.go +$ ./quoteargs one "two three" four\ five +["one" "two three" "four five"] +``` + +特别是对于这类一次性运行的程序,我们希望尽快的构建并运行它。`go run`命令实际上是结合了构建和运行的两个步骤: + +``` +$ go run quoteargs.go one "two three" four\ five +["one" "two three" "four five"] +``` +(译注:其实也可以偷懒,直接go run `*.go`) + +第一行的参数列表中,第一个不是以`.go`结尾的将作为可执行程序的参数运行。 + +默认情况下,`go build`命令构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。依赖分析和编译过程虽然都是很快的,但是随着项目增加到几十个包和成千上万行代码,依赖关系分析和编译时间的消耗将变的可观,有时候可能需要几秒种,即使这些依赖项没有改变。 + +`go install`命令和`go build`命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。被编译的包会被保存到$GOPATH/pkg目录下,目录路径和 src目录路径对应,可执行程序被保存到$GOPATH/bin目录。(很多用户会将$GOPATH/bin添加到可执行程序的搜索列表中。)还有,`go install`命令和`go build`命令都不会重新编译没有发生变化的包,这可以使后续构建更快捷。为了方便编译依赖的包,`go build -i`命令将安装每个目标所依赖的包。 + +因为编译对应不同的操作系统平台和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类型: + +gopl.io/ch10/cross +```Go +func main() { + fmt.Println(runtime.GOOS, runtime.GOARCH) +} +``` + +下面以64位和32位环境分别编译和执行: + +``` +$ go build gopl.io/ch10/cross +$ ./cross +darwin amd64 +$ GOARCH=386 go build gopl.io/ch10/cross +$ ./cross +darwin 386 +``` + +有些包可能需要针对不同平台和处理器类型使用不同版本的代码文件,以便于处理底层的可移植性问题或为一些特定代码提供优化。如果一个文件名包含了一个操作系统或处理器类型名字,例如net_linux.go或asm_amd64.s,Go语言的构建工具将只在对应的平台编译这些文件。还有一个特别的构建注释参数可以提供更多的构建过程控制。例如,文件中可能包含下面的注释: + +```Go +// +build linux darwin +``` + +在包声明和包注释的前面,该构建注释参数告诉`go build`只在编译程序对应的目标操作系统是Linux或Mac OS X时才编译这个文件。下面的构建注释则表示不编译这个文件: + +```Go +// +build ignore +``` + +更多细节,可以参考go/build包的构建约束部分的文档。 + +``` +$ go doc go/build +``` + +### 10.7.4. 包文档 + +Go语言的编码风格鼓励为每个包提供良好的文档。包中每个导出的成员和包声明前都应该包含目的和用法说明的注释。 + +Go语言中的文档注释一般是完整的句子,第一行通常是摘要说明,以被注释者的名字开头。注释中函数的参数或其它的标识符并不需要额外的引号或其它标记注明。例如,下面是fmt.Fprintf的文档注释。 + +```Go +// Fprintf formats according to a format specifier and writes to w. +// It returns the number of bytes written and any write error encountered. +func Fprintf(w io.Writer, format string, a ...interface{}) (int, error) +``` + +Fprintf函数格式化的细节在fmt包文档中描述。如果注释后紧跟着包声明语句,那注释对应整个包的文档。包文档对应的注释只能有一个(译注:其实可以有多个,它们会组合成一个包文档注释),包注释可以出现在任何一个源文件中。如果包的注释内容比较长,一般会放到一个独立的源文件中;fmt包注释就有300行之多。这个专门用于保存包文档的源文件通常叫doc.go。 + +好的文档并不需要面面俱到,文档本身应该是简洁但不可忽略的。事实上,Go语言的风格更喜欢简洁的文档,并且文档也是需要像代码一样维护的。对于一组声明语句,可以用一个精炼的句子描述,如果是显而易见的功能则并不需要注释。 + +在本书中,只要空间允许,我们之前很多包声明都包含了注释文档,但你可以从标准库中发现很多更好的例子。有两个工具可以帮到你。 + +首先是`go doc`命令,该命令打印其后所指定的实体的声明与文档注释,该实体可能是一个包: + +``` +$ go doc time +package time // import "time" + +Package time provides functionality for measuring and displaying time. + +const Nanosecond Duration = 1 ... +func After(d Duration) <-chan Time +func Sleep(d Duration) +func Since(t Time) Duration +func Now() Time +type Duration int64 +type Time struct { ... } +...many more... +``` + +或者是某个具体的包成员: + +``` +$ go doc time.Since +func Since(t Time) Duration + + Since returns the time elapsed since t. + It is shorthand for time.Now().Sub(t). +``` + +或者是一个方法: + +``` +$ go doc time.Duration.Seconds +func (d Duration) Seconds() float64 + + Seconds returns the duration as a floating-point number of seconds. +``` + +该命令并不需要输入完整的包导入路径或正确的大小写。下面的命令将打印encoding/json包的`(*json.Decoder).Decode`方法的文档: + +``` +$ go doc json.decode +func (dec *Decoder) Decode(v interface{}) error + + Decode reads the next JSON-encoded value from its input and stores + it in the value pointed to by v. +``` + +第二个工具,名字也叫godoc,它提供可以相互交叉引用的HTML页面,但是包含和`go doc`命令相同以及更多的信息。图10.1演示了time包的文档,11.6节将看到godoc演示可以交互的示例程序。godoc的在线服务 https://godoc.org ,包含了成千上万的开源包的检索工具。 + +![](../images/ch10-01.png) + +你也可以在自己的工作区目录运行godoc服务。运行下面的命令,然后在浏览器查看 http://localhost:8000/pkg 页面: + +``` +$ godoc -http :8000 +``` + +其中`-analysis=type`和`-analysis=pointer`命令行标志参数用于打开文档和代码中关于静态分析的结果。 + +### 10.7.5. 内部包 + +在Go语言程序中,包是最重要的封装机制。没有导出的标识符只在同一个包内部可以访问,而导出的标识符则是面向全宇宙都是可见的。 + +有时候,一个中间的状态可能也是有用的,标识符对于一小部分信任的包是可见的,但并不是对所有调用者都可见。例如,当我们计划将一个大的包拆分为很多小的更容易维护的子包,但是我们并不想将内部的子包结构也完全暴露出去。同时,我们可能还希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。 + +为了满足这些需求,Go语言的构建工具对包含internal名字的路径段的包导入路径做了特殊处理。这种包叫internal包,一个internal包只能被和internal目录有同一个父目录的包所导入。例如,net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入,但是不能被net/url包导入。不过net/url包却可以导入net/http/httputil包。 + +``` +net/http +net/http/internal/chunked +net/http/httputil +net/url +``` + +### 10.7.6. 查询包 + +`go list`命令可以查询可用包的信息。其最简单的形式,可以测试包是否在工作区并打印它的导入路径: + +``` +$ go list github.com/go-sql-driver/mysql +github.com/go-sql-driver/mysql +``` + +`go list`命令的参数还可以用`"..."`表示匹配任意的包的导入路径。我们可以用它来列出工作区中的所有包: + +``` +$ go list ... +archive/tar +archive/zip +bufio +bytes +cmd/addr2line +cmd/api +...many more... +``` + +或者是特定子目录下的所有包: + +``` +$ go list gopl.io/ch3/... +gopl.io/ch3/basename1 +gopl.io/ch3/basename2 +gopl.io/ch3/comma +gopl.io/ch3/mandelbrot +gopl.io/ch3/netflag +gopl.io/ch3/printints +gopl.io/ch3/surface +``` + +或者是和某个主题相关的所有包: + +``` +$ go list ...xml... +encoding/xml +gopl.io/ch7/xmlselect +``` + +`go list`命令还可以获取每个包完整的元信息,而不仅仅只是导入路径,这些元信息可以以不同格式提供给用户。其中`-json`命令行参数表示用JSON格式打印每个包的元信息。 + +``` +$ go list -json hash +{ + "Dir": "/home/gopher/go/src/hash", + "ImportPath": "hash", + "Name": "hash", + "Doc": "Package hash provides interfaces for hash functions.", + "Target": "/home/gopher/go/pkg/darwin_amd64/hash.a", + "Goroot": true, + "Standard": true, + "Root": "/home/gopher/go", + "GoFiles": [ + "hash.go" + ], + "Imports": [ + "io" + ], + "Deps": [ + "errors", + "io", + "runtime", + "sync", + "sync/atomic", + "unsafe" + ] +} +``` + +命令行参数`-f`则允许用户使用text/template包(§4.6)的模板语言定义输出文本的格式。下面的命令将打印strconv包的依赖的包,然后用join模板函数将结果链接为一行,连接时每个结果之间用一个空格分隔: + + +``` +$ go list -f '{{join .Deps " "}}' strconv +errors math runtime unicode/utf8 unsafe +``` +{% endraw %} + +译注:上面的命令在Windows的命令行运行会遇到`template: main:1: unclosed action`的错误。产生这个错误的原因是因为命令行对命令中的`" "`参数进行了转义处理。可以按照下面的方法解决转义字符串的问题: + + +``` +$ go list -f "{{join .Deps \" \"}}" strconv +``` +{% endraw %} + +下面的命令打印compress子目录下所有包的导入包列表: + + +``` +$ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/... +compress/bzip2 -> bufio io sort +compress/flate -> bufio fmt io math sort strconv +compress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time +compress/lzw -> bufio errors fmt io +compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io +``` +{% endraw %} + +译注:Windows下有同样有问题,要避免转义字符串的干扰: + + +``` +$ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/... +``` +{% endraw %} + +`go list`命令对于一次性的交互式查询或自动化构建或测试脚本都很有帮助。我们将在11.2.4节中再次使用它。每个子命令的更多信息,包括可设置的字段和意义,可以用`go help list`命令查看。 + +在本章,我们解释了Go语言工具中除了测试命令之外的所有重要的子命令。在下一章,我们将看到如何用`go test`命令去运行Go语言程序中的测试代码。 + +**练习 10.4:** 创建一个工具,根据命令行指定的参数,报告工作区所有依赖包指定的其它包集合。提示:你需要运行`go list`命令两次,一次用于初始化包,一次用于所有包。你可能需要用encoding/json(§4.5)包来分析输出的JSON格式的信息。 diff --git a/ch10/ch10.md b/ch10/ch10.md index 0a4a876..6f680fa 100644 --- a/ch10/ch10.md +++ b/ch10/ch10.md @@ -1,4 +1,4 @@ -# 第十章 包和工具 +# 第10章 包和工具 现在随便一个小程序的实现都可能包含超过10000个函数。然而作者一般只需要考虑其中很小的一部分和做很少的设计,因为绝大部分代码都是由他人编写的,它们通过类似包或模块的方式被重用。 diff --git a/ch11/ch11-02-4.md b/ch11/ch11-02-4.md index 5e71d7a..62dbcfd 100644 --- a/ch11/ch11-02-4.md +++ b/ch11/ch11-02-4.md @@ -16,7 +16,7 @@ 我们可以用go list命令查看包对应目录中哪些Go源文件是产品代码,哪些是包内测试,还有哪些是外部测试包。我们以fmt包作为一个例子:GoFiles表示产品代码对应的Go源文件列表;也就是go build命令要编译的部分。 -{% raw %} + ``` $ go list -f={{.GoFiles}} fmt @@ -27,7 +27,7 @@ $ go list -f={{.GoFiles}} fmt TestGoFiles表示的是fmt包内部测试代码,以_test.go为后缀文件名,不过只在测试时被构建: -{% raw %} + ``` $ go list -f={{.TestGoFiles}} fmt @@ -40,7 +40,7 @@ $ go list -f={{.TestGoFiles}} fmt XTestGoFiles表示的是属于外部测试包的测试代码,也就是fmt_test包,因此它们必须先导入fmt包。同样,这些文件也只是在测试时被构建运行: -{% raw %} + ``` $ go list -f={{.XTestGoFiles}} fmt diff --git a/ch11/ch11-02.md b/ch11/ch11-02.md index e646f71..e40e263 100644 --- a/ch11/ch11-02.md +++ b/ch11/ch11-02.md @@ -216,14 +216,409 @@ ok gopl.io/ch11/word2 0.015s **练习 11.2:** 为(§6.5)的IntSet编写一组测试,用于检查每个操作后的行为和基于内置map的集合等价,后面练习11.7将会用到。 -{% include "./ch11-02-1.md" %} +### 11.2.1. 随机测试 -{% include "./ch11-02-2.md" %} +表格驱动的测试便于构造基于精心挑选的测试数据的测试用例。另一种测试思路是随机测试,也就是通过构造更广泛的随机输入来测试探索函数的行为。 -{% include "./ch11-02-3.md" %} +那么对于一个随机的输入,我们如何能知道希望的输出结果呢?这里有两种处理策略。第一个是编写另一个对照函数,使用简单和清晰的算法,虽然效率较低但是行为和要测试的函数是一致的,然后针对相同的随机输入检查两者的输出结果。第二种是生成的随机输入的数据遵循特定的模式,这样我们就可以知道期望的输出的模式。 -{% include "./ch11-02-4.md" %} +下面的例子使用的是第二种方法:randomPalindrome函数用于随机生成回文字符串。 -{% include "./ch11-02-5.md" %} +```Go +import "math/rand" + +// randomPalindrome returns a palindrome whose length and contents +// are derived from the pseudo-random number generator rng. +func randomPalindrome(rng *rand.Rand) string { + n := rng.Intn(25) // random length up to 24 + runes := make([]rune, n) + for i := 0; i < (n+1)/2; i++ { + r := rune(rng.Intn(0x1000)) // random rune up to '\u0999' + runes[i] = r + runes[n-1-i] = r + } + return string(runes) +} + +func TestRandomPalindromes(t *testing.T) { + // Initialize a pseudo-random number generator. + seed := time.Now().UTC().UnixNano() + t.Logf("Random seed: %d", seed) + rng := rand.New(rand.NewSource(seed)) + + for i := 0; i < 1000; i++ { + p := randomPalindrome(rng) + if !IsPalindrome(p) { + t.Errorf("IsPalindrome(%q) = false", p) + } + } +} +``` + +虽然随机测试会有不确定因素,但是它也是至关重要的,我们可以从失败测试的日志获取足够的信息。在我们的例子中,输入IsPalindrome的p参数将告诉我们真实的数据,但是对于函数将接受更复杂的输入,不需要保存所有的输入,只要日志中简单地记录随机数种子即可(像上面的方式)。有了这些随机数初始化种子,我们可以很容易修改测试代码以重现失败的随机测试。 + +通过使用当前时间作为随机种子,在整个过程中的每次运行测试命令时都将探索新的随机数据。如果你使用的是定期运行的自动化测试集成系统,随机测试将特别有价值。 + +**练习 11.3:** TestRandomPalindromes测试函数只测试了回文字符串。编写新的随机测试生成器,用于测试随机生成的非回文字符串。 + +**练习 11.4:** 修改randomPalindrome函数,以探索IsPalindrome是否对标点和空格做了正确处理。 + +译者注:**拓展阅读**感兴趣的读者可以再了解一下go-fuzz + +### 11.2.2. 测试一个命令 + +对于测试包`go test`是一个有用的工具,但是稍加努力我们也可以用它来测试可执行程序。如果一个包的名字是 main,那么在构建时会生成一个可执行程序,不过main包可以作为一个包被测试器代码导入。 + +让我们为2.3.2节的echo程序编写一个测试。我们先将程序拆分为两个函数:echo函数完成真正的工作,main函数用于处理命令行输入参数和echo可能返回的错误。 + +gopl.io/ch11/echo +```Go +// Echo prints its command-line arguments. +package main + +import ( + "flag" + "fmt" + "io" + "os" + "strings" +) + +var ( + n = flag.Bool("n", false, "omit trailing newline") + s = flag.String("s", " ", "separator") +) + +var out io.Writer = os.Stdout // modified during testing + +func main() { + flag.Parse() + if err := echo(!*n, *s, flag.Args()); err != nil { + fmt.Fprintf(os.Stderr, "echo: %v\n", err) + os.Exit(1) + } +} + +func echo(newline bool, sep string, args []string) error { + fmt.Fprint(out, strings.Join(args, sep)) + if newline { + fmt.Fprintln(out) + } + return nil +} +``` + +在测试中我们可以用各种参数和标志调用echo函数,然后检测它的输出是否正确,我们通过增加参数来减少echo函数对全局变量的依赖。我们还增加了一个全局名为out的变量来替代直接使用os.Stdout,这样测试代码可以根据需要将out修改为不同的对象以便于检查。下面就是echo_test.go文件中的测试代码: + +```Go +package main + +import ( + "bytes" + "fmt" + "testing" +) + +func TestEcho(t *testing.T) { + var tests = []struct { + newline bool + sep string + args []string + want string + }{ + {true, "", []string{}, "\n"}, + {false, "", []string{}, ""}, + {true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"}, + {true, ",", []string{"a", "b", "c"}, "a,b,c\n"}, + {false, ":", []string{"1", "2", "3"}, "1:2:3"}, + } + for _, test := range tests { + descr := fmt.Sprintf("echo(%v, %q, %q)", + test.newline, test.sep, test.args) + + out = new(bytes.Buffer) // captured output + if err := echo(test.newline, test.sep, test.args); err != nil { + t.Errorf("%s failed: %v", descr, err) + continue + } + got := out.(*bytes.Buffer).String() + if got != test.want { + t.Errorf("%s = %q, want %q", descr, got, test.want) + } + } +} +``` + +要注意的是测试代码和产品代码在同一个包。虽然是main包,也有对应的main入口函数,但是在测试的时候main包只是TestEcho测试函数导入的一个普通包,里面main函数并没有被导出,而是被忽略的。 + +通过将测试放到表格中,我们很容易添加新的测试用例。让我通过增加下面的测试用例来看看失败的情况是怎么样的: + +```Go +{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation! +``` + +`go test`输出如下: + +``` +$ go test gopl.io/ch11/echo +--- FAIL: TestEcho (0.00s) + echo_test.go:31: echo(true, ",", ["a" "b" "c"]) = "a,b,c", want "a b c\n" +FAIL +FAIL gopl.io/ch11/echo 0.006s +``` + +错误信息描述了尝试的操作(使用Go类似语法),实际的结果和期望的结果。通过这样的错误信息,你可以在检视代码之前就很容易定位错误的原因。 + +要注意的是在测试代码中并没有调用log.Fatal或os.Exit,因为调用这类函数会导致程序提前退出;调用这些函数的特权应该放在main函数中。如果真的有意外的事情导致函数发生panic异常,测试驱动应该尝试用recover捕获异常,然后将当前测试当作失败处理。如果是可预期的错误,例如非法的用户输入、找不到文件或配置文件不当等应该通过返回一个非空的error的方式处理。幸运的是(上面的意外只是一个插曲),我们的echo示例是比较简单的也没有需要返回非空error的情况。 + +### 11.2.3. 白盒测试 + +一种测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理。黑盒测试只需要测试包公开的文档和API行为,内部实现对测试代码是透明的。相反,白盒测试有访问包内部函数和数据结构的权限,因此可以做到一些普通客户端无法实现的测试。例如,一个白盒测试可以在每个操作之后检测不变量的数据类型。(白盒测试只是一个传统的名称,其实称为clear box测试会更准确。) + +黑盒和白盒这两种测试方法是互补的。黑盒测试一般更健壮,随着软件实现的完善测试代码很少需要更新。它们可以帮助测试者了解真实客户的需求,也可以帮助发现API设计的一些不足之处。相反,白盒测试则可以对内部一些棘手的实现提供更多的测试覆盖。 + +我们已经看到两种测试的例子。TestIsPalindrome测试仅仅使用导出的IsPalindrome函数,因此这是一个黑盒测试。TestEcho测试则调用了内部的echo函数,并且更新了内部的out包级变量,这两个都是未导出的,因此这是白盒测试。 + +当我们准备TestEcho测试的时候,我们修改了echo函数使用包级的out变量作为输出对象,因此测试代码可以用另一个实现代替标准输出,这样可以方便对比echo输出的数据。使用类似的技术,我们可以将产品代码的其他部分也替换为一个容易测试的伪对象。使用伪对象的好处是我们可以方便配置,容易预测,更可靠,也更容易观察。同时也可以避免一些不良的副作用,例如更新生产数据库或信用卡消费行为。 + +下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。当用户使用了超过90%的存储配额之后将发送提醒邮件。(译注:一般在实现业务机器监控,包括磁盘、cpu、网络等的时候,需要类似的到达阈值=>触发报警的逻辑,所以是很实用的案例。) + +gopl.io/ch11/storage1 +```Go +package storage + +import ( + "fmt" + "log" + "net/smtp" +) + +func bytesInUse(username string) int64 { return 0 /* ... */ } + +// Email sender configuration. +// NOTE: never put passwords in source code! +const sender = "notifications@example.com" +const password = "correcthorsebatterystaple" +const hostname = "smtp.example.com" + +const template = `Warning: you are using %d bytes of storage, +%d%% of your quota.` + +func CheckQuota(username string) { + used := bytesInUse(username) + const quota = 1000000000 // 1GB + percent := 100 * used / quota + if percent < 90 { + return // OK + } + msg := fmt.Sprintf(template, used, percent) + auth := smtp.PlainAuth("", sender, password, hostname) + err := smtp.SendMail(hostname+":587", auth, sender, + []string{username}, []byte(msg)) + if err != nil { + log.Printf("smtp.SendMail(%s) failed: %s", username, err) + } +} +``` + +我们想测试这段代码,但是我们并不希望发送真实的邮件。因此我们将邮件处理逻辑放到一个私有的notifyUser函数中。 + +gopl.io/ch11/storage2 +```Go +var notifyUser = func(username, msg string) { + auth := smtp.PlainAuth("", sender, password, hostname) + err := smtp.SendMail(hostname+":587", auth, sender, + []string{username}, []byte(msg)) + if err != nil { + log.Printf("smtp.SendEmail(%s) failed: %s", username, err) + } +} + +func CheckQuota(username string) { + used := bytesInUse(username) + const quota = 1000000000 // 1GB + percent := 100 * used / quota + if percent < 90 { + return // OK + } + msg := fmt.Sprintf(template, used, percent) + notifyUser(username, msg) +} +``` + +现在我们可以在测试中用伪邮件发送函数替代真实的邮件发送函数。它只是简单记录要通知的用户和邮件的内容。 + +```Go +package storage + +import ( + "strings" + "testing" +) +func TestCheckQuotaNotifiesUser(t *testing.T) { + var notifiedUser, notifiedMsg string + notifyUser = func(user, msg string) { + notifiedUser, notifiedMsg = user, msg + } + + // ...simulate a 980MB-used condition... + + const user = "joe@example.org" + CheckQuota(user) + if notifiedUser == "" && notifiedMsg == "" { + t.Fatalf("notifyUser not called") + } + if notifiedUser != user { + t.Errorf("wrong user (%s) notified, want %s", + notifiedUser, user) + } + const wantSubstring = "98% of your quota" + if !strings.Contains(notifiedMsg, wantSubstring) { + t.Errorf("unexpected notification message <<%s>>, "+ + "want substring %q", notifiedMsg, wantSubstring) + } +} +``` + +这里有一个问题:当测试函数返回后,CheckQuota将不能正常工作,因为notifyUsers依然使用的是测试函数的伪发送邮件函数(当更新全局对象的时候总会有这种风险)。 我们必须修改测试代码恢复notifyUsers原先的状态以便后续其他的测试没有影响,要确保所有的执行路径后都能恢复,包括测试失败或panic异常的情形。在这种情况下,我们建议使用defer语句来延后执行处理恢复的代码。 + +```Go +func TestCheckQuotaNotifiesUser(t *testing.T) { + // Save and restore original notifyUser. + saved := notifyUser + defer func() { notifyUser = saved }() + + // Install the test's fake notifyUser. + var notifiedUser, notifiedMsg string + notifyUser = func(user, msg string) { + notifiedUser, notifiedMsg = user, msg + } + // ...rest of test... +} +``` + +这种处理模式可以用来暂时保存和恢复所有的全局变量,包括命令行标志参数、调试选项和优化参数;安装和移除导致生产代码产生一些调试信息的钩子函数;还有有些诱导生产代码进入某些重要状态的改变,比如超时、错误,甚至是一些刻意制造的并发行为等因素。 + +以这种方式使用全局变量是安全的,因为go test命令并不会同时并发地执行多个测试。 + +### 11.2.4. 外部测试包 + +考虑下这两个包:net/url包,提供了URL解析的功能;net/http包,提供了web服务和HTTP客户端的功能。如我们所料,上层的net/http包依赖下层的net/url包。然后,net/url包中的一个测试是演示不同URL和HTTP客户端的交互行为。也就是说,一个下层包的测试代码导入了上层的包。 + +![](../images/ch11-01.png) + +这样的行为在net/url包的测试代码中会导致包的循环依赖,正如图11.1中向上箭头所示,同时正如我们在10.1节所讲的,Go语言规范是禁止包的循环依赖的。 + +不过我们可以通过外部测试包的方式解决循环依赖的问题,也就是在net/url包所在的目录声明一个独立的url_test测试包。其中包名的`_test`后缀告诉go test工具它应该建立一个额外的包来运行测试。我们将这个外部测试包的导入路径视作是net/url_test会更容易理解,但实际上它并不能被其他任何包导入。 + +因为外部测试包是一个独立的包,所以能够导入那些`依赖待测代码本身`的其他辅助包;包内的测试代码就无法做到这点。在设计层面,外部测试包是在所有它依赖的包的上层,正如图11.2所示。 + +![](../images/ch11-02.png) + +通过避免循环的导入依赖,外部测试包可以更灵活地编写测试,特别是集成测试(需要测试多个组件之间的交互),可以像普通应用程序那样自由地导入其他包。 + +我们可以用go list命令查看包对应目录中哪些Go源文件是产品代码,哪些是包内测试,还有哪些是外部测试包。我们以fmt包作为一个例子:GoFiles表示产品代码对应的Go源文件列表;也就是go build命令要编译的部分。 + + + +``` +$ go list -f={{.GoFiles}} fmt +[doc.go format.go print.go scan.go] +``` + +{% endraw %} + +TestGoFiles表示的是fmt包内部测试代码,以_test.go为后缀文件名,不过只在测试时被构建: + + + +``` +$ go list -f={{.TestGoFiles}} fmt +[export_test.go] +``` + +{% endraw %} + +包的测试代码通常都在这些文件中,不过fmt包并非如此;稍后我们再解释export_test.go文件的作用。 + +XTestGoFiles表示的是属于外部测试包的测试代码,也就是fmt_test包,因此它们必须先导入fmt包。同样,这些文件也只是在测试时被构建运行: + + + +``` +$ go list -f={{.XTestGoFiles}} fmt +[fmt_test.go scan_test.go stringer_test.go] +``` + +{% endraw %} + +有时候外部测试包也需要访问被测试包内部的代码,例如在一个为了避免循环导入而被独立到外部测试包的白盒测试。在这种情况下,我们可以通过一些技巧解决:我们在包内的一个_test.go文件中导出一个内部的实现给外部测试包。因为这些代码只有在测试时才需要,因此一般会放在export_test.go文件中。 + +例如,fmt包的fmt.Scanf函数需要unicode.IsSpace函数提供的功能。但是为了避免太多的依赖,fmt包并没有导入包含巨大表格数据的unicode包;相反fmt包有一个叫isSpace内部的简易实现。 + +为了确保fmt.isSpace和unicode.IsSpace函数的行为保持一致,fmt包谨慎地包含了一个测试。一个在外部测试包内的白盒测试,是无法直接访问到isSpace内部函数的,因此fmt通过一个后门导出了isSpace函数。export_test.go文件就是专门用于外部测试包的后门。 + +```Go +package fmt + +var IsSpace = isSpace +``` + +这个测试文件并没有定义测试代码;它只是通过fmt.IsSpace简单导出了内部的isSpace函数,提供给外部测试包使用。这个技巧可以广泛用于位于外部测试包的白盒测试。 + + +### 11.2.5. 编写有效的测试 + +许多Go语言新人会惊异于Go语言极简的测试框架。很多其它语言的测试框架都提供了识别测试函数的机制(通常使用反射或元数据),通过设置一些“setup”和“teardown”的钩子函数来执行测试用例运行的初始化和之后的清理操作,同时测试工具箱还提供了很多类似assert断言、值比较函数、格式化输出错误信息和停止一个失败的测试等辅助函数(通常使用异常机制)。虽然这些机制可以使得测试非常简洁,但是测试输出的日志却会像火星文一般难以理解。此外,虽然测试最终也会输出PASS或FAIL的报告,但是它们提供的信息格式却非常不利于代码维护者快速定位问题,因为失败信息的具体含义非常隐晦,比如“assert: 0 == 1”或成页的海量跟踪日志。 + +Go语言的测试风格则形成鲜明对比。它期望测试者自己完成大部分的工作,定义函数避免重复,就像普通编程那样。编写测试并不是一个机械的填空过程;一个测试也有自己的接口,尽管它的维护者也是测试仅有的一个用户。一个好的测试不应该引发其他无关的错误信息,它只要清晰简洁地描述问题的症状即可,有时候可能还需要一些上下文信息。在理想情况下,维护者可以在不看代码的情况下就能根据错误信息定位错误产生的原因。一个好的测试不应该在遇到一点小错误时就立刻退出测试,它应该尝试报告更多的相关的错误信息,因为我们可能从多个失败测试的模式中发现错误产生的规律。 + +下面的断言函数比较两个值,然后生成一个通用的错误信息,并停止程序。它很好用也确实有效,但是当测试失败的时候,打印的错误信息却几乎是没有价值的。它并没有为快速解决问题提供一个很好的入口。 + +```Go +import ( + "fmt" + "strings" + "testing" +) +// A poor assertion function. +func assertEqual(x, y int) { + if x != y { + panic(fmt.Sprintf("%d != %d", x, y)) + } +} +func TestSplit(t *testing.T) { + words := strings.Split("a:b:c", ":") + assertEqual(len(words), 3) + // ... +} +``` + +从这个意义上说,断言函数犯了过早抽象的错误:仅仅测试两个整数是否相同,而没能根据上下文提供更有意义的错误信息。我们可以根据具体的错误打印一个更有价值的错误信息,就像下面例子那样。只有在测试中出现重复模式时才采用抽象。 + +```Go +func TestSplit(t *testing.T) { + s, sep := "a:b:c", ":" + words := strings.Split(s, sep) + if got, want := len(words), 3; got != want { + t.Errorf("Split(%q, %q) returned %d words, want %d", + s, sep, got, want) + } + // ... +} +``` + +现在的测试不仅报告了调用的具体函数、它的输入和结果的意义;并且打印的真实返回的值和期望返回的值;并且即使断言失败依然会继续尝试运行更多的测试。一旦我们写了这样结构的测试,下一步自然不是用更多的if语句来扩展测试用例,我们可以用像IsPalindrome的表驱动测试那样来准备更多的s和sep测试用例。 + +前面的例子并不需要额外的辅助函数,如果有可以使测试代码更简单的方法我们也乐意接受。(我们将在13.3节看到一个类似reflect.DeepEqual辅助函数。)一个好的测试的关键是首先实现你期望的具体行为,然后才是考虑简化测试代码、避免重复。如果直接从抽象、通用的测试库着手,很难取得良好结果。 + +**练习11.5:** 用表格驱动的技术扩展TestSplit测试,并打印期望的输出结果。 + +### 11.2.6. 避免脆弱的测试 + +如果一个应用程序对于新出现的但有效的输入经常失败说明程序容易出bug(不够稳健);同样,如果一个测试仅仅对程序做了微小变化就失败则称为脆弱。就像一个不够稳健的程序会挫败它的用户一样,一个脆弱的测试同样会激怒它的维护者。最脆弱的测试代码会在程序没有任何变化的时候产生不同的结果,时好时坏,处理它们会耗费大量的时间但是并不会得到任何好处。 + +当一个测试函数会产生一个复杂的输出如一个很长的字符串、一个精心设计的数据结构或一个文件时,人们很容易想预先写下一系列固定的用于对比的标杆数据。但是随着项目的发展,有些输出可能会发生变化,尽管很可能是一个改进的实现导致的。而且不仅仅是输出部分,函数复杂的输入部分可能也跟着变化了,因此测试使用的输入也就不再有效了。 + +避免脆弱测试代码的方法是只检测你真正关心的属性。保持测试代码的简洁和内部结构的稳定。特别是对断言部分要有所选择。不要对字符串进行全字匹配,而是针对那些在项目的发展中是比较稳定不变的子串。很多时候值得花力气来编写一个从复杂输出中提取用于断言的必要信息的函数,虽然这可能会带来很多前期的工作,但是它可以帮助迅速及时修复因为项目演化而导致的不合逻辑的失败测试。 -{% include "./ch11-02-6.md" %} diff --git a/ch11/ch11.md b/ch11/ch11.md index 76b56f7..812945d 100644 --- a/ch11/ch11.md +++ b/ch11/ch11.md @@ -1,4 +1,4 @@ -# 第十一章 测试 +# 第11章 测试 Maurice Wilkes,第一个存储程序计算机EDSAC的设计者,1949年他在实验室爬楼梯时有一个顿悟。在《计算机先驱回忆录》(Memoirs of a Computer Pioneer)里,他回忆到:“忽然间有一种醍醐灌顶的感觉,我整个后半生的美好时光都将在寻找程序BUG中度过了”。肯定从那之后的大部分正常的码农都会同情Wilkes过分悲观的想法,虽然也许会有人困惑于他对软件开发的难度的天真看法。 diff --git a/ch12/ch12.md b/ch12/ch12.md index 510e82f..cc09f31 100644 --- a/ch12/ch12.md +++ b/ch12/ch12.md @@ -1,4 +1,4 @@ -# 第十二章 反射 +# 第12章 反射 Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。 diff --git a/ch13/ch13.md b/ch13/ch13.md index e1a2ce1..a835c64 100644 --- a/ch13/ch13.md +++ b/ch13/ch13.md @@ -1,4 +1,4 @@ -# 第十三章 底层编程 +# 第13章 底层编程 Go语言的设计包含了诸多安全策略,限制了可能导致程序运行出错的用法。编译时类型检查可以发现大多数类型不匹配的操作,例如两个字符串做减法的错误。字符串、map、slice和chan等所有的内置类型,都有严格的类型转换规则。 diff --git a/ch2/ch2-03.md b/ch2/ch2-03.md index 9488be5..86b279c 100644 --- a/ch2/ch2-03.md +++ b/ch2/ch2-03.md @@ -32,10 +32,275 @@ var b, f, s = true, 2.3, "four" // bool, float64, string var f, err = os.Open(name) // os.Open returns a file and an error ``` -{% include "./ch2-03-1.md" %} +### 2.3.1. 简短变量声明 -{% include "./ch2-03-2.md" %} +在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。下面是lissajous函数中的三个简短变量声明语句(§1.4): -{% include "./ch2-03-3.md" %} +```Go +anim := gif.GIF{LoopCount: nframes} +freq := rand.Float64() * 3.0 +t := 0.0 +``` -{% include "./ch2-03-4.md" %} +因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。 + +```Go +i := 100 // an int +var boiling float64 = 100 // a float64 +var names []string +var err error +var p Point +``` + +和var形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量: + +```Go +i, j := 0, 1 +``` + +但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。 + +请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值(§2.4.1),后者是将右边各个表达式的值赋值给左边对应位置的各个变量: + +```Go +i, j = j, i // 交换 i 和 j 的值 +``` + +和普通var形式的变量声明语句一样,简短变量声明语句也可以用函数的返回值来声明和初始化变量,像下面的os.Open函数调用将返回两个值: + +```Go +f, err := os.Open(name) +if err != nil { + return err +} +// ...use f... +f.Close() +``` + +这里有一个比较微妙的地方:简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了(§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。 + +在下面的代码中,第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量,然后对已经声明的err进行了赋值操作。 + +```Go +in, err := os.Open(infile) +// ... +out, err := os.Create(outfile) +``` + +简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过: + +```Go +f, err := os.Open(infile) +// ... +f, err := os.Create(outfile) // compile error: no new variables +``` + +解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。 + +简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。 + +### 2.3.2. 指针 + +一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫x的变量,但是还有很多变量始终以表达式方式引入,例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。 + +一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。 + +如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是`*int`,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时`*p`表达式对应p指针指向的变量的值。一般`*p`表达式读取指针指向的变量的值,这里为int类型的值,同时因为`*p`对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。 + +```Go +x := 1 +p := &x // p, of type *int, points to x +fmt.Println(*p) // "1" +*p = 2 // equivalent to x = 2 +fmt.Println(x) // "2" +``` + +对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。 + +变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受`&`取地址操作。 + +任何类型的指针的零值都是nil。如果p指向某个有效变量,那么`p != nil`测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。 + +```Go +var x, y int +fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false" +``` + +在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。 + +```Go +var p = f() + +func f() *int { + v := 1 + return &v +} +``` + +每次调用f函数都将返回不同的结果: + +```Go +fmt.Println(f() == f()) // "false" +``` + +因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,可用在一个表达式中(译注:这是对C语言中`++v`操作的模拟,这里只是为了说明指针的用法,incr函数模拟的做法并不推荐): + +```Go +func incr(p *int) int { + *p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!! + return *p +} + +v := 1 +incr(&v) // side effect: v is now 2 +fmt.Println(incr(&v)) // "3" (and v is 3) +``` + +每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,`*p`就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量,但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作)。不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结构体、数组和接口都会创建所引用变量的别名。 + +指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点,在早些的echo版本中,就包含了两个可选的命令行参数:`-n`用于忽略行尾的换行符,`-s sep`用于指定分隔字符(默认是空格)。下面这是第四个版本,对应包路径为gopl.io/ch2/echo4。 + +gopl.io/ch2/echo4 +```Go +// Echo4 prints its command-line arguments. +package main + +import ( + "flag" + "fmt" + "strings" +) + +var n = flag.Bool("n", false, "omit trailing newline") +var sep = flag.String("s", " ", "separator") + +func main() { + flag.Parse() + fmt.Print(strings.Join(flag.Args(), *sep)) + if !*n { + fmt.Println() + } +} +``` + +调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性:第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是false),最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数,或者输入`-h`或`-help`参数,那么将打印所有标志参数的名字、默认值和描述信息。类似的,调用flag.String函数将创建一个对应字符串类型的标志参数变量,同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的`sep`和`n`变量分别是指向对应命令行标志参数变量的指针,因此必须用`*sep`和`*n`形式的指针语法间接引用它们。 + +当程序运行时,必须在使用标志参数对应的变量之前先调用flag.Parse函数,用于更新每个标志参数对应变量的值(之前是默认值)。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,返回值对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用os.Exit(2)终止程序。 + +让我们运行一些echo测试用例: + +``` +$ go build gopl.io/ch2/echo4 +$ ./echo4 a bc def +a bc def +$ ./echo4 -s / a bc def +a/bc/def +$ ./echo4 -n a bc def +a bc def$ +$ ./echo4 -help +Usage of ./echo4: + -n omit trailing newline + -s string + separator (default " ") +``` + +### 2.3.3. new函数 + +另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为`*T`。 + +```Go +p := new(int) // p, *int 类型, 指向匿名的 int 变量 +fmt.Println(*p) // "0" +*p = 2 // 设置 int 匿名变量的值为 2 +fmt.Println(*p) // "2" +``` + +用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。 + +下面的两个newInt函数有着相同的行为: + +```Go +func newInt() *int { + return new(int) +} + +func newInt() *int { + var dummy int + return &dummy +} +``` + +每次调用new函数都是返回一个新的变量的地址,因此下面两个地址是不同的: + +```Go +p := new(int) +q := new(int) +fmt.Println(p == q) // "false" +``` + +当然也可能有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如`struct{}`和`[0]int`,有可能有相同的地址(依赖具体的语言实现)(译注:请谨慎使用大小为0的类型,因为如果类型的大小为0的话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看`runtime.SetFinalizer`函数相关文档)。 + +new函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活(§4.4.1)。 + +由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。例如下面的例子: + +```Go +func delta(old, new int) int { return new - old } +``` + +由于new被定义为int类型的变量名,因此在delta函数内部是无法使用内置的new函数的。 + +### 2.3.4. 变量的生命周期 + +变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。 + +例如,下面是从1.4节的Lissajous程序摘录的代码片段: + +```Go +for t := 0.0; t < cycles*2*math.Pi; t += res { + x := math.Sin(t) + y := math.Sin(t*freq + phase) + img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), + blackIndex) +} +``` + +译注:函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样: + +```Go +for t := 0.0; t < cycles*2*math.Pi; t += res { + x := math.Sin(t) + y := math.Sin(t*freq + phase) + img.SetColorIndex( + size+int(x*size+0.5), size+int(y*size+0.5), + blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性 + ) // 小括弧另起一行缩进,和大括弧的风格保存一致 +} +``` + +在每次循环的开始会创建临时变量t,然后在每次循环迭代中创建临时变量x和y。 + +那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。 + +因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。 + +编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。 + +```Go +var global *int + +func f() { + var x int + x = 1 + global = &x +} + +func g() { + y := new(int) + *y = 1 +} +``` + +f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量`*y`将是不可达的,也就是说可以马上被回收的。因此,`*y`并没有从函数g中逃逸,编译器可以选择在栈上分配`*y`的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。 + +Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。 diff --git a/ch2/ch2-04.md b/ch2/ch2-04.md index eb5c717..32d17d0 100644 --- a/ch2/ch2-04.md +++ b/ch2/ch2-04.md @@ -25,6 +25,101 @@ v++ // 等价方式 v = v + 1;v 变成 2 v-- // 等价方式 v = v - 1;v 变成 1 ``` -{% include "./ch2-04-1.md" %} +### 2.4.1. 元组赋值 -{% include "./ch2-04-2.md" %} +元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值: + +```Go +x, y = y, x + +a[i], a[j] = a[j], a[i] +``` + +或者是计算两个整数值的的最大公约数(GCD)(译注:GCD不是那个敏感字,而是greatest common divisor的缩写,欧几里德的GCD是最早的非平凡算法): + +```Go +func gcd(x, y int) int { + for y != 0 { + x, y = y, x%y + } + return x +} +``` + +或者是计算斐波纳契数列(Fibonacci)的第N个数: + +```Go +func fib(n int) int { + x, y := 0, 1 + for i := 0; i < n; i++ { + x, y = y, x+y + } + return x +} +``` + +元组赋值也可以使一系列琐碎赋值更加紧凑(译注: 特别是在for循环的初始化部分), + +```Go +i, j, k = 2, 3, 5 +``` + +但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。 + +有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。 + +```Go +f, err = os.Open("foo.txt") // function call returns two values +``` + +通常,这类函数会用额外的返回值来表达某种错误类型,例如os.Open是用额外的返回值返回一个error类型的错误,还有一些是用来返回布尔值,通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功: + +```Go +v, ok = m[key] // map lookup +v, ok = x.(T) // type assertion +v, ok = <-ch // channel receive +``` + +译注:map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于只产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发生运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。例如下面的例子: + +```Go +v = m[key] // map查找,失败时返回零值 +v = x.(T) // type断言,失败时panic异常 +v = <-ch // 管道接收,失败时返回零值(阻塞不算是失败) + +_, ok = m[key] // map返回2个值 +_, ok = mm[""], false // map返回1个值 +_ = mm[""] // map返回1个值 +``` + +和变量声明一样,我们可以用下划线空白标识符`_`来丢弃不需要的值。 + +```Go +_, err = io.Copy(dst, src) // 丢弃字节数 +_, ok = x.(T) // 只检测类型,忽略具体值 +``` + + +### 2.4.2. 可赋值性 + +赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句会隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量(§4.2)也会产生赋值行为。例如下面的语句: + +```Go +medals := []string{"gold", "silver", "bronze"} +``` + +隐式地对slice的每个元素进行赋值操作,类似这样写的行为: + +```Go +medals[0] = "gold" +medals[1] = "silver" +medals[2] = "bronze" +``` + +map和chan的元素,虽然不是普通的变量,但是也有类似的隐式赋值行为。 + +不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。 + +可赋值性的规则对于不同类型有着不同要求,对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型,它的规则是简单的:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。常量(§3.6)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。 + +对于两个值是否可以用`==`或`!=`进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释。 diff --git a/ch2/ch2-06.md b/ch2/ch2-06.md index 8fafa3d..08ceb39 100644 --- a/ch2/ch2-06.md +++ b/ch2/ch2-06.md @@ -62,6 +62,136 @@ fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F" **练习 2.1:** 向tempconv包添加类型、常量和函数用来处理Kelvin绝对温度的转换,Kelvin 绝对零度是−273.15°C,Kelvin绝对温度1K和摄氏度1°C的单位间隔是一样的。 -{% include "./ch2-06-1.md" %} +### 2.6.1. 导入包 -{% include "./ch2-06-2.md" %} +在Go语言程序中,每个包都有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用Go语言自带的go工具箱时(第十章),一个导入路径代表一个目录中的一个或多个Go源文件。 + +除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。 + +要使用gopl.io/ch2/tempconv包,需要先导入: + +gopl.io/ch2/cf +```Go +// Cf converts its numeric argument to Celsius and Fahrenheit. +package main + +import ( + "fmt" + "os" + "strconv" + + "gopl.io/ch2/tempconv" +) + +func main() { + for _, arg := range os.Args[1:] { + t, err := strconv.ParseFloat(arg, 64) + if err != nil { + fmt.Fprintf(os.Stderr, "cf: %v\n", err) + os.Exit(1) + } + f := tempconv.Fahrenheit(t) + c := tempconv.Celsius(t) + fmt.Printf("%s = %s, %s = %s\n", + f, tempconv.FToC(f), c, tempconv.CToF(c)) + } +} +``` + +导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下,导入的包绑定到tempconv名字(译注:指包声明语句指定的名字),但是我们也可以绑定到另一个名称,以避免名字冲突(§10.4)。 + +cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换: + +``` +$ go build gopl.io/ch2/cf +$ ./cf 32 +32°F = 0°C, 32°C = 89.6°F +$ ./cf 212 +212°F = 100°C, 212°C = 413.6°F +$ ./cf -40 +-40°F = -40°C, -40°C = -40°F +``` + +如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有效减少不必要的依赖,虽然在调试期间可能会让人讨厌,因为删除一个类似log.Print("got here!")的打印语句可能导致需要同时删除log包导入声明,否则,编译器将会发出一个错误。在这种情况下,我们需要将不必要的导入删除或注释掉。 + +不过有更好的解决方案,我们可以使用golang.org/x/tools/cmd/goimports导入工具,它可以根据需要自动添加或删除导入的包;许多编辑器都可以集成goimports工具,然后在保存文件的时候自动运行。类似的还有gofmt工具,可以用来格式化Go源文件。 + +**练习 2.2:** 写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等。 + +### 2.6.2. 包的初始化 + +包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化: + +```Go +var a = b + c // a 第三个初始化, 为 3 +var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c) +var c = 1 // c 第一个初始化, 为 1 + +func f() int { return c + 1 } +``` + +如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。 + +对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数 + +```Go +func init() { /* ... */ } +``` + +这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。 + +每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。 + +下面的代码定义了一个PopCount函数,用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pc,pc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数,这样的话在处理64bit宽度的数字时就没有必要循环64次,只需要8次查表就可以了。(这并不是最快的统计1bit数目的算法,但是它可以方便演示init函数的用法,并且演示了如何预生成辅助表格,这是编程中常用的技术)。 + +gopl.io/ch2/popcount +```Go +package popcount + +// pc[i] is the population count of i. +var pc [256]byte + +func init() { + for i := range pc { + pc[i] = pc[i/2] + byte(i&1) + } +} + +// PopCount returns the population count (number of set bits) of x. +func PopCount(x uint64) int { + return int(pc[byte(x>>(0*8))] + + pc[byte(x>>(1*8))] + + pc[byte(x>>(2*8))] + + pc[byte(x>>(3*8))] + + pc[byte(x>>(4*8))] + + pc[byte(x>>(5*8))] + + pc[byte(x>>(6*8))] + + pc[byte(x>>(7*8))]) +} +``` + +译注:对于pc这类需要复杂处理的初始化,可以通过将初始化逻辑包装为一个匿名函数处理,像下面这样: + +```Go +// pc[i] is the population count of i. +var pc [256]byte = func() (pc [256]byte) { + for i := range pc { + pc[i] = pc[i/2] + byte(i&1) + } + return +}() +``` + +要注意的是在init函数中,range循环只使用了索引,省略了没有用到的值部分。循环也可以这样写: + +```Go +for i, _ := range pc { +``` + +我们在下一节和10.5节还将看到其它使用init函数的地方。 + +**练习 2.3:** 重写PopCount函数,用一个循环代替单一的表达式。比较两个版本的性能。(11.4节将展示如何系统地比较两个不同实现的性能。) + +**练习 2.4:** 用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。比较和查表算法的性能差异。 + +**练习 2.5:** 表达式`x&(x-1)`用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数,然后比较性能。 diff --git a/ch2/ch2.md b/ch2/ch2.md index ca598b5..1ae3e48 100644 --- a/ch2/ch2.md +++ b/ch2/ch2.md @@ -1,4 +1,4 @@ -# 第二章 程序结构 +# 第2章 程序结构 Go语言和其他编程语言一样,一个大的程序是由很多小的基础构件组成的。变量保存值,简单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的数据结构。然后使用if和for之类的控制语句来组织和控制表达式的执行流程。然后多个语句被组织到一个个函数中,以便代码的隔离和复用。函数以源文件和包的方式被组织。 diff --git a/ch3/ch3-05.md b/ch3/ch3-05.md index 6136a08..414b446 100644 --- a/ch3/ch3-05.md +++ b/ch3/ch3-05.md @@ -66,12 +66,386 @@ s[0] = 'L' // compile error: cannot assign to s[0] 不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。 -{% include "./ch3-05-1.md" %} +### 3.5.1. 字符串面值 -{% include "./ch3-05-2.md" %} +字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号内即可: -{% include "./ch3-05-3.md" %} +``` +"Hello, 世界" +``` + +![](../images/ch3-04.png) + +因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。 + +在一个双引号包含的字符串面值中,可以用以反斜杠`\`开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式: + +``` +\a 响铃 +\b 退格 +\f 换页 +\n 换行 +\r 回车 +\t 制表符 +\v 垂直制表符 +\' 单引号(只用在 '\'' 形式的rune符号面值中) +\" 双引号(只用在 "..." 形式的字符串面值中) +\\ 反斜杠 +``` + +可以通过十六进制或八进制转义在字符串面值中包含任意的字节。一个十六进制的转义形式是`\xhh`,其中两个h表示十六进制数字(大写或小写都可以)。一个八进制转义形式是`\ooo`,包含三个八进制的o数字(0到7),但是不能超过`\377`(译注:对应一个字节的范围,十进制为255)。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。 + +一个原生的字符串面值形式是\`...\`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写\`字符的,可以用八进制或十六进制转义或+"\`"连接字符串常量完成)。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统(译注:Windows系统会把回车和换行一起放入文本文件中)。 + +原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。 + +```Go +const GoUsage = `Go is a tool for managing Go source code. + +Usage: + go command [arguments] +...` +``` + +### 3.5.2. Unicode + +在很久以前,世界还是比较简单的,起码计算机世界就只有一个ASCII字符集:美国信息交换标准代码。ASCII,更准确地说是美国的ASCII,使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。对于早期的计算机程序来说,这些就足够了,但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展,混合多种语言的数据变得很常见(译注:比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符)。如何有效处理这些包含了各种语言的丰富多样的文本数据呢? + +答案就是使用Unicode( http://unicode.org ),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。 + +在第八版本的Unicode标准里收集了超过120,000个字符,涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢?通用的表示一个Unicode码点的数据类型是int32,也就是Go语言中rune对应的类型;它的同义词rune符文正是这个意思。 + +我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4,每个Unicode码点都使用同样大小的32bit来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个,也就是说用16bit编码方式就能表达常用字符。但是,还有其它更好的编码方法吗? + +### 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码点也是采用类似的策略处理。 + +``` +0xxxxxxx runes 0-127 (ASCII) +110xxxxx 10xxxxxx 128-2047 (values <128 unused) +1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused) +11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused) +``` + +变长的编码无法直接通过索引来访问第n个字符,但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑,完全兼容ASCII码,并且可以自动同步:它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看(译注:像GBK之类的编码,如果不知道起点位置则可能会出现歧义)。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。 + +Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。 + +有很多Unicode字符很难直接从键盘输入,并且还有很多字符有着相似的结构;有一些甚至是不可见的字符(译注:中文和日文就有很多相似但不同的字)。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式:`\uhhhh`对应16bit的码点值,`\Uhhhhhhhh`对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。例如:下面的字母串面值都表示相同的值: + +``` +"世界" +"\xe4\xb8\x96\xe7\x95\x8c" +"\u4e16\u754c" +"\U00004e16\U0000754c" +``` + +上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。 + +Unicode转义也可以使用在rune字符中。下面三个字符是等价的: + +``` +'世' '\u4e16' '\U00004e16' +``` + +对于小于256的码点值可以写在一个十六进制转义字节中,例如`\x41`对应字符'A',但是对于更大的码点则必须使用`\u`或`\U`转义形式。因此,`\xe4\xb8\x96`并不是一个合法的rune字符,虽然这三个字节对应一个有效的UTF8编码的码点。 + +得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀: + +```Go +func HasPrefix(s, prefix string) bool { + return len(s) >= len(prefix) && s[:len(prefix)] == prefix +} +``` + +或者是后缀测试: + +```Go +func HasSuffix(s, suffix string) bool { + return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix +} +``` + +或者是包含子串测试: + +```Go +func Contains(s, substr string) bool { + for i := 0; i < len(s); i++ { + if HasPrefix(s[i:], substr) { + return true + } + } + return false +} +``` + +对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。(上面的函数都来自strings字符串处理包,真实的代码包含了一个用哈希技术优化的Contains 实现。) + +另一方面,如果我们真的关心每个Unicode字符,我们可以使用其它处理方式。考虑前面的第一个例子中的字符串,它混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节,以UTF8形式编码,但是只对应9个Unicode字符: + +```Go +import "unicode/utf8" + +s := "Hello, 世界" +fmt.Println(len(s)) // "13" +fmt.Println(utf8.RuneCountInString(s)) // "9" +``` + +为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用: + +```Go +for i := 0; i < len(s); { + r, size := utf8.DecodeRuneInString(s[i:]) + fmt.Printf("%d\t%c\n", i, r) + i += size +} +``` + +每一次调用DecodeRuneInString函数都返回一个r和长度,r对应字符本身,长度对应r采用UTF8编码后的编码字节数目。长度可以用于更新第i个字符在字符串中的字节索引位置。但是这种编码方式是笨拙的,我们需要更简洁的语法。幸运的是,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。下面的循环运行如图3.5所示;需要注意的是对于非ASCII,索引更新的步长将超过1个字节。 + +![](../images/ch3-05.png) + +```Go +for i, r := range "Hello, 世界" { + fmt.Printf("%d\t%q\t%d\n", i, r, r) +} +``` + +我们可以使用一个简单的循环来统计字符串中字符的数目,像这样: + +```Go +n := 0 +for _, _ = range s { + n++ +} +``` + +像其它形式的循环那样,我们也可以忽略不需要的变量: + +```Go +n := 0 +for range s { + n++ +} +``` + +或者我们可以直接调用utf8.RuneCountInString(s)函数。 + +正如我们前面提到的,文本字符串采用UTF8编码只是一种惯例,但是对于循环的真正字符串并不是一个惯例,这是正确的。如果用于循环的字符串只是一个普通的二进制数据,或者是含有错误编码的UTF8数据,将会发生什么呢? + +每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符`\uFFFD`,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串。 + +UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。 + +将[]rune类型转换应用到UTF8编码的字符串,将返回字符串编码的Unicode码点序列: + +```Go +// "program" in Japanese katakana +s := "プログラム" +fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0" +r := []rune(s) +fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]" +``` + +(在第一个Printf中的`% x`参数用于在每个十六进制数字前插入一个空格。) + +如果是将一个[]rune类型的Unicode字符slice或数组转为string,则对它们进行UTF8编码: + +```Go +fmt.Println(string(r)) // "プログラム" +``` + +将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串: + +```Go +fmt.Println(string(65)) // "A", not "65" +fmt.Println(string(0x4eac)) // "京" +``` + +如果对应码点的字符是无效的,则用`\uFFFD`无效字符作为替换: + +```Go +fmt.Println(string(1234567)) // "?" +``` + +### 3.5.4. 字符串和Byte切片 + +标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。 + +bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。 + +strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。 + +unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。 + +下面例子的basename函数灵感源于Unix shell的同名工具。在我们实现的版本中,basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除: + +```Go +fmt.Println(basename("a/b/c.go")) // "c" +fmt.Println(basename("c.d.go")) // "c.d" +fmt.Println(basename("abc")) // "abc" +``` + +第一个版本并没有使用任何库,全部手工硬编码实现: + +gopl.io/ch3/basename1 +```Go +// basename removes directory components and a .suffix. +// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c +func basename(s string) string { + // Discard last '/' and everything before. + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '/' { + s = s[i+1:] + break + } + } + // Preserve everything before last '.'. + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '.' { + s = s[:i] + break + } + } + return s +} +``` + +这个简化版本使用了strings.LastIndex库函数: + +gopl.io/ch3/basename2 +```Go +func basename(s string) string { + slash := strings.LastIndex(s, "/") // -1 if "/" not found + s = s[slash+1:] + if dot := strings.LastIndex(s, "."); dot >= 0 { + s = s[:dot] + } + return s +} +``` + +path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名,但是在其他一些领域可能会用于文件名,例如URL路径组件。相比之下,path/filepath包则使用操作系统本身的路径规则,例如POSIX系统使用/foo/bar,而Microsoft Windows使用`c:\foo\bar`等。 + +让我们继续另一个字符串的例子。函数的功能是将一个表示整数值的字符串,每隔三个字符插入一个逗号分隔符,例如“12345”处理后成为“12,345”。这个版本只适用于整数类型;支持浮点数类型的留作练习。 + +gopl.io/ch3/comma +```Go +// comma inserts commas in a non-negative decimal integer string. +func comma(s string) string { + n := len(s) + if n <= 3 { + return s + } + return comma(s[:n-3]) + "," + s[n-3:] +} +``` + +输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话,则不需要插入逗号分隔符。否则,comma函数将在最后三个字符前的位置将字符串切割为两个子串并插入逗号分隔符,然后通过递归调用自身来得出前面的子串。 + +一个字符串是包含只读字节的数组,一旦创建,是不可变的。相比之下,一个字节slice的元素则可以自由地修改。 + +字符串和字节slice之间可以相互转换: + +```Go +s := "abc" +b := []byte(s) +s2 := string(b) +``` + +从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在变量b被修改的情况下,原始的s字符串也不会改变。将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝,以确保s2字符串是只读的。 + +为了避免转换中不必要的内存分配,bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数: + +```Go +func Contains(s, substr string) bool +func Count(s, sep string) int +func Fields(s string) []string +func HasPrefix(s, prefix string) bool +func Index(s, sep string) int +func Join(a []string, sep string) string +``` + +bytes包中也对应的六个函数: + +```Go +func Contains(b, subslice []byte) bool +func Count(s, sep []byte) int +func Fields(s []byte) [][]byte +func HasPrefix(s, prefix []byte) bool +func Index(s, sep []byte) int +func Join(s [][]byte, sep []byte) []byte +``` + +它们之间唯一的区别是字符串类型参数被替换成了字节slice类型的参数。 + +bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要初始化,因为零值也是有效的: + +gopl.io/ch3/printints +```Go +// intsToString is like fmt.Sprint(values) but adds commas. +func intsToString(values []int) string { + var buf bytes.Buffer + buf.WriteByte('[') + for i, v := range values { + if i > 0 { + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "%d", v) + } + buf.WriteByte(']') + return buf.String() +} + +func main() { + fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]" +} +``` + +当向bytes.Buffer添加任意字符的UTF8编码时,最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法对于写入类似'['和']'等ASCII字符则会更加有效。 + +bytes.Buffer类型有着很多实用的功能,我们在第七章讨论接口时将会涉及到,我们将看看如何将它用作一个I/O的输入和输出对象,例如当做Fprintf的io.Writer输出对象,或者当作io.Reader类型的输入源对象。 + +**练习 3.10:** 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作。 + +**练习 3.11:** 完善comma函数,以支持浮点数处理和一个可选的正负号的处理。 + +**练习 3.12:** 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。 + +### 3.5.5. 字符串和数字的转换 + +除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。 + +将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”): + +```Go +x := 123 +y := fmt.Sprintf("%d", x) +fmt.Println(y, strconv.Itoa(x)) // "123 123" +``` + +FormatInt和FormatUint函数可以用不同的进制来格式化数字: + +```Go +fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011" +``` + +fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候: + +```Go +s := fmt.Sprintf("x=%b", x) // "x=1111011" +``` + +如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数: + +```Go +x, err := strconv.Atoi("123") // x is an int +y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits +``` + +ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。 + +有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。 -{% include "./ch3-05-4.md" %} -{% include "./ch3-05-5.md" %} diff --git a/ch3/ch3-06.md b/ch3/ch3-06.md index 6a9d14c..e6b50ed 100644 --- a/ch3/ch3-06.md +++ b/ch3/ch3-06.md @@ -58,9 +58,183 @@ fmt.Println(a, b, c, d) // "1 1 2 2" 如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是iota常量生成器语法。 -{% include "./ch3-06-1.md" %} +### 3.6.1. iota 常量生成器 -{% include "./ch3-06-2.md" %} +常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。 + +下面是来自time包的例子,它首先定义了一个Weekday命名类型,然后为一周的每天定义了一个常量,从周日0开始。在其它编程语言中,这种类型一般被称为枚举类型。 + +```Go +type Weekday int + +const ( + Sunday Weekday = iota + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday +) +``` + +周日将对应0,周一为1,如此等等。 + +我们也可以在复杂的常量表达式中使用iota,下面是来自net包的例子,用于给一个无符号整数的最低5bit的每个bit指定一个名字: + +```Go +type Flags uint + +const ( + FlagUp Flags = 1 << iota // is up + FlagBroadcast // supports broadcast access capability + FlagLoopback // is a loopback interface + FlagPointToPoint // belongs to a point-to-point link + FlagMulticast // supports multicast access capability +) +``` + +随着iota的递增,每个常量对应表达式1 << iota,是连续的2的幂,分别对应一个bit位置。使用这些常量可以用于测试、设置或清除对应的bit位的值: + +gopl.io/ch3/netflag +```Go +func IsUp(v Flags) bool { return v&FlagUp == FlagUp } +func TurnDown(v *Flags) { *v &^= FlagUp } +func SetBroadcast(v *Flags) { *v |= FlagBroadcast } +func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 } + +func main() { + var v Flags = FlagMulticast | FlagUp + fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true" + TurnDown(&v) + fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false" + SetBroadcast(&v) + fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false" + fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true" +} +``` + +下面是一个更复杂的例子,每个常量都是1024的幂: + +```Go +const ( + _ = 1 << (10 * iota) + KiB // 1024 + MiB // 1048576 + GiB // 1073741824 + TiB // 1099511627776 (exceeds 1 << 32) + PiB // 1125899906842624 + EiB // 1152921504606846976 + ZiB // 1180591620717411303424 (exceeds 1 << 64) + YiB // 1208925819614629174706176 +) +``` + +不过iota常量生成规则也有其局限性。例如,它并不能用于产生1000的幂(KB、MB等),因为Go语言并没有计算幂的运算符。 + +**练习 3.13:** 编写KB、MB的常量声明,然后扩展到YB。 + +### 3.6.2. 无类型常量 + +Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。 + +通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围,但是它们依然是合法的常量,而且像下面的常量表达式依然有效(译注:YiB/ZiB是在编译期计算出来的,并且结果常量是1024,是Go语言int变量能有效表示的): + +```Go +fmt.Println(YiB/ZiB) // "1024" +``` + +另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方: + +```Go +var x float32 = math.Pi +var y float64 = math.Pi +var z complex128 = math.Pi +``` + +如果math.Pi被确定为特定类型,比如float64,那么结果精度可能会不一样,同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换: + +```Go +const Pi64 float64 = math.Pi + +var x float32 = float32(Pi64) +var y float64 = Pi64 +var z complex128 = complex128(Pi64) +``` + +对于常量面值,不同的写法可能会对应不同的类型。例如0、0.0、0i和`\u0000`虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。 + +前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果: + +```Go +var f float64 = 212 +fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 is a float64 +fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0 +fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float +``` + +只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。 + +```Go +var f float64 = 3 + 0i // untyped complex -> float64 +f = 2 // untyped integer -> float64 +f = 1e123 // untyped floating-point -> float64 +f = 'a' // untyped rune -> float64 +``` + +上面的语句相当于: + +```Go +var f float64 = float64(3 + 0i) +f = float64(2) +f = float64(1e123) +f = float64('a') +``` + +无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理: + +```Go +const ( + deadbeef = 0xdeadbeef // untyped int with value 3735928559 + a = uint32(deadbeef) // uint32 with value 3735928559 + b = float32(deadbeef) // float32 with value 3735928576 (rounded up) + c = float64(deadbeef) // float64 with value 3735928559 (exact) + d = int32(deadbeef) // compile error: constant overflows int32 + e = float64(1e309) // compile error: constant overflows float64 + f = uint(-1) // compile error: constant underflows uint +) +``` + +对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子: + +```Go +i := 0 // untyped integer; implicit int(0) +r := '\000' // untyped rune; implicit rune('\000') +f := 0.0 // untyped floating-point; implicit float64(0.0) +c := 0i // untyped complex; implicit complex128(0i) +``` + +注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。 +如果不知道浮点数类型的内存大小是很难写出正确的数值算法的,因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型。 + + +如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样: + +```Go +var i = int8(0) +var i int8 = 0 +``` + +当尝试将这些无类型的常量转为一个接口值时(见第7章),这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。 + +```Go +fmt.Printf("%T\n", 0) // "int" +fmt.Printf("%T\n", 0.0) // "float64" +fmt.Printf("%T\n", 0i) // "complex128" +fmt.Printf("%T\n", '\000') // "int32" (rune) +``` + +现在我们已经讲述了Go语言中全部的基础数据类型。下一步将演示如何用基础数据类型组合成数组或结构体等复杂数据类型,然后构建用于解决实际编程问题的数据结构,这将是第四章的讨论主题。 diff --git a/ch3/ch3.md b/ch3/ch3.md index 949e55e..2df8768 100644 --- a/ch3/ch3.md +++ b/ch3/ch3.md @@ -1,4 +1,4 @@ -# 第三章 基础数据类型 +# 第3章 基础数据类型 虽然从底层而言,所有的数据都是由比特组成,但计算机一般操作的是固定大小的数,如整数、浮点数、比特数组、内存地址等。进一步将这些数组织在一起,就可表达更多的对象,例如数据包、像素点、诗歌,甚至其他任何对象。Go语言提供了丰富的数据组织形式,这依赖于Go语言内置的数据类型。这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构的便捷性。 diff --git a/ch4/ch4-02.md b/ch4/ch4-02.md index 4f035c0..41070d7 100644 --- a/ch4/ch4-02.md +++ b/ch4/ch4-02.md @@ -125,6 +125,230 @@ make([]T, len, cap) // same as make([]T, cap)[:len] 在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。 -{% include "./ch4-02-1.md" %} +### 4.2.1. append函数 -{% include "./ch4-02-2.md" %} +内置的append函数用于向slice追加元素: + +```Go +var runes []rune +for _, r := range "Hello, 世界" { + runes = append(runes, r) +} +fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']" +``` + +在循环中使用append函数构建一个由九个rune字符构成的slice,当然对应这个特殊的问题我们可以通过Go语言内置的[]rune("Hello, 世界")转换操作完成。 + +append函数对于理解slice底层是如何工作的非常重要,所以让我们仔细查看究竟是发生了什么。下面是第一个版本的appendInt函数,专门用于处理[]int类型的slice: + +gopl.io/ch4/append +```Go +func appendInt(x []int, y int) []int { + var z []int + zlen := len(x) + 1 + if zlen <= cap(x) { + // There is room to grow. Extend the slice. + z = x[:zlen] + } else { + // There is insufficient space. Allocate a new array. + // Grow by doubling, for amortized linear complexity. + zcap := zlen + if zcap < 2*len(x) { + zcap = 2 * len(x) + } + z = make([]int, zlen, zcap) + copy(z, x) // a built-in function; see text + } + z[len(x)] = y + return z +} +``` + +每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。 + +如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。 + +虽然通过循环复制元素更直接,不过内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和`dst = src`赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数(我们这里没有用到),等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围。 + +为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操作的平均时间是一个常数时间。这个程序演示了效果: + +```Go +func main() { + var x, y []int + for i := 0; i < 10; i++ { + y = appendInt(x, i) + fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y) + x = y + } +} +``` + +每一次容量的变化都会导致重新分配内存和copy操作: + +``` +0 cap=1 [0] +1 cap=2 [0 1] +2 cap=4 [0 1 2] +3 cap=4 [0 1 2 3] +4 cap=8 [0 1 2 3 4] +5 cap=8 [0 1 2 3 4 5] +6 cap=8 [0 1 2 3 4 5 6] +7 cap=8 [0 1 2 3 4 5 6 7] +8 cap=16 [0 1 2 3 4 5 6 7 8] +9 cap=16 [0 1 2 3 4 5 6 7 8 9] +``` + +让我们仔细查看i=3次的迭代。当时x包含了[0 1 2]三个元素,但是容量是4,因此可以简单将新的元素添加到末尾,不需要新的内存分配。然后新的y的长度和容量都是4,并且和x引用着相同的底层数组,如图4.2所示。 + +![](../images/ch4-02.png) + +在下一次迭代时i=4,现在没有新的空余的空间了,因此appendInt函数分配一个容量为8的底层数组,将x的4个元素[0 1 2 3]复制到新空间的开头,然后添加新的元素i,新元素的值是4。新的y的长度是5,容量是8;后面有3个空闲的位置,三次迭代都不需要分配新的空间。当前迭代中,y和x是对应不同底层数组的view。这次操作如图4.3所示。 + +![](../images/ch4-03.png) + +内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量: + +```Go +runes = append(runes, r) +``` + +更新slice变量不仅对调用append函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型: + +```Go +type IntSlice struct { + ptr *int + len, cap int +} +``` + +我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。 + +```Go +var x []int +x = append(x, 1) +x = append(x, 2, 3) +x = append(x, 4, 5, 6) +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节详细解释这个特性。 + +```Go +func appendInt(x []int, y ...int) []int { + var z []int + zlen := len(x) + len(y) + // ...expand z to at least zlen... + copy(z[len(x):], y) + return z +} +``` + +为了避免重复,和前面相同的代码并没有显示。 + +### 4.2.2. Slice内存技巧 + +让我们看看更多的例子,比如旋转slice、反转slice或在slice原有内存空间修改元素。给定一个字符串列表,下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表: + +gopl.io/ch4/nonempty +```Go +// Nonempty is an example of an in-place slice algorithm. +package main + +import "fmt" + +// nonempty returns a slice holding only the non-empty strings. +// The underlying array is modified during the call. +func nonempty(strings []string) []string { + i := 0 + for _, s := range strings { + if s != "" { + strings[i] = s + i++ + } + } + return strings[:i] +} +``` + +比较微妙的地方是,输入的slice和输出的slice共享一个底层数组。这可以避免分配另一个数组,不过原来的数据将可能会被覆盖,正如下面两个打印语句看到的那样: + +```Go +data := []string{"one", "", "three"} +fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]` +fmt.Printf("%q\n", data) // `["one" "three" "three"]` +``` + +因此我们通常会这样使用nonempty函数:`data = nonempty(data)`。 + +nonempty函数也可以使用append函数实现: + +```Go +func nonempty2(strings []string) []string { + out := strings[:0] // zero-length slice of original + for _, s := range strings { + if s != "" { + out = append(out, s) + } + } + return out +} +``` + +无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。 + +一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack: + +```Go +stack = append(stack, v) // push v +``` + +stack的顶部位置对应slice的最后一个元素: + +```Go +top := stack[len(stack)-1] // top of stack +``` + +通过收缩stack可以弹出栈顶的元素 + +```Go +stack = stack[:len(stack)-1] // pop +``` + +要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成: + +```Go +func remove(slice []int, i int) []int { + copy(slice[i:], slice[i+1:]) + return slice[:len(slice)-1] +} + +func main() { + s := []int{5, 6, 7, 8, 9} + fmt.Println(remove(s, 2)) // "[5 6 8 9]" +} +``` + +如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素: + +```Go +func remove(slice []int, i int) []int { + slice[i] = slice[len(slice)-1] + return slice[:len(slice)-1] +} + +func main() { + s := []int{5, 6, 7, 8, 9} + fmt.Println(remove(s, 2)) // "[5 6 9 8] +} +``` + +**练习 4.3:** 重写reverse函数,使用数组指针代替slice。 + +**练习 4.4:** 编写一个rotate函数,通过一次循环完成旋转。 + +**练习 4.5:** 写一个函数在原地完成消除[]string中相邻重复的字符串的操作。 + +**练习 4.6:** 编写一个函数,原地将一个UTF-8编码的[]byte类型的slice中相邻的空格(参考unicode.IsSpace)替换成一个空格返回 + +**练习 4.7:** 修改reverse函数用于原地反转UTF-8编码的[]byte。是否可以不用分配额外的内存? diff --git a/ch4/ch4-04.md b/ch4/ch4-04.md index 96d59e6..1bab6be 100644 --- a/ch4/ch4-04.md +++ b/ch4/ch4-04.md @@ -134,8 +134,229 @@ if _, ok := seen[s]; !ok { } ``` -{% include "./ch4-04-1.md" %} +### 4.4.1. 结构体字面值 -{% include "./ch4-04-2.md" %} +结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。 -{% include "./ch4-04-3.md" %} +```Go +type Point struct{ X, Y int } + +p := Point{1, 2} +``` + +这里有两种形式的结构体字面值语法,上面的是第一种写法,要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。它要求写代码和读代码的人要记住结构体的每个成员的类型和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。 + +其实更常用的是第二种写法,以成员名字和相应的值来初始化,可以包含部分或全部的成员,如1.4节的Lissajous程序的写法: + +```Go +anim := gif.GIF{LoopCount: nframes} +``` + +在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。 + +两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。 + +```Go +package p +type T struct{ a, b int } // a and b are not exported + +package q +import "p" +var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b +var _ = p.T{1, 2} // compile error: can't reference a, b +``` + +虽然上面最后一行代码的编译错误信息中并没有显式提到未导出的成员,但是这样企图隐式使用未导出成员的行为也是不允许的。 + +结构体可以作为函数的参数和返回值。例如,这个Scale函数将Point类型的值缩放后返回: + +```Go +func Scale(p Point, factor int) Point { + return Point{p.X * factor, p.Y * factor} +} + +fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}" +``` + +如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回, + +```Go +func Bonus(e *Employee, percent int) int { + return e.Salary * percent / 100 +} +``` + +如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。 + +```Go +func AwardAnnualRaise(e *Employee) { + e.Salary = e.Salary * 105 / 100 +} +``` + +因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址: + +```Go +pp := &Point{1, 2} +``` + +它和下面的语句是等价的 + +```Go +pp := new(Point) +*pp = Point{1, 2} +``` + +不过&Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用。 + +### 4.4.2. 结构体比较 + +如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的: + +```Go +type Point struct{ X, Y int } + +p := Point{1, 2} +q := Point{2, 1} +fmt.Println(p.X == q.X && p.Y == q.Y) // "false" +fmt.Println(p == q) // "false" +``` + +可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。 + +```Go +type address struct { + hostname string + port int +} + +hits := make(map[address]int) +hits[address{"golang.org", 443}]++ +``` + + +### 4.4.3. 结构体嵌入和匿名成员 + +在本节中,我们将看到如何使用Go语言提供的不同寻常的结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员,这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。 + +考虑一个二维的绘图程序,提供了一个各种图形的库,例如矩形、椭圆形、星形和轮形等几何形状。这里是其中两个的定义: + +```Go +type Circle struct { + X, Y, Radius int +} + +type Wheel struct { + X, Y, Radius, Spokes int +} +``` + +一个Circle代表的圆形类型包含了标准圆心的X和Y坐标信息,和一个Radius表示的半径信息。一个Wheel轮形除了包含Circle类型所有的全部成员外,还增加了Spokes表示径向辐条的数量。我们可以这样创建一个wheel变量: + +```Go +var w Wheel +w.X = 8 +w.Y = 8 +w.Radius = 5 +w.Spokes = 20 +``` + +随着库中几何形状数量的增多,我们一定会注意到它们之间的相似和重复之处,所以我们可能为了便于维护而将相同的属性独立出来: + +```Go +type Point struct { + X, Y int +} + +type Circle struct { + Center Point + Radius int +} + +type Wheel struct { + Circle Circle + Spokes int +} +``` + +这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐: + +```Go +var w Wheel +w.Circle.Center.X = 8 +w.Circle.Center.Y = 8 +w.Circle.Radius = 5 +w.Spokes = 20 +``` + +Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。 + +```Go +type Circle struct { + Point + Radius int +} + +type Wheel struct { + Circle + Spokes int +} +``` + +得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径: + +```Go +var w Wheel +w.X = 8 // equivalent to w.Circle.Point.X = 8 +w.Y = 8 // equivalent to w.Circle.Point.Y = 8 +w.Radius = 5 // equivalent to w.Circle.Radius = 5 +w.Spokes = 20 +``` + +在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。 + +不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过: + +```Go +w = Wheel{8, 8, 5, 20} // compile error: unknown fields +w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields +``` + +结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的: + +gopl.io/ch4/embed +```Go +w = Wheel{Circle{Point{8, 8}, 5}, 20} + +w = Wheel{ + Circle: Circle{ + Point: Point{X: 8, Y: 8}, + Radius: 5, + }, + Spokes: 20, // NOTE: trailing comma necessary here (and at Radius) +} + +fmt.Printf("%#v\n", w) +// Output: +// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20} + +w.X = 42 + +fmt.Printf("%#v\n", w) +// Output: +// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20} +``` + +需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。 + +因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员 + +```Go +w.X = 8 // equivalent to w.circle.point.X = 8 +``` + +但是在包外部,因为circle和point没有导出,不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。 + +到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢? + +答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心,我们将在6.3节中专门讨论。 diff --git a/ch4/ch4-06.md b/ch4/ch4-06.md index 22655d4..d71881a 100644 --- a/ch4/ch4-06.md +++ b/ch4/ch4-06.md @@ -4,7 +4,7 @@ 一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的`{{action}}`对象。大部分的字符串只是按字面值打印,但是对于actions部分将触发其它的行为。每个actions都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。下面是一个简单的模板字符串: -{% raw %} + gopl.io/ch4/issuesreport ```Go @@ -19,7 +19,7 @@ Age: {{.CreatedAt | daysAgo}} days {% endraw %} -{% raw %} + 这个模板先打印匹配到的issue总数,然后打印每个issue的编号、创建用户、标题还有存在的时间。对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被初始化为调用模板时的参数,在当前例子中对应github.IssuesSearchResult类型的变量。模板中`{{.TotalCount}}`对应action将展开为结构体中TotalCount成员以默认的方式打印的值。模板中`{{range .Items}}`和`{{end}}`对应一个循环action,因此它们之间的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。 @@ -90,7 +90,7 @@ Age: 695 days 下面的模板以HTML格式输出issue列表。注意import语句的不同: -{% raw %} + gopl.io/ch4/issueshtml ```Go @@ -142,7 +142,7 @@ $ ./issueshtml repo:golang/go 3133 10535 >issues2.html ![](../images/ch4-05.png) -{% raw %} + gopl.io/ch4/autoescape ```Go diff --git a/ch4/ch4.md b/ch4/ch4.md index e8ff345..a8c5d3d 100644 --- a/ch4/ch4.md +++ b/ch4/ch4.md @@ -1,4 +1,4 @@ -# 第四章 复合数据类型 +# 第4章 复合数据类型 在第三章我们讨论了基本数据类型,它们可以用于构建程序中数据的结构,是Go语言世界的原子。在本章,我们将讨论复合数据类型,它是以不同的方式组合基本类型而构造出来的复合数据类型。我们主要讨论四种类型——数组、slice、map和结构体——同时在本章的最后,我们将演示如何使用结构体来解码和编码到对应JSON格式的数据,并且通过结合使用模板来生成HTML页面。 diff --git a/ch5/ch5-04.md b/ch5/ch5-04.md index 286cd02..29a921d 100644 --- a/ch5/ch5-04.md +++ b/ch5/ch5-04.md @@ -34,6 +34,157 @@ Go这样设计的原因是由于对于某个应该在控制流程中处理的错 正因此,Go使用控制流机制(如if和return)处理错误,这使得编码人员能更多的关注错误处理。 -{% include "./ch5-04-1.md" %} +### 5.4.1. 错误处理策略 -{% include "./ch5-04-2.md" %} +当一次函数调用返回错误时,调用者应该选择合适的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。 + +首先,也是最常用的方式是传播错误。这意味着函数中某个子程序的失败,会变成该函数的失败。下面,我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败,findLinks会直接将这个HTTP错误返回给调用者: + +```Go +resp, err := http.Get(url) +if err != nil{ + return nil, err +} +``` + +当对html.Parse的调用失败时,findLinks不会直接返回html.Parse的错误,因为缺少两条重要信息:1、发生错误时的解析器(html parser);2、发生错误的url。因此,findLinks构造了一个新的错误信息,既包含了这两项,也包括了底层的解析出错的信息。 + +```Go +doc, err := html.Parse(resp.Body) +resp.Body.Close() +if err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url,err) +} +``` + +fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数添加额外的前缀上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链,就像美国宇航局事故调查时做的那样: + +``` +genesis: crashed: no parachute: G-switch failed: bad relay orientation +``` + +由于错误信息经常是以链式组合在一起的,所以错误信息中应避免大写和换行符。最终的错误信息可能很长,我们可以通过类似grep的工具处理错误信息(译者注:grep是一种文本搜索工具)。 + +编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。 + +以os包为例,os包确保文件操作(如os.Open、Read、Write、Close)返回的每个错误的描述不仅仅包含错误的原因(如无权限,文件目录不存在)也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息。 + +一般而言,被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中。 + +让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。 + +gopl.io/ch5/wait +```Go +// WaitForServer attempts to contact the server of a URL. +// It tries for one minute using exponential back-off. +// It reports an error if all attempts fail. +func WaitForServer(url string) error { + const timeout = 1 * time.Minute + deadline := time.Now().Add(timeout) + for tries := 0; time.Now().Before(deadline); tries++ { + _, err := http.Head(url) + if err == nil { + return nil // success + } + log.Printf("server not responding (%s);retrying…", err) + time.Sleep(time.Second << uint(tries)) // exponential back-off + } + return fmt.Errorf("server %s failed to respond after %s", url, timeout) +} +``` + +如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序。需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。 + +```Go +// (In function main.) +if err := WaitForServer(url); err != nil { + fmt.Fprintf(os.Stderr, "Site is down: %v\n", err) + os.Exit(1) +} +``` + +调用log.Fatalf可以更简洁的代码达到与上文相同的效果。log中的所有函数,都默认会在错误信息之前输出时间信息。 + +```Go +if err := WaitForServer(url); err != nil { + log.Fatalf("Site is down: %v\n", err) +} +``` + +长时间运行的服务器常采用默认的时间格式,而交互式工具很少采用包含如此多信息的格式。 + +``` +2006/01/02 15:04:05 Site is down: no such domain: +bad.gopl.io +``` + +我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名。 + +```Go +log.SetPrefix("wait: ") +log.SetFlags(0) +``` + +第四种策略:有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过log包提供函数 + +```Go +if err := Ping(); err != nil { + log.Printf("ping failed: %v; networking disabled",err) +} +``` + +或者标准错误流输出错误信息。 + +```Go +if err := Ping(); err != nil { + fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err) +} +``` + +log包中的所有函数会为没有换行符的字符串增加换行符。 + +第五种,也是最后一种策略:我们可以直接忽略掉错误。 + +```Go +dir, err := ioutil.TempDir("", "scratch") +if err != nil { + return fmt.Errorf("failed to create temp dir: %v",err) +} +// ...use temp dir… +os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically +``` + +尽管os.RemoveAll会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该清晰地写下你的意图。 + +在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。Go中大部分函数的代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑。 + +### 5.4.2. 文件结尾错误(EOF) + +函数经常会返回多种错误,这对终端用户来说可能会很有趣,但对程序而言,这使得情况变得复杂。很多时候,程序必须根据错误类型,作出不同的响应。让我们考虑这样一个例子:从文件中读取n个字节。如果n等于文件的长度,读取过程的任何错误都表示失败。如果n小于文件的长度,调用者会重复的读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因,io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在io包中定义: + +```Go +package io + +import "errors" + +// EOF is the error returned by Read when no more input is available. +var EOF = errors.New("EOF") +``` + +调用者只需通过简单的比较,就可以检测出这个错误。下面的例子展示了如何从标准输入中读取字符,以及判断文件结束。(4.3的chartcount程序展示了更加复杂的代码) + +```Go +in := bufio.NewReader(os.Stdin) +for { + r, _, err := in.ReadRune() + if err == io.EOF { + break // finished reading + } + if err != nil { + return fmt.Errorf("read failed:%v", err) + } + // ...use r… +} +``` + +因为文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息——“EOF”。对于其他错误,我们可能需要在错误信息中描述错误的类型和数量,这使得我们不能像io.EOF一样采用固定的错误信息。在7.11节中,我们会提出更系统的方法区分某些固定的错误值。 diff --git a/ch5/ch5-06.md b/ch5/ch5-06.md index 161d3de..2ca05a6 100644 --- a/ch5/ch5-06.md +++ b/ch5/ch5-06.md @@ -244,4 +244,61 @@ http://research.swtch.com/gotour **练习5.14:** 使用breadthFirst遍历其他数据结构。比如,topoSort例子中的课程依赖关系(有向图)、个人计算机的文件层次结构(树);你所在城市的公交或地铁线路(无向图)。 -{% include "./ch5-06-1.md" %} +### 5.6.1. 警告:捕获迭代变量 + +本节,将介绍Go词法作用域的一个陷阱。请务必仔细的阅读,弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。 + +考虑这样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。 + +```Go +var rmdirs []func() +for _, d := range tempDirs() { + dir := d // NOTE: necessary! + os.MkdirAll(dir, 0755) // creates parent directories too + rmdirs = append(rmdirs, func() { + os.RemoveAll(dir) + }) +} +// ...do some work… +for _, rmdir := range rmdirs { + rmdir() // clean up +} +``` + +你可能会感到困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是像下面的代码一样直接使用循环变量dir。需要注意,下面的代码是错误的。 + +```go +var rmdirs []func() +for _, dir := range tempDirs() { + os.MkdirAll(dir, 0755) + rmdirs = append(rmdirs, func() { + os.RemoveAll(dir) // NOTE: incorrect! + }) +} +``` + +问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。 + +通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。 + +```Go +for _, dir := range tempDirs() { + dir := dir // declares inner dir, initialized to outer dir + // ... +} +``` + +这个问题不仅存在基于range的循环,在下面的例子中,对循环变量i的使用也存在同样的问题: + +```Go +var rmdirs []func() +dirs := tempDirs() +for i := 0; i < len(dirs); i++ { + os.MkdirAll(dirs[i], 0755) // OK + rmdirs = append(rmdirs, func() { + os.RemoveAll(dirs[i]) // NOTE: incorrect! + }) +} +``` + +如果你使用go语句(第八章)或者defer语句(5.8节)会经常遇到此类问题。这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。 diff --git a/ch5/ch5.md b/ch5/ch5.md index bb41c6e..794b69c 100644 --- a/ch5/ch5.md +++ b/ch5/ch5.md @@ -1,4 +1,4 @@ -# 第五章 函数 +# 第5章 函数 函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。 diff --git a/ch6/ch6-02.md b/ch6/ch6-02.md index 6ab0769..a8d43c0 100644 --- a/ch6/ch6-02.md +++ b/ch6/ch6-02.md @@ -91,4 +91,69 @@ pptr.Distance(q) // implicit (*pptr) 1. 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。 2. 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的因素,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C++的人这里应该很快能明白。 -{% include "./ch6-02-1.md" %} +### 6.2.1. Nil也是一个合法的接收器类型 + +就像一些函数允许nil指针作为参数一样,方法理论上也可以用nil指针作为其接收器,尤其当nil对于对象来说是合法的零值时,比如map或者slice。在下面的简单int链表的例子里,nil代表的是空链表: + +```go +// An IntList is a linked list of integers. +// A nil *IntList represents the empty list. +type IntList struct { + Value int + Tail *IntList +} +// Sum returns the sum of the list elements. +func (list *IntList) Sum() int { + if list == nil { + return 0 + } + return list.Value + list.Tail.Sum() +} +``` + +当你定义一个允许nil作为接收器值的方法的类型时,在类型前面的注释中指出nil变量代表的意义是很有必要的,就像我们上面例子里做的这样。 + +下面是net/url包里Values类型定义的一部分。 + +net/url +```go +package url + +// Values maps a string key to a list of values. +type Values map[string][]string +// Get returns the first value associated with the given key, +// or "" if there are none. +func (v Values) Get(key string) string { + if vs := v[key]; len(vs) > 0 { + return vs[0] + } + return "" +} +// Add adds the value to key. +// It appends to any existing values associated with key. +func (v Values) Add(key, value string) { + v[key] = append(v[key], value) +} +``` + +这个定义向外部暴露了一个map的命名类型,并且提供了一些能够简单操作这个map的方法。这个map的value字段是一个string的slice,所以这个Values是一个多维map。客户端使用这个变量的时候可以使用map固有的一些操作(make,切片,m[key]等等),也可以使用这里提供的操作方法,或者两者并用,都是可以的: + +gopl.io/ch6/urlvalues +```go +m := url.Values{"lang": {"en"}} // direct construction +m.Add("item", "1") +m.Add("item", "2") + +fmt.Println(m.Get("lang")) // "en" +fmt.Println(m.Get("q")) // "" +fmt.Println(m.Get("item")) // "1" (first value) +fmt.Println(m["item"]) // "[1 2]" (direct map access) + +m = nil +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。 + +由于url.Values是一个map类型,并且间接引用了其key/value对,因此url.Values.Add对这个map里的元素做任何的更新、删除操作对调用方都是可见的。实际上,就像在普通函数中一样,虽然可以通过引用来操作内部值,但在方法想要修改引用本身时是不会影响原始值的,比如把他置换为nil,或者让这个引用指向了其它的对象,调用方都不会受影响。(译注:因为传入的是存储了内存地址的变量,你改变这个变量本身是影响不了原始的变量的,想想C语言,是差不多的) diff --git a/ch6/ch6.md b/ch6/ch6.md index 76086b1..0785441 100644 --- a/ch6/ch6.md +++ b/ch6/ch6.md @@ -1,4 +1,4 @@ -# 第六章 方法 +# 第6章 方法 从90年代早期开始,面向对象编程(OOP)就成为了称霸工程界和教育界的编程范式,所以之后几乎所有大规模被应用的语言都包含了对OOP的支持,go语言也不例外。 diff --git a/ch7/ch7-05.md b/ch7/ch7-05.md index 356238c..1957557 100644 --- a/ch7/ch7-05.md +++ b/ch7/ch7-05.md @@ -108,4 +108,57 @@ fmt.Printf("%T\n", w) // "*bytes.Buffer" 在fmt包内部,使用反射来获取接口动态类型的名称。我们会在第12章中学到反射相关的知识。 -{% include "./ch7-05-1.md" %} +### 7.5.1. 警告:一个包含nil指针的接口不是nil接口 + +一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。这个细微区别产生了一个容易绊倒每个Go程序员的陷阱。 + +思考下面的程序。当debug变量设置为true时,main函数会将f函数的输出收集到一个bytes.Buffer类型中。 + +```go +const debug = true + +func main() { + var buf *bytes.Buffer + if debug { + buf = new(bytes.Buffer) // enable collection of output + } + f(buf) // NOTE: subtly incorrect! + if debug { + // ...use buf... + } +} + +// If out is non-nil, output will be written to it. +func f(out io.Writer) { + // ...do something... + if out != nil { + out.Write([]byte("done!\n")) + } +} +``` + +我们可能会预计当把变量debug设置为false时可以禁止对输出的收集,但是实际上在out.Write方法调用时程序发生了panic: + +```go +if out != nil { + out.Write([]byte("done!\n")) // panic: nil pointer dereference +} +``` + +当main函数调用函数f时,它给f函数的out参数赋了一个\*bytes.Buffer的空指针,所以out的动态值是nil。然而,它的动态类型是\*bytes.Buffer,意思就是out变量是一个包含空指针值的非空接口(如图7.5),所以防御性检查out!=nil的结果依然是true。 + +![](../images/ch7-05.png) + +动态分配机制依然决定(\*bytes.Buffer).Write的方法会被调用,但是这次的接收者的值是nil。对于一些如\*os.File的类型,nil是一个有效的接收者(§6.2.1),但是\*bytes.Buffer类型不在这些种类中。这个方法会被调用,但是当它尝试去获取缓冲区时会发生panic。 + +问题在于尽管一个nil的\*bytes.Buffer指针有实现这个接口的方法,它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(\*bytes.Buffer).Write方法的接收者非空的隐含先觉条件,所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer,因此可以避免一开始就将一个不完整的值赋值给这个接口: + +```go +var buf io.Writer +if debug { + buf = new(bytes.Buffer) // enable collection of output +} +f(buf) // OK +``` + +现在我们已经把接口值的技巧都讲完了,让我们来看更多的一些在Go标准库中的重要接口类型。在下面的三章中,我们会看到接口类型是怎样用在排序,web服务,错误处理中的。 diff --git a/ch7/ch7.md b/ch7/ch7.md index bc0c504..6982285 100644 --- a/ch7/ch7.md +++ b/ch7/ch7.md @@ -1,4 +1,4 @@ -# 第七章 接口 +# 第7章 接口 接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。 diff --git a/ch8/ch8-04.md b/ch8/ch8-04.md index 0ba5723..2701476 100644 --- a/ch8/ch8-04.md +++ b/ch8/ch8-04.md @@ -39,10 +39,283 @@ ch = make(chan int, 3) // buffered channel with capacity 3 我们将先讨论无缓存的channel,然后在8.4.4节讨论带缓存的channel。 -{% include "./ch8-04-1.md" %} +### 8.4.1. 不带缓存的Channels -{% include "./ch8-04-2.md" %} +一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。 + +基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒发送者goroutine之前(译注:*happens before*,这是Go语言并发内存模型的一个关键术语!)。 + +在讨论并发编程时,当我们说x事件在y事件之前发生(*happens before*),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。 + +当我们说x事件既不是在y事件之前发生也不是在y事件之后发生,我们就说x事件和y事件是并发的。这并不是意味着x事件和y事件就一定是同时发生的,我们只是不能确定这两个事件发生的先后顺序。在下一章中我们将看到,当两个goroutine并发访问了相同的变量时,我们有必要保证某些事件的执行顺序,以避免出现某些并发问题。 + +在8.3节的客户端程序,它在主goroutine中(译注:就是执行main函数的goroutine)将标准输入复制到server,因此当客户端程序关闭标准输入时,后台goroutine可能依然在工作。我们需要让主goroutine等待后台goroutine完成工作后再退出,我们使用了一个channel来同步两个goroutine: + +gopl.io/ch8/netcat3 +```Go +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + done := make(chan struct{}) + go func() { + io.Copy(os.Stdout, conn) // NOTE: ignoring errors + log.Println("done") + done <- struct{}{} // signal the main goroutine + }() + mustCopy(conn, os.Stdin) + conn.Close() + <-done // wait for background goroutine to finish +} +``` + +当用户关闭了标准输入,主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”消息。 + +基于channels发送消息有两个重要方面。首先每个消息都有一个值,但是有时候通讯的事实和发生的时刻也同样重要。当我们更希望强调通讯发生的时刻时,我们将它称为**消息事件**。有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用`struct{}`空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,`done <- 1`语句也比`done <- struct{}{}`更短。 + +**练习 8.3:** 在netcat3例子中,conn虽然是一个interface类型的值,但是其底层真实类型是`*net.TCPConn`,代表一个TCP连接。一个TCP连接有读和写两个部分,可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码,只关闭网络连接中写的部分,这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。(要在reverb2服务器也完成同样的功能是比较困难的;参考**练习 8.4**。) + + +### 8.4.2. 串联的Channels(Pipeline) + +Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来,如图8.1所示。 + +![](../images/ch8-01.png) + +第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。为了保持例子清晰,我们有意选择了非常简单的函数,当然三个goroutine的计算很简单,在现实中确实没有必要为如此简单的运算构建三个goroutine。 + +gopl.io/ch8/pipeline1 +```Go +func main() { + naturals := make(chan int) + squares := make(chan int) + + // Counter + go func() { + for x := 0; ; x++ { + naturals <- x + } + }() + + // Squarer + go func() { + for { + x := <-naturals + squares <- x * x + } + }() + + // Printer (in main goroutine) + for { + fmt.Println(<-squares) + } +} +``` + +如您所料,上面的程序将生成0、1、4、9、……形式的无穷数列。像这样的串联Channels的管道(Pipelines)可以用在需要长时间运行的服务中,每个长时间运行的goroutine可能会包含一个死循环,在不同goroutine的死循环内部使用串联的Channels来通信。但是,如果我们希望通过Channels只发送有限的数列该如何处理呢? + +如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现: + +```Go +close(naturals) +``` + +当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值。关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列,然后将它们发送给打印者goroutine。 + +没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应的channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel. + +```Go +// Squarer +go func() { + for { + x, ok := <-naturals + if !ok { + break // channel was closed and drained + } + squares <- x * x + } + close(squares) +}() +``` + +因为上面的语法是笨拙的,而且这种处理模式很常见,因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。 + +在下面的改进中,我们的计数器goroutine只生成100个含数字的序列,然后关闭naturals对应的channel,这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。(在一个更复杂的程序中,可以通过defer语句关闭对应的channel。)最后,主goroutine也可以正常终止循环并退出程序。 + +gopl.io/ch8/pipeline2 +```Go +func main() { + naturals := make(chan int) + squares := make(chan int) + + // Counter + go func() { + for x := 0; x < 100; x++ { + naturals <- x + } + close(naturals) + }() + + // Squarer + go func() { + for x := range naturals { + squares <- x * x + } + close(squares) + }() + + // Printer (in main goroutine) + for x := range squares { + fmt.Println(x) + } +} +``` + +其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的时候调用对应的Close方法来关闭文件。) + +试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制,我们将在8.9节讨论。 + + + +### 8.4.3. 单方向的Channel + +随着程序的增长,人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine,然后用两个channels来连接它们,它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法: + +```Go +func counter(out chan int) +func squarer(out, in chan int) +func printer(in chan int) +``` + +其中计算平方的squarer函数在两个串联Channels的中间,因此拥有两个channel类型的参数,一个用于输入一个用于输出。两个channel都拥有相同的类型,但是它们的使用方式相反:一个只用于接收,另一个只用于发送。参数的名字in和out已经明确表示了这个意图,但是并无法保证squarer函数向一个in参数对应的channel发送数据或者从一个out参数对应的channel接收数据。 + +这种场景是典型的。当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。 + +为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。类型`chan<- int`表示一个只发送int的channel,只能发送不能接收。相反,类型`<-chan int`表示一个只接收int的channel,只能接收不能发送。(箭头`<-`和关键字chan的相对位置表明了channel的方向。)这种限制将在编译期检测。 + +因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。 + +这是改进的版本,这一次参数使用了单方向channel类型: + +gopl.io/ch8/pipeline3 +```Go +func counter(out chan<- int) { + for x := 0; x < 100; x++ { + out <- x + } + close(out) +} + +func squarer(out chan<- int, in <-chan int) { + for v := range in { + out <- v * v + } + close(out) +} + +func printer(in <-chan int) { + for v := range in { + fmt.Println(v) + } +} + +func main() { + naturals := make(chan int) + squares := make(chan int) + go counter(naturals) + go squarer(squares, naturals) + printer(squares) +} +``` + +调用counter(naturals)时,naturals的类型将隐式地从chan int转换成chan<- int。调用printer(squares)也会导致相似的隐式转换,这一次是转换为`<-chan int`类型只接收型的channel。任何双向channel向单向channel变量的赋值操作都将导致该隐式转换。这里并没有反向转换的语法:也就是不能将一个类似`chan<- int`类型的单向型的channel转换为`chan int`类型的双向型的channel。 + + + +### 8.4.4. 带缓存的Channels + +带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。图8.2是ch变量对应的channel的图形表示形式。 + +```Go +ch = make(chan string, 3) +``` + +![](../images/ch8-02.png) + +向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。 + +我们可以在无阻塞的情况下连续向新创建的channel发送三个值: + +```Go +ch <- "A" +ch <- "B" +ch <- "C" +``` + +此刻,channel的内部缓存队列将是满的(图8.3),如果有第四个发送操作将发生阻塞。 + +![](../images/ch8-03.png) + +如果我们接收一个值, + +```Go +fmt.Println(<-ch) // "A" +``` + +那么channel的缓存队列将不是满的也不是空的(图8.4),因此对该channel执行的发送或接收操作都不会发生阻塞。通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。 + +![](../images/ch8-04.png) + +在某些特殊情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取: + +```Go +fmt.Println(cap(ch)) // "3" +``` + +同样,对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助。 + +```Go +fmt.Println(len(ch)) // "2" +``` + +在继续执行两次接收操作后channel内部的缓存队列将又成为空的,如果有第四个接收操作将发生阻塞: + +```Go +fmt.Println(<-ch) // "B" +fmt.Println(<-ch) // "C" +``` + +在这个例子中,发送和接收操作都发生在同一个goroutine中,但是在真实的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用,虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,如果没有其他goroutine从channel接收,发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列,使用slice就可以了。 + +下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。(顺便说一下,多个goroutines并发地向同一个channel发送数据,或从同一个channel接收数据都是常见的用法。) + +```Go +func mirroredQuery() string { + responses := make(chan string, 3) + go func() { responses <- request("asia.gopl.io") }() + go func() { responses <- request("europe.gopl.io") }() + go func() { responses <- request("americas.gopl.io") }() + return <-responses // return the quickest response +} + +func request(hostname string) (response string) { /* ... */ } +``` + +如果我们使用了无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。 + +关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。同样,即使我们知道将要发送到一个channel的信息的数量上限,创建一个对应容量大小的带缓存channel也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓存将导致程序死锁。 + +Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师,一个烘焙,一个上糖衣,还有一个将每个蛋糕传递到它下一个厨师的生产线。在狭小的厨房空间环境,每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它;这类似于在一个无缓存的channel上进行沟通。 + +如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕的制作中;这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近,那么其中大部分的传输工作将是迅速的,个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动,例如一个厨师可以短暂地休息,然后再加快赶上进度而不影响其他人。 + +另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。 + +生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如,如果第二阶段是需要精心制作的复杂操作,一个厨师可能无法跟上第一个厨师的进度,或者是无法满足第三阶段厨师的需求。要解决这个问题,我们可以再雇佣另一个厨师来帮助完成第二阶段的工作,他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。 + +我们没有太多的空间展示全部细节,但是gopl.io/ch8/cake包模拟了这个蛋糕店,可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试(§11.4) 。 -{% include "./ch8-04-3.md" %} -{% include "./ch8-04-4.md" %} diff --git a/ch8/ch8.md b/ch8/ch8.md index cc7811f..b059f5a 100644 --- a/ch8/ch8.md +++ b/ch8/ch8.md @@ -1,4 +1,4 @@ -# 第八章 Goroutines和Channels +# 第8章 Goroutines和Channels 并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的批处理问题——读取数据、计算、写输出,现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。 diff --git a/ch9/ch9-08.md b/ch9/ch9-08.md index 8986137..99c7281 100644 --- a/ch9/ch9-08.md +++ b/ch9/ch9-08.md @@ -2,10 +2,55 @@ 在上一章中我们说goroutine和操作系统的线程区别可以先忽略。尽管两者的区别实际上只是一个量的区别,但量变会引起质变的道理同样适用于goroutine和线程。现在正是我们来区分开两者的最佳时机。 -{% include "./ch9-08-1.md" %} +### 9.8.1. 动态栈 -{% include "./ch9-08-2.md" %} +每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费,比如对于我们用到的,一个只是用来WaitGroup之后关闭channel的goroutine来说。而对于go程序来说,同时创建成百上千个goroutine是非常普遍的,如果每一个goroutine都需要这么大的栈的话,那这么多的goroutine就不太可能了。除去大小的问题之外,固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率,允许创建更多的线程,并且可以允许更深的递归调用,不过这两者是没法同时兼备的。 -{% include "./ch9-08-3.md" %} +相反,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。一个goroutine的栈,和操作系统线程一样,会保存其活跃或挂起的函数调用的本地变量,但是和OS线程不太一样的是,一个goroutine的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB,比传统的固定大小的线程栈要大得多,尽管一般情况下,大多goroutine都不需要这么大的栈。 + +** 练习 9.4:** 创建一个流水线程序,支持用channel连接任意数量的goroutine,在跑爆内存之前,可以创建多少流水线阶段?一个变量通过整个流水线需要用多久?(这个练习题翻译不是很确定) + +### 9.8.2. Goroutine调度 + +OS线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度,所以从一个线程向另一个“移动”需要完整的上下文切换,也就是说,保存一个用户线程的状态到内存,恢复另一个线程的到寄存器,然后更新调度器的数据结构。这几步操作很慢,因为其局部性很差需要几次内存访问,并且会增加运行的cpu周期。 + +Go的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如m:n调度,因为其会在n个操作系统线程上多工(调度)m个goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的goroutine(译注:按程序独立)。 + +和操作系统的线程调度不同的是,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。 + +** 练习 9.5: ** 写一个有两个goroutine的程序,两个goroutine会向两个无buffer channel反复地发送ping-pong消息。这样的程序每秒可以支持多少次通信? + +### 9.8.3. GOMAXPROCS + +Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的,但是GOMAXPROCS并不需要将这几种情况计算在内。 + +你可以用GOMAXPROCS的环境变量来显式地控制这个参数,或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果,这个程序会无限打印0和1。 + + +```go +for { + go fmt.Print(0) + fmt.Print(1) +} + +$ GOMAXPROCS=1 go run hacker-cliché.go +111111111111111111110000000000000000000011111... + +$ GOMAXPROCS=2 go run hacker-cliché.go +010101010101010101011001100101011010010100110... +``` + +在第一次执行时,最多同时只能有一个goroutine被执行。初始情况下只有main goroutine被执行,所以会打印很多1。过了一段时间后,GO调度器会将其置为休眠,并唤醒另一个goroutine,这时候就开始打印很多0了,在打印的时候,goroutine是被调度到操作系统线程上的。在第二次执行时,我们使用了两个操作系统线程,所以两个goroutine可以一起被执行,以同样的频率交替打印0和1。我们必须强调的是goroutine的调度是受很多因子影响的,而runtime也是在不断地发展演进的,所以这里的你实际得到的结果可能会因为版本的不同而与我们运行的结果有所不同。 + +** 练习9.6:** 测试一下计算密集型的并发程序(练习8.5那样的)会被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少?你的电脑CPU有多少个核心? + +### 9.8.4. Goroutine没有ID号 + +在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份(id),并且这个身份信息可以以一个普通值的形式被很容易地获取到,典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易,只需要以线程的id作为key的一个map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不冲突。 + +goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之,由于thread-local storage总是会被滥用。比如说,一个web server是用一种支持tls的语言实现的,而非常普遍的是很多函数会去寻找HTTP请求的信息,这代表它们就是去其存储层(这个存储层有可能是tls)查找的。这就像是那些过分依赖全局变量的程序一样,会导致一种非健康的“距离外行为”,在这种行为下,一个函数的行为可能并不仅由自己的参数所决定,而是由其所运行在的线程所决定。因此,如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。 + +Go鼓励更为简单的模式,这种模式下参数(译注:外部显式参数和内部显式参数。tls 中的内容算是"外部"隐式参数)对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。 + +你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中,我们会回顾一些之前的实例和工具,支持我们写出更大规模的程序:如何将一个工程组织成一系列的包,如何获取,构建,测试,性能测试,剖析,写文档,并且将这些包分享出去。 -{% include "./ch9-08-4.md" %} diff --git a/ch9/ch9.md b/ch9/ch9.md index 8ce23f6..c0c044f 100644 --- a/ch9/ch9.md +++ b/ch9/ch9.md @@ -1,4 +1,4 @@ -# 第九章 基于共享变量的并发 +# 第9章 基于共享变量的并发 前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上回避了在写并发代码时必须处理的一些重要而且细微的问题。 diff --git a/css.png b/css.png new file mode 100644 index 0000000..5d8fa07 Binary files /dev/null and b/css.png differ diff --git a/index.md b/index.md new file mode 100644 index 0000000..ec077ee --- /dev/null +++ b/index.md @@ -0,0 +1,19 @@ +# Go语言圣经(中文版) + +Go语言圣经 [《The Go Programming Language》](http://gopl.io) 中文版本,仅供学习交流之用。对于希望学习CGO、Go汇编语言等高级用法的同学,我们推荐[《Go语言高级编程》](https://github.com/chai2010/advanced-go-programming-book)开源图书。如果希望深入学习Go语言语法树结构,可以参考[《Go语法树入门——开启自制编程语言和编译器之旅》](https://github.com/chai2010/go-ast-book)。如果想从头实现一个玩具Go语言可以参考[《从头实现µGo语言》](https://github.com/chai2010/ugo-compiler-book)。 + + +![](cover.jpg) + +- 项目主页:[https://github.com/gopl-zh](https://github.com/gopl-zh) +- 项目主页(旧):[http://github.com/golang-china/gopl-zh](http://github.com/golang-china/gopl-zh) +- 原版官网:[http://gopl.io](http://gopl.io) + + +译者信息: + +- 译者:柴树杉,Github [@chai2010](https://github.com/chai2010),Twitter [@chaishushan](https://twitter.com/chaishushan) +- 译者:Xargin, [https://github.com/cch123](https://github.com/cch123) +- 译者:CrazySssst +- 译者:foreversmart + diff --git a/js/bigPicture.js b/js/bigPicture.js new file mode 100644 index 0000000..41cdd1f --- /dev/null +++ b/js/bigPicture.js @@ -0,0 +1 @@ +var BigPicture=function(){var t,n,e,o,i,r,a,c,p,s,l,d,u,f,m,b,g,h,x,v,y,w,_,T,k,M,S,L,E,A,H,z,I,C=[],D={},O="appendChild",N="createElement",V="removeChild";function W(){var n=t.getBoundingClientRect();return"transform:translate3D("+(n.left-(e.clientWidth-n.width)/2)+"px, "+(n.top-(e.clientHeight-n.height)/2)+"px, 0) scale3D("+t.clientWidth/o.clientWidth+", "+t.clientHeight/o.clientHeight+", 0)"}function q(t){var n=A.length-1;if(!u){if(t>0&&E===n||t<0&&!E){if(!I.loop)return j(i,""),void setTimeout(j,9,i,"animation:"+(t>0?"bpl":"bpf")+" .3s;transition:transform .35s");E=t>0?-1:n+1}if([(E=Math.max(0,Math.min(E+t,n)))-1,E,E+1].forEach(function(t){if(t=Math.max(0,Math.min(t,n)),!D[t]){var e=A[t].src,o=document[N]("IMG");o.addEventListener("load",F.bind(null,e)),o.src=e,D[t]=o}}),D[E].complete)return B(t);u=1,j(m,"opacity:.4;"),e[O](m),D[E].onload=function(){y&&B(t)},D[E].onerror=function(){A[E]={error:"Error loading image"},y&&B(t)}}}function B(n){u&&(e[V](m),u=0);var r=A[E];if(r.error)alert(r.error);else{var a=e.querySelector("img:last-of-type");j(i=o=D[E],"animation:"+(n>0?"bpfl":"bpfr")+" .35s;transition:transform .35s"),j(a,"animation:"+(n>0?"bpfol":"bpfor")+" .35s both"),e[O](i),r.el&&(t=r.el)}H.innerHTML=E+1+"/"+A.length,X(A[E].caption),M&&M([i,A[E]])}function P(){var t,n,e=.95*window.innerHeight,o=.95*window.innerWidth,i=I.dimensions||[1920,1080],r=i[0],a=i[1],p=a/r;p>e/o?n=(t=Math.min(a,e))/p:t=(n=Math.min(r,o))*p,c.style.cssText+="width:"+n+"px;height:"+t+"px;"}function G(t){~[1,4].indexOf(o.readyState)?(U(),setTimeout(function(){o.play()},99)):o.error?U(t):f=setTimeout(G,35,t)}function R(n){I.noLoader||(n&&j(m,"top:"+t.offsetTop+"px;left:"+t.offsetLeft+"px;height:"+t.clientHeight+"px;width:"+t.clientWidth+"px"),t.parentElement[n?O:V](m),u=n)}function X(t){t&&(g.innerHTML=t),j(b,"opacity:"+(t?"1;pointer-events:auto":"0"))}function F(t){!~C.indexOf(t)&&C.push(t)}function U(t){if(u&&R(),T&&T(),"string"==typeof t)return $(),I.onError?I.onError():alert("Error: The requested "+t+" could not be loaded.");_&&F(s),o.style.cssText+=W(),j(e,"opacity:1;pointer-events:auto"),k=setTimeout(k,410),v=1,y=!!A,setTimeout(function(){o.style.cssText+="transition:transform .35s;transform:none",h&&setTimeout(X,250,h)},60)}function Y(t){var n=t?t.target:e,i=[b,x,r,a,g,L,S,m];n.blur(),w||~i.indexOf(n)||(o.style.cssText+=W(),j(e,"pointer-events:auto"),setTimeout($,350),clearTimeout(k),v=0,w=1)}function $(){if((o===c?p:o).removeAttribute("src"),document.body[V](e),e[V](o),j(e,""),j(o,""),X(0),y){for(var t=e.querySelectorAll("img"),n=0;n',n}function d(t,n){var e=document[N]("button");return e.className="bp-lr",e.innerHTML='',j(e,n),e.onclick=function(n){n.stopPropagation(),q(t)},e}var f=document[N]("STYLE");f.innerHTML="#bp_caption,#bp_container{bottom:0;left:0;right:0;position:fixed;opacity:0}#bp_container>*,#bp_loader{position:absolute;right:0;z-index:10}#bp_container,#bp_caption,#bp_container svg{pointer-events:none}#bp_container{top:0;z-index:9999;background:rgba(0,0,0,.7);opacity:0;transition:opacity .35s}#bp_loader{top:0;left:0;bottom:0;display:flex;align-items:center;cursor:wait;background:0;z-index:9}#bp_loader svg{width:50%;max-width:300px;max-height:50%;margin:auto;animation:bpturn 1s infinite linear}#bp_aud,#bp_container img,#bp_sv,#bp_vid{user-select:none;max-height:96%;max-width:96%;top:0;bottom:0;left:0;margin:auto;box-shadow:0 0 3em rgba(0,0,0,.4);z-index:-1}#bp_sv{background:#111}#bp_sv svg{width:66px}#bp_caption{font-size:.9em;padding:1.3em;background:rgba(15,15,15,.94);color:#fff;text-align:center;transition:opacity .3s}#bp_aud{width:650px;top:calc(50% - 20px);bottom:auto;box-shadow:none}#bp_count{left:0;right:auto;padding:14px;color:rgba(255,255,255,.7);font-size:22px;cursor:default}#bp_container button{position:absolute;border:0;outline:0;background:0;cursor:pointer;transition:all .1s}#bp_container>.bp-x{padding:0;height:41px;width:41px;border-radius:100%;top:8px;right:14px;opacity:.8;line-height:1}#bp_container>.bp-x:focus,#bp_container>.bp-x:hover{background:rgba(255,255,255,.2)}.bp-x svg,.bp-xc svg{height:21px;width:20px;fill:#fff;vertical-align:top;}.bp-xc svg{width:16px}#bp_container .bp-xc{left:2%;bottom:100%;padding:9px 20px 7px;background:#d04444;border-radius:2px 2px 0 0;opacity:.85}#bp_container .bp-xc:focus,#bp_container .bp-xc:hover{opacity:1}.bp-lr{top:50%;top:calc(50% - 130px);padding:99px 0;width:6%;background:0;border:0;opacity:.4;transition:opacity .1s}.bp-lr:focus,.bp-lr:hover{opacity:.8}@keyframes bpf{50%{transform:translatex(15px)}100%{transform:none}}@keyframes bpl{50%{transform:translatex(-15px)}100%{transform:none}}@keyframes bpfl{0%{opacity:0;transform:translatex(70px)}100%{opacity:1;transform:none}}@keyframes bpfr{0%{opacity:0;transform:translatex(-70px)}100%{opacity:1;transform:none}}@keyframes bpfol{0%{opacity:1;transform:none}100%{opacity:0;transform:translatex(-70px)}}@keyframes bpfor{0%{opacity:1;transform:none}100%{opacity:0;transform:translatex(70px)}}@keyframes bpturn{0%{transform:none}100%{transform:rotate(360deg)}}@media (max-width:600px){.bp-lr{font-size:15vw}}",document.head[O](f),(e=document[N]("DIV")).id="bp_container",e.onclick=Y,l=s("bp-x"),e[O](l),"ontouchstart"in window&&(z=1,e.ontouchstart=function(n){var e=n.changedTouches;t=e[0].pageX},e.ontouchmove=function(t){t.preventDefault()},e.ontouchend=function(n){var e=n.changedTouches;if(y){var o=e[0].pageX-t;o<-30&&q(1),o>30&&q(-1)}}),i=document[N]("IMG"),(r=document[N]("VIDEO")).id="bp_vid",r.setAttribute("playsinline",1),r.controls=1,r.loop=1,(a=document[N]("audio")).id="bp_aud",a.controls=1,a.loop=1,(H=document[N]("span")).id="bp_count",(b=document[N]("DIV")).id="bp_caption",(x=s("bp-xc")).onclick=X.bind(null,0),b[O](x),g=document[N]("SPAN"),b[O](g),e[O](b),S=d(1,"transform:scalex(-1)"),L=d(-1,"left:0;right:auto"),(m=document[N]("DIV")).id="bp_loader",m.innerHTML='',(c=document[N]("DIV")).id="bp_sv",(p=document[N]("IFRAME")).setAttribute("allowfullscreen",1),p.allow="autoplay; fullscreen",p.onload=function(){return c[V](m)},j(p,"border:0;position:absolute;height:100%;width:100%;left:0;top:0"),c[O](p),i.onload=U,i.onerror=U.bind(null,"image"),window.addEventListener("resize",function(){y||u&&R(1),o===c&&P()}),document.addEventListener("keyup",function(t){var n=t.keyCode;27===n&&v&&Y(),y&&(39===n&&q(1),37===n&&q(-1),38===n&&q(10),40===n&&q(-10))}),document.addEventListener("keydown",function(t){y&&~[37,38,39,40].indexOf(t.keyCode)&&t.preventDefault()}),document.addEventListener("focus",function(t){v&&!e.contains(t.target)&&(t.stopPropagation(),l.focus())},1),n=1}(),u&&(clearTimeout(f),$()),I=w,d=w.ytSrc||w.vimeoSrc,T=w.animationStart,k=w.animationEnd,M=w.onChangeImage,_=0,h=(t=w.el).getAttribute("data-caption"),w.gallery?function(n,r){var a=I.galleryAttribute||"data-bp";if(Array.isArray(n))A=n,h=n[E=r||0].caption;else{var c=(A=[].slice.call("string"==typeof n?document.querySelectorAll(n+" ["+a+"]"):n)).indexOf(t);E=0===r||r?r:-1!==c?c:0,A=A.map(function(t){return{el:t,src:t.getAttribute(a),caption:t.getAttribute("data-caption")}})}_=1,!~C.indexOf(s=A[E].src)&&R(1),A.length>1?(e[O](H),H.innerHTML=E+1+"/"+A.length,z||(e[O](S),e[O](L))):A=0,(o=i).src=s}(w.gallery,w.position):d||w.iframeSrc?(o=c,I.ytSrc?W="https://www.youtube.com/embed/"+d+"?html5=1&rel=0&playsinline=1&autoplay=1":I.vimeoSrc?W="https://player.vimeo.com/video/"+d+"?autoplay=1":I.iframeSrc&&(W=I.iframeSrc),j(m,""),c[O](m),p.src=W,P(),setTimeout(U,9)):w.imgSrc?(_=1,!~C.indexOf(s=w.imgSrc)&&R(1),(o=i).src=s):w.audio?(R(1),(o=a).src=w.audio,G("audio file")):w.vidSrc?(R(1),w.dimensions&&j(r,"width:"+w.dimensions[0]+"px"),D=w.vidSrc,Array.isArray(D)?(o=r.cloneNode(),D.forEach(function(t){var n=document[N]("SOURCE");n.src=t,n.type="video/"+t.match(/.(\w+)$/)[1],o[O](n)})):(o=r).src=D,G("video")):(o=i).src="IMG"===t.tagName?t.src:window.getComputedStyle(t).backgroundImage.replace(/^url|[(|)|'|"]/g,""),e[O](o),document.body[O](e),{close:Y,next:function(){return q(1)},prev:function(){return q(-1)}};var W}}(); diff --git a/js/custom.js b/js/custom.js new file mode 100644 index 0000000..3a5ca26 --- /dev/null +++ b/js/custom.js @@ -0,0 +1,146 @@ +// https://giscus.app + +const data_repo = "gopl-zh/gopl-zh.github.com"; +const data_repo_id = "MDEwOlJlcG9zaXRvcnk2MTUzMTQ2Mw=="; +const data_category = "General"; +const data_category_id = "DIC_kwDOA6rlR84CQnJW"; + +var initAll = function () { + var path = window.location.pathname; + if (path.endsWith("/print.html")) { + return; + } + + var images = document.querySelectorAll("main img") + Array.prototype.forEach.call(images, function (img) { + img.addEventListener("click", function () { + BigPicture({ + el: img, + }); + }); + }); + + // Un-active everything when you click it + Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function (el) { + el.addEventHandler("click", function () { + Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function (el) { + el.classList.remove("active"); + }); + el.classList.add("active"); + }); + }); + + var updateFunction = function () { + var id = null; + var elements = document.getElementsByClassName("header"); + Array.prototype.forEach.call(elements, function (el) { + if (window.pageYOffset >= el.offsetTop) { + id = el; + } + }); + + Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function (el) { + el.classList.remove("active"); + }); + + Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function (el) { + if (id == null) { + return; + } + if (id.href.localeCompare(el.href) == 0) { + el.classList.add("active"); + } + }); + }; + + var pagetoc = document.getElementsByClassName("pagetoc")[0]; + var elements = document.getElementsByClassName("header"); + Array.prototype.forEach.call(elements, function (el) { + var link = document.createElement("a"); + + // Indent shows hierarchy + var indent = ""; + switch (el.parentElement.tagName) { + case "H1": + return; + case "H3": + indent = "20px"; + break; + case "H4": + indent = "40px"; + break; + default: + break; + } + + link.appendChild(document.createTextNode(el.text)); + link.style.paddingLeft = indent; + link.href = el.href; + pagetoc.appendChild(link); + }); + updateFunction.call(); + + // Handle active elements on scroll + window.addEventListener("scroll", updateFunction); + + document.getElementById("theme-list").addEventListener("click", function (e) { + var iframe = document.querySelector('.giscus-frame'); + if (!iframe) return; + var theme; + if (e.target.className === "theme") { + theme = e.target.id; + } else { + return; + } + + // 若当前 mdbook 主题不是 Light 或 Rust ,则将 giscuz 主题设置为 transparent_dark + var giscusTheme = "light" + if (theme != "light" && theme != "rust") { + giscusTheme = "transparent_dark"; + } + + var msg = { + setConfig: { + theme: giscusTheme + } + }; + iframe.contentWindow.postMessage({ giscus: msg }, 'https://giscus.app'); + }); + + pagePath = pagePath.replace("index.md", ""); + pagePath = pagePath.replace(".md", ""); + if (pagePath.length > 0) { + if (pagePath.charAt(pagePath.length-1) == "/"){ + pagePath = pagePath.substring(0, pagePath.length-1); + } + }else { + pagePath = "index"; + } + + var giscusTheme = "light"; + const themeClass = document.getElementsByTagName("html")[0].className; + if (themeClass.indexOf("light") == -1 && themeClass.indexOf("rust") == -1) { + giscusTheme = "transparent_dark"; + } + + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "https://giscus.app/client.js"; + script.async = true; + script.crossOrigin = "anonymous"; + script.setAttribute("data-repo", data_repo); + script.setAttribute("data-repo-id", data_repo_id); + script.setAttribute("data-category", data_category); + script.setAttribute("data-category-id", data_category_id); + script.setAttribute("data-mapping", "specific"); + script.setAttribute("data-term", pagePath); + script.setAttribute("data-reactions-enabled", "1"); + script.setAttribute("data-emit-metadata", "0"); + script.setAttribute("data-input-position", "top"); + script.setAttribute("data-theme", giscusTheme); + script.setAttribute("data-lang", "zh-CN"); + script.setAttribute("data-loading", "lazy"); + document.getElementById("giscus-container").appendChild(script); +}; + +window.addEventListener('load', initAll); diff --git a/preface.md b/preface.md index 450805e..64e133e 100644 --- a/preface.md +++ b/preface.md @@ -1,88 +1,109 @@ -# Go语言圣经(中文版) - -Go语言圣经 [《The Go Programming Language》](http://gopl.io) 中文版本,仅供学习交流之用。对于希望学习CGO、Go汇编语言等高级用法的同学,我们推荐[《Go语言高级编程》](https://github.com/chai2010/advanced-go-programming-book)开源图书。如果希望深入学习Go语言语法树结构,可以参考[《Go语法树入门——开启自制编程语言和编译器之旅》](https://github.com/chai2010/go-ast-book)。如果想从头实现一个玩具Go语言可以参考[《从头实现µGo语言》](https://github.com/chai2010/ugo-compiler-book)。 - -[![](cover_middle.jpg)](https://github.com/golang-china/gopl-zh) - -- 项目主页:http://github.com/golang-china/gopl-zh -- 原版官网:http://gopl.io - - -译者信息: - -- 译者:柴树杉,Github [@chai2010](https://github.com/chai2010),Twitter [@chaishushan](https://twitter.com/chaishushan) -- 译者:Xargin, https://github.com/cch123 -- 译者:CrazySssst -- 译者:foreversmart - - -## 关注微信公众号光谷码农和 TechPaper - - - - - - -
- - - -
- -## 在线预览 - -- https://docs.hacknode.org/gopl-zh/ -- https://books.studygolang.com/gopl-zh/ -- https://wizardforcel.gitbooks.io/gopl-zh/ -- https://book.itsfun.top/gopl-zh/ #每夜自动构建 - -{% include "./version.md" %} - -------- - -# 译者序 - -在上个世纪70年代,贝尔实验室的[Ken Thompson][KenThompson]和[Dennis M. Ritchie][DennisRitchie]合作发明了[UNIX](http://doc.cat-v.org/unix/)操作系统,同时[Dennis M. Ritchie][DennisRitchie]为了解决[UNIX](http://doc.cat-v.org/unix/)系统的移植性问题而发明了C语言,贝尔实验室的[UNIX](http://doc.cat-v.org/unix/)和C语言两大发明奠定了整个现代IT行业最重要的软件基础(目前的三大桌面操作系统的中[Linux](http://www.linux.org/)和[Mac OS X](http://www.apple.com/cn/osx/)都是源于[UNIX]()系统,两大移动平台的操作系统iOS和Android也都是源于[UNIX](http://doc.cat-v.org/unix/)系统。C系家族的编程语言占据统治地位达几十年之久)。在[UNIX]()和C语言发明40年之后,目前已经在Google工作的[Ken Thompson](http://genius.cat-v.org/ken-thompson/)和[Rob Pike](http://genius.cat-v.org/rob-pike/)(他们在贝尔实验室时就是同事)、还有[Robert Griesemer](http://research.google.com/pubs/author96.html)(设计了V8引擎和HotSpot虚拟机)一起合作,为了解决在21世纪多核和网络化环境下越来越复杂的编程问题而发明了Go语言。从Go语言库早期代码库日志可以看出它的演化历程(Git用`git log --before={2008-03-03} --reverse`命令查看): - -![](./images/go-log04.png) - -从早期提交日志中也可以看出,Go语言是从[Ken Thompson](http://genius.cat-v.org/ken-thompson/)发明的B语言、[Dennis M. Ritchie](http://genius.cat-v.org/dennis-ritchie/)发明的C语言逐步演化过来的,是C语言家族的成员,因此很多人将Go语言称为21世纪的C语言。纵观这几年来的发展趋势,Go语言已经成为云计算、云存储时代最重要的基础编程语言。 - -在C语言发明之后约5年的时间之后(1978年),[Brian W. Kernighan](http://www.cs.princeton.edu/~bwk/)和[Dennis M. Ritchie](http://genius.cat-v.org/dennis-ritchie/)合作编写出版了C语言方面的经典教材《[The C Programming Language](http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html)》,该书被誉为C语言程序员的圣经,作者也被大家亲切地称为[K&R](https://en.wikipedia.org/wiki/K%26R)。同样在Go语言正式发布(2009年)约5年之后(2014年开始写作,2015年出版),由Go语言核心团队成员[Alan A. A. Donovan](https://github.com/adonovan)和[K&R](https://en.wikipedia.org/wiki/K%26R)中的[Brian W. Kernighan](http://www.cs.princeton.edu/~bwk/)合作编写了Go语言方面的经典教材《[The Go Programming Language](http://gopl.io)》。Go语言被誉为21世纪的C语言,如果说[K&R](https://en.wikipedia.org/wiki/K%26R)所著的是圣经的旧约,那么D&K所著的必将成为圣经的新约。该书介绍了Go语言几乎全部特性,并且随着语言的深入层层递进,对每个细节都解读得非常细致,每一节内容都精彩不容错过,是广大Gopher的必读书目。大部分Go语言核心团队的成员都参与了该书校对工作,因此该书的质量是可以完全放心的。 - -同时,单凭阅读和学习其语法结构并不能真正地掌握一门编程语言,必须进行足够多的编程实践——亲自编写一些程序并研究学习别人写的程序。要从利用Go语言良好的特性使得程序模块化,充分利用Go的标准函数库以Go语言自己的风格来编写程序。书中包含了上百个精心挑选的习题,希望大家能先用自己的方式尝试完成习题,然后再参考官方给出的解决方案。 - -该书英文版约从2015年10月开始公开发售,其中日文版本最早参与翻译和审校(参考致谢部分)。在2015年10月,我们并不知道中文版是否会及时引进、将由哪家出版社引进、引进将由何人来翻译、何时能出版,这些信息都成了一个秘密。中国的Go语言社区是全球最大的Go语言社区,我们从一开始就始终紧跟着Go语言的发展脚步。我们应该也完全有能力以中国Go语言社区的力量同步完成Go语言圣经中文版的翻译工作。与此同时,国内有很多Go语言爱好者也在积极关注该书(本人也在第一时间购买了纸质版本,[亚马逊价格314人民币](http://www.amazon.cn/The-Go-Programming-Language-Donovan-Alan-A-A/dp/0134190440/)。补充:国内也即将出版英文版,[价格79元](http://product.china-pub.com/4912464))。为了Go语言的学习和交流,大家决定合作免费翻译该书。 - -翻译工作从2015年11月20日前后开始,到2016年1月底初步完成,前后历时约2个月时间(在其它语言版本中,全球第一个完成翻译的,基本做到和原版同步)。其中,[chai2010](https://github.com/chai2010)翻译了前言、第2 ~ 4章、第10 ~ 13章,[Xargin](https://github.com/cch123)翻译了第1章、第6章、第8 ~ 9章,[CrazySssst](https://github.com/CrazySssst)翻译了第5章,[foreversmart](https://github.com/foreversmart)翻译了第7章,大家共同参与了基本的校验工作,还有其他一些朋友提供了积极的反馈建议。如果大家还有任何问题或建议,可以直接到中文版项目页面提交[Issue](https://github.com/golang-china/gopl-zh/issues),如果发现英文版原文在[勘误](http://www.gopl.io/errata.html)中未提到的任何错误,可以直接去[英文版项目](https://github.com/adonovan/gopl.io/)提交。 - -最后,希望这本书能够帮助大家用Go语言快乐地编程。 - -2016年 1月 于 武汉 - -------- - # 前言 -*“Go是一个开源的编程语言,它很容易用于构建简单、可靠和高效的软件。”(摘自Go语言官方网站:http://golang.org )* +## Go语言起源 -Go语言由来自Google公司的[Robert Griesemer](http://research.google.com/pubs/author96.html),[Rob Pike](http://genius.cat-v.org/rob-pike/)和[Ken Thompson](http://genius.cat-v.org/ken-thompson/)三位大牛于2007年9月开始设计和实现,然后于2009年的11月对外正式发布(译注:关于Go语言的创世纪过程请参考 http://talks.golang.org/2015/how-go-was-made.slide )。语言及其配套工具的设计目标是具有表达力,高效的编译和执行效率,有效地编写高效和健壮的程序。 +编程语言的演化跟生物物种的演化类似,一个成功的编程语言的后代一般都会继承它们祖先的优点;当然有时多种语言杂合也可能会产生令人惊讶的特性;还有一些激进的新特性可能并没有先例。通过观察这些影响,我们可以学到为什么一门语言是这样子的,它已经适应了怎样的环境。 -Go语言有着和C语言类似的语法外表,和C语言一样是专业程序员的必备工具,可以用最小的代价获得最大的战果。 -但是它不仅仅是一个更新的C语言。它还从其他语言借鉴了很多好的想法,同时避免引入过度的复杂性。 -Go语言中和并发编程相关的特性是全新的也是有效的,同时对数据抽象和面向对象编程的支持也很灵活。 -Go语言同时还集成了自动垃圾收集技术用于更好地管理内存。 +下图展示了有哪些早期的编程语言对Go语言的设计产生了重要影响。 -Go语言尤其适合编写网络服务相关基础设施,同时也适合开发一些工具软件和系统软件。 -但是Go语言确实是一个通用的编程语言,它也可以用在图形图像驱动编程、移动应用程序开发 -和机器学习等诸多领域。目前Go语言已经成为受欢迎的作为无类型的脚本语言的替代者: -因为Go编写的程序通常比脚本语言运行的更快也更安全,而且很少会发生意外的类型错误。 +![](../images/ch0-01.png) -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语言编写的程序无需修改就可以运行在上面这些环境。 +Go语言有时候被描述为“C类似语言”,或者是“21世纪的C语言”。Go从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。 -本书是为了帮助你开始以有效的方式使用Go语言,充分利用语言本身的特性和自带的标准库去编写清晰地道的Go程序。 +但是在Go语言的家族树中还有其它的祖先。其中一个有影响力的分支来自[Niklaus Wirth](https://en.wikipedia.org/wiki/Niklaus_Wirth)所设计的[Pascal][Pascal]语言。然后[Modula-2][Modula-2]语言激发了包的概念。然后[Oberon][Oberon]语言摒弃了模块接口文件和模块实现文件之间的区别。第二代的[Oberon-2][Oberon-2]语言直接影响了包的导入和声明的语法,还有[Oberon][Oberon]语言的面向对象特性所提供的方法的声明语法等。 + +Go语言的另一支祖先,带来了Go语言区别其他语言的重要特性,灵感来自于贝尔实验室的[Tony Hoare](https://en.wikipedia.org/wiki/Tony_Hoare)于1978年发表的鲜为外界所知的关于并发研究的基础文献 *顺序通信进程* ( *[communicating sequential processes][CSP]* ,缩写为[CSP][CSP]。在[CSP][CSP]中,程序是一组中间没有共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。不过[Tony Hoare](https://en.wikipedia.org/wiki/Tony_Hoare)的[CSP][CSP]只是一个用于描述并发性基本概念的描述语言,并不是一个可以编写可执行程序的通用编程语言。 + +接下来,Rob Pike和其他人开始不断尝试将[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)引入实际的编程语言中。他们第一次尝试引入[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)特性的编程语言叫[Squeak](http://doc.cat-v.org/bell_labs/squeak/)(老鼠间交流的语言),是一个提供鼠标和键盘事件处理的编程语言,它的管道是静态创建的。然后是改进版的[Newsqueak](http://doc.cat-v.org/bell_labs/squeak/)语言,提供了类似C语言语句和表达式的语法和类似[Pascal][Pascal]语言的推导语法。Newsqueak是一个带垃圾回收的纯函数式语言,它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建的,属于第一类值,可以保存到变量中。 + +在Plan9操作系统中,这些优秀的想法被吸收到了一个叫[Alef][Alef]的编程语言中。Alef试图将Newsqueak语言改造为系统编程语言,但是因为缺少垃圾回收机制而导致并发编程很痛苦。(译注:在Alef之后还有一个叫[Limbo][Limbo]的编程语言,Go语言从其中借鉴了很多特性。 具体请参考Pike的讲稿:http://talks.golang.org/2012/concurrency.slide#9 ) + +Go语言的其他的一些特性零散地来自于其他一些编程语言;比如iota语法是从[APL][APL]语言借鉴,词法作用域与嵌套函数来自于[Scheme][Scheme]语言(和其他很多语言)。当然,我们也可以从Go中发现很多创新的设计。比如Go语言的切片为动态数组提供了有效的随机存取的性能,这可能会让人联想到链表的底层的共享机制。还有Go语言新发明的defer语句。 + +## Go语言项目 + +所有的编程语言都反映了语言设计者对编程哲学的反思,通常包括之前的语言所暴露的一些不足地方的改进。Go项目是在Google公司维护超级复杂的几个软件系统遇到的一些问题的反思(但是这类问题绝不是Google公司所特有的)。 + +正如[Rob Pike](http://genius.cat-v.org/rob-pike/)所说,“软件的复杂性是乘法级相关的”,通过增加一个部分的复杂性来修复问题通常将慢慢地增加其他部分的复杂性。通过增加功能、选项和配置是修复问题的最快的途径,但是这很容易让人忘记简洁的内涵,即从长远来看,简洁依然是好软件的关键因素。 + +简洁的设计需要在工作开始的时候舍弃不必要的想法,并且在软件的生命周期内严格区别好的改变和坏的改变。通过足够的努力,一个好的改变可以在不破坏原有完整概念的前提下保持自适应,正如[Fred Brooks](http://www.cs.unc.edu/~brooks/)所说的“概念完整性”;而一个坏的改变则不能达到这个效果,它们仅仅是通过肤浅的和简单的妥协来破坏原有设计的一致性。只有通过简洁的设计,才能让一个系统保持稳定、安全和持续的进化。 + +Go项目包括编程语言本身,附带了相关的工具和标准库,最后但并非代表不重要的是,关于简洁编程哲学的宣言。就事后诸葛的角度来看,Go语言的这些地方都做的还不错:拥有自动垃圾回收、一个包系统、函数作为一等公民、词法作用域、系统调用接口、只读的UTF8字符串等。但是Go语言本身只有很少的特性,也不太可能添加太多的特性。例如,它没有隐式的数值转换,没有构造函数和析构函数,没有运算符重载,没有默认参数,也没有继承,没有泛型,没有异常,没有宏,没有函数修饰,更没有线程局部存储。但是,语言本身是成熟和稳定的,而且承诺保证向后兼容:用之前的Go语言编写程序可以用新版本的Go语言编译器和标准库直接构建而不需要修改代码。 + +Go语言有足够的类型系统以避免动态语言中那些粗心的类型错误,但是,Go语言的类型系统相比传统的强类型语言又要简洁很多。虽然,有时候这会导致一个“无类型”的抽象类型概念,但是Go语言程序员并不需要像C++或Haskell程序员那样纠结于具体类型的安全属性。在实践中,Go语言简洁的类型系统给程序员带来了更多的安全性和更好的运行时性能。 + +Go语言鼓励当代计算机系统设计的原则,特别是局部的重要性。它的内置数据类型和大多数的准库数据结构都经过精心设计而避免显式的初始化或隐式的构造函数,因为很少的内存分配和内存初始化代码被隐藏在库代码中了。Go语言的聚合类型(结构体和数组)可以直接操作它们的元素,只需要更少的存储空间、更少的内存写操作,而且指针操作比其他间接操作的语言也更有效率。由于现代计算机是一个并行的机器,Go语言提供了基于CSP的并发特性支持。Go语言的动态栈使得轻量级线程goroutine的初始栈可以很小,因此,创建一个goroutine的代价很小,创建百万级的goroutine完全是可行的。 + +Go语言的标准库(通常被称为语言自带的电池),提供了清晰的构建模块和公共接口,包含I/O操作、文本处理、图像、密码学、网络和分布式应用程序等,并支持许多标准化的文件格式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释,从而最终简化程序的逻辑,而且,每个Go程序结构都是如此的相似,因此,Go程序也很容易学习。使用Go语言自带工具构建Go语言项目只需要使用文件名和标识符名称,一个偶尔的特殊注释来确定所有的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等;Go语言源代码本身就包含了构建规范。 + +## 本书的组织 + +我们假设你已经有一种或多种其他编程语言的使用经历,不管是类似C、C++或Java的编译型语言,还是类似Python、Ruby、JavaScript的脚本语言,因此我们不会像对完全的编程语言初学者那样解释所有的细节。因为,Go语言的变量、常量、表达式、控制流和函数等基本语法也是类似的。 + +第一章包含了本教程的基本结构,通过十几个程序介绍了用Go语言如何实现类似读写文件、文本格式化、创建图像、网络客户端和服务器通讯等日常工作。 + +第二章描述了Go语言程序的基本元素结构、变量、新类型定义、包和文件、以及作用域等概念。第三章讨论了数字、布尔值、字符串和常量,并演示了如何显示和处理Unicode字符。第四章描述了复合类型,从简单的数组、字典、切片到动态列表。第五章涵盖了函数,并讨论了错误处理、panic和recover,还有defer语句。 + +第一章到第五章是基础部分,主流命令式编程语言这部分都类似。个别之处,Go语言有自己特色的语法和风格,但是大多数程序员能很快适应。其余章节是Go语言特有的:方法、接口、并发、包、测试和反射等语言特性。 + +Go语言的面向对象机制与一般语言不同。它没有类层次结构,甚至可以说没有类;仅仅通过组合(而不是继承)简单的对象来构建复杂的对象。方法不仅可以定义在结构体上,而且,可以定义在任何用户自定义的类型上;并且,具体类型和抽象类型(接口)之间的关系是隐式的,所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论,接口在第七章讨论。 + +第八章讨论了基于顺序通信进程(CSP)概念的并发编程,使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。 + +第十章描述了包机制和包的组织结构。这一章还展示了如何有效地利用Go自带的工具,使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。 + +第十一章讨论了单元测试,Go语言的工具和标准库中集成了轻量级的测试功能,避免了强大但复杂的测试框架。测试库提供了一些基本构件,必要时可以用来构建复杂的测试构件。 + +第十二章讨论了反射,一种程序在运行期间审视自己的能力。反射是一个强大的编程工具,不过要谨慎地使用;这一章利用反射机制实现一些重要的Go语言库函数,展示了反射的强大用法。第十三章解释了底层编程的细节,在必要时,可以使用unsafe包绕过Go语言安全的类型系统。 + +每一章都有一些练习题,你可以用来测试你对Go的理解,你也可以探讨书中这些例子的扩展和替代。 + +书中所有的代码都可以从 http://gopl.io 上的Git仓库下载。go get命令根据每个例子的导入路径智能地获取、构建并安装。只需要选择一个目录作为工作空间,然后将GOPATH环境变量设置为该路径。 + +必要时,Go语言工具会创建目录。例如: + +``` +$ export GOPATH=$HOME/gobook # 选择工作目录 +$ go get gopl.io/ch1/helloworld # 获取/编译/安装 +$ $GOPATH/bin/helloworld # 运行程序 +Hello, 世界 # 这是中文 +``` + +运行这些例子需要安装Go1.5以上的版本。 + +``` +$ go version +go version go1.5 linux/amd64 +``` + +如果使用其他的操作系统,请参考 https://golang.org/doc/install 提供的说明安装。 + + +## 更多的信息 + +最佳的帮助信息来自Go语言的官方网站,https://golang.org ,它提供了完善的参考文档,包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程,还有各种各样的在线文本资源和视频资源,它们是本书最有价值的补充。Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章,包括当前语言的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息(译注: http://talks.golang.org/ 包含了官方收录的各种报告的讲稿)。 + +在线访问的一个有价值的地方是可以从web页面运行Go语言的程序(而纸质书则没有这么便利了)。这个功能由来自 https://play.golang.org 的 Go Playground 提供,并且可以方便地嵌入到其他页面中,例如 https://golang.org 的主页,或 godoc 提供的文档页面中。 + +Playground可以简单的通过执行一个小程序来测试对语法、语义和对程序库的理解,类似其他很多语言提供的REPL即时运行的工具。同时它可以生成对应的url,非常适合共享Go语言代码片段,汇报bug或提供反馈意见等。 + +基于 Playground 构建的 Go Tour,https://tour.golang.org ,是一个系列的Go语言入门教程,它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。 + +当然,Playground 和 Tour 也有一些限制,它们只能导入标准库,而且因为安全的原因对一些网络库做了限制。如果要在编译和运行时需要访问互联网,对于一些更复杂的实验,你可能需要在自己的电脑上构建并运行程序。幸运的是下载Go语言的过程很简单,从 https://golang.org 下载安装包应该不超过几分钟(译注:感谢伟大的长城,让大陆的Gopher们都学会了自己打洞的基本生活技能,下载时间可能会因为洞的大小等因素从几分钟到几天或更久),然后就可以在自己电脑上编写和运行Go程序了。 + +Go语言是一个开源项目,你可以在 https://golang.org/pkg 阅读标准库中任意函数和类型的实现代码,和下载安装包的代码完全一致。这样,你可以知道很多函数是如何工作的, 通过挖掘找出一些答案的细节,或者仅仅是出于欣赏专业级Go代码。 + +## 致谢 + +[Rob Pike](http://genius.cat-v.org/rob-pike/)和[Russ Cox](http://research.swtch.com/),以及很多其他Go团队的核心成员多次仔细阅读了本书的手稿,他们对本书的组织结构和表述用词等给出了很多宝贵的建议。在准备日文版翻译的时候,Yoshiki Shibata更是仔细地审阅了本书的每个部分,及时发现了诸多英文和代码的错误。我们非常感谢本书的每一位审阅者,并感谢对本书给出了重要的建议的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。 + +我们还感谢Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma(译注:中国人,Go团队成员。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao(译注:好像是陶哲轩的兄弟)以及Howard Trickey给出的许多有价值的建议。我们还要感谢David Brailsford和Raph Levien关于类型设置的建议。 + +我们从来自Addison-Wesley的编辑Greg Doench收到了很多帮助,从最开始就得到了越来越多的帮助。来自AW生产团队的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood,感谢你们的热心帮助。 + +[Alan Donovan](https://github.com/adonovan)特别感谢:Sameer Ajmani、Chris Demetriou、Walt Drummond和Google公司的Reid Tatge允许他有充裕的时间去写本书;感谢Stephen Donovan的建议和始终如一的鼓励,以及他的妻子Leila Kazemi并没有让他为了家庭琐事而分心,并热情坚定地支持这个项目。 + +[Brian Kernighan](http://www.cs.princeton.edu/~bwk/)特别感谢:朋友和同事对他的耐心和宽容,让他慢慢地梳理本书的写作思路。同时感谢他的妻子Meg和其他很多朋友对他写作事业的支持。 + +2015年 10月 于 纽约 -{% include "./links.md" %} diff --git a/style.css b/style.css new file mode 100644 index 0000000..b260507 --- /dev/null +++ b/style.css @@ -0,0 +1,54 @@ +@media only screen and (max-width:1079px) { + .sidetoc { + display: none; + } +} + +@media only screen and (min-width:1080px) { + main { + position: relative; + padding-right: 170px; + } + .sidetoc { + margin-left: auto; + margin-right: auto; + /*left: calc(100% + (var(--content-max-width))/4 - 180px);*/ + left: calc(100% - 200px); + position: absolute; + } + .pagetoc { + position: fixed; + width: 200px; + height: calc(100vh - var(--menu-bar-height) - 0.67em * 4); + overflow: auto; + z-index: 1000; + } + .pagetoc a { + border-left: 1px solid var(--sidebar-bg); + color: var(--fg) !important; + display: block; + padding-bottom: 5px; + padding-top: 5px; + padding-left: 10px; + text-align: left; + text-decoration: none; + font-size: 1.2rem; + } + .pagetoc a:hover, + .pagetoc a.active { + background: var(--sidebar-bg); + color: var(--sidebar-fg) !important; + } + .pagetoc .active { + background: var(--sidebar-bg); + color: var(--sidebar-fg); + } +} + +.page-footer { + margin-top: 50px; + border-top: 1px solid #ccc; + overflow: hidden; + padding: 10px 0; + color: gray; +} \ No newline at end of file diff --git a/theme/index.hbs b/theme/index.hbs new file mode 100644 index 0000000..a1a5541 --- /dev/null +++ b/theme/index.hbs @@ -0,0 +1,344 @@ + + + + + + {{ title }} + {{#if is_print }} + + {{/if}} + {{#if base_url}} + + {{/if}} + + + + {{> head}} + + + + + + + {{#if favicon_svg}} + + {{/if}} + {{#if favicon_png}} + + {{/if}} + + + + {{#if print_enable}} + + {{/if}} + + + + {{#if copy_fonts}} + + {{/if}} + + + + + + + + {{#each additional_css}} + + {{/each}} + + {{#if mathjax_support}} + + + {{/if}} + + + + + + + + + + + + + + + + + +
+ +
+ {{> header}} + + + + {{#if search_enabled}} + + {{/if}} + + + + +
+ +
+
+ + +
+ + {{{ content }}} + + +
+ + + + + +
+ + + +
+ +
+ +
+ + +
+
+ + + +
+ + {{#if livereload}} + + + {{/if}} + + {{#if google_analytics}} + + + {{/if}} + + {{#if playground_line_numbers}} + + {{/if}} + + {{#if playground_copyable}} + + {{/if}} + + {{#if playground_js}} + + + + + + {{/if}} + + {{#if search_js}} + + + + {{/if}} + + + + + + + + + {{#each additional_js}} + + {{/each}} + + {{#if is_print}} + {{#if mathjax_support}} + + {{else}} + + {{/if}} + {{/if}} + + + diff --git a/version.md b/version.md new file mode 100644 index 0000000..95280bd --- /dev/null +++ b/version.md @@ -0,0 +1,8 @@ + + + +### 版本信息 + +- 仓库版本:[7fa86ea953e75f4b8a68a11bf9f082c61a96e658](gopl-zh-7fa86ea953e75f4b8a68a11bf9f082c61a96e658.zip) +- 更新时间:2022-08-04 14:16:21 +- 构建时间:2022-08-04 14:19:13