如今一个不太大的程序可能会包含10000个函数。程序的作者只需要考虑其中的一部分,是自己设计的可能会少,因为大多数都是其他人所写的,且能够通过包实现重复使用。

Go提供了超过100个标准包。Go社区,一个蓬勃发展的生态,用于包设计、分享、重用和发展,已经发布了很多包。本章,我们将会展示如何使用已经存在的包和创建包。

Go也自带了go tool工具,一个用于go包管理的复杂巧妙的但易于使用的命令。
本书开头,我们已经展示了如何使用go tool来download、build和run程序。本章中,我们将会看到工具的底层概念underlying concept,探索它的更多能力,其中包括了打印文档、查询在命名空间workspace中的包的元数据。下一章将探索它的测试特性。

10.1 介绍

包系统的目的是使得大型程序的设计和维护变得practical,通过将相关联的组件组合到一起形成一个单元,这些单元义域理解和变化,并且独立于程序的其他包。这种包特性允许包被其他人分享和重用,在一个组织内分发,或者应用在更广泛的范围。

每个包定义一个独特的名字,包含它自己的标识符。每个名字关联一个特定的包,请选择简短、清楚的名字作为类型、函数等我们最常用的标识符来命名,不要和程序中的其他部分造成命名冲突。

包通过控制名字在包外是否可见或者可导出,来提供封装特性。通过限制包成员的可见性,隐藏了包内API的helper functions和types,允许包的持有者有把握地更改包内应用,包外的代码将不受影响。限制包成员的可见性,同样也隐藏了变量,因此clients只能通过导出的函数来访问和更新变量,这样保证了内部变量的一致性(preserve internal invariants)和并发访问的相互排斥(在函数中加互斥锁?)。

当我们改变一个文件,必须重新编译文件的包和潜在的依赖此文件的所有包。即使是从头开始编译,go语言的编译速度也明显得快于其他语言。有三个原因。第一,所有的import必须明确地列在源文件的开头,因此编译器不需要查看整个源文件来确定它的依赖。第二,一个包的依赖构成一个非环的定向图,因此没有环,包能够被单独编译有可能还会并发编译。最后,对于一个已经编译好的目标文件,不仅记录了它自身的导出信息,也记录了它依赖的导出信息。因此当编译一个包时,编译器只需要对每个import而导入一个目标文件,而不需要查看它之下的依赖文件。

10.2 导入路径

每个包被一个唯一的string标识,称为import path。import path是在import声明中的字符串:

import (
"fmt"
"math/rand"
"encoding/json"
"golang.org/x/net/html"
"github.com/go-sql-driver/mysql"
)

正如我们在2.6.1中提到的那样,go语言规范并没有指明这些string的含义,也没有定义该如何确定import path,这些都是由tools决定的。在本章中,我们将详细查看go tools是如何诠释他们的,这也是go程序用来build,test等操作的大部分功能。另外也有第三方扩展的tools存在,例如,使用google内部的多语言构建系统的go程序员,使用不同的规则来命名和定位包,定义测试案例等到,因为这会更匹配他们内部的环境。

对于尝试分享和发布的包,导入路径应该是全球唯一的。为避免冲突,非标准包的包的导入路径应该以拥有此机构的域名开头,这也使得查找包变得可能。例如,上面的声明导入了一个Go机构拥有的HTML解析器包和一个公开的第三方MySql数据库驱动包。

10.3 包声明 Package Declaration

包声明要求放在每个go源文件的开头。主要目的是当此包被另外包导入时,来确定此包的默认标识符。
例如,math/rand包的每个文件都以pack rand开头,因此当你导入这个包时,你可以通过rand.Int, rand.Float64这种形式访问它内部的成员。

package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Int())
}

按照惯例,包名是导入路径的的最后一段,因此,两个包有可能导入路径不同包名却相同。
有三个主要的例外不适用于“最后一段”这个惯例。第一,一个执行go程序包名字总是为main,不管包的导入路径。这个名字是给go build一个信号,告诉它必须调用连接器来生成一个可执行文件。
第二,在包的目录中有一些以_test.go结尾的文件,它们的包名也是以_test结尾。这种目录可能会定义两个包:一个常规包,一个称之为external test package. 这个_test后缀告诉go test必须构建两个包,并且指示了哪个文件对应哪个包。(所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的。)External test packages包用来避免测试代码中的循环导入依赖,具体细节我们将在11.2.4节中介绍。
第三,一些为了依赖管理的工具将在包的导入路径后加版本号,例如"gopkg.in/yaml.v2,。包的名字排除掉后缀,仍然是yaml。

10.4 导入声明 Import Declarations

源文件在包声明之后,非导入声明之前,可以有0个或多个import声明语句。每个import声明可以定义一个或多个导入路径。两种形式都可用不过第二种形式最常用。

import "fmt"
import "os"
import (
	"fmt"
	"os"
)

通过使用空格,import 的包被分组,分组表示包的域名不同。导入的顺序无关紧要,但每个组内的包按字母排序。

import (
"fmt"
"html/template"
"os"
"golang.org/x/net/html"
"golang.org/x/net/ipv4"
)

如果我们想要导入两个相同名字的包,如math/rand 和crypto/rand, 导入声明过必须定义一个可选的名字来避免冲突。这称之为renaming import

import (
	"crypto/rand"
	mrand "math/rand"     // alternative name mrand avoids conflict
)

这个可选的名字只作用在此导入文件中,在另外的文件,甚至相同包的其他文件中,可能会按照默认名字或其他名字来导入这个包。

在没有冲突时renaming package也会很有用。如果导入包名很臃肿,就像有时自动声明代码的时候,一个缩写的包名会很方便使用。短的名字也应该避免产生疑惑。选择一个可选名字能够避免本地变量的冲突。例如,文件中许多本地变量名为path,我们在导入path包时就可以用pathpkg名称。

每个import声明建立一个从当前包到被导入包的依赖关系。如果出现环形依赖,go build tool会报错。

10.5 空白导入 Blank Import

在一个文件中导入一个包但是未引用它定义的名字,就会产生一个错误。然而,有时我们必须导入一个包仅仅是为了这些功能:计算包级别变量的初始化表达式,执行它的init函数。为了防止unused import错误,我们必须使用_来命名导入的包名,即空标识符。通常,空标识符不会被使用。
```import _ “image/png” // register PNG decoder````

这叫做a blank import. 它通常被用来执行一个编译器的机制,在此机制中主程序能够通过空白导入可选的包来开启可选的选项。我们先看一下如何使用它,接着看他是如何工作的。

标准库的image包导出一个Decode函数,用来从io.Reader读取字节,计算那个image格式来被使用于编码数据,包含合适的解码器,返回返回Image.Image。使用image.Decode, 很容易创建一个简单的转换器,读取一种格式的图像,写入另一种格式:

// The jpeg command reads a PNG image from the standard input
// and writes it as a JPEG image to the standard output.
package main
import (
	"fmt"
	"image"
	"image/jpeg"
	_ "image/png" // register PNG decoder
	"io"
	"os"
)
func main() {
	if err := toJPEG(os.Stdin, os.Stdout); err != nil {
		fmt.Fprintf(os.Stderr, "jpeg: %v\n", err)
		os.Exit(1)
	}
}
func toJPEG(in io.Reader, out io.Writer) error {
	img, kind, err := image.Decode(in)
	if err != nil {
		return err
	}
	fmt.Fprintln(os.Stderr, "Input format =", kind)
	return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
}

如果我们将3.3章节的程序结果给此转换程序,它检测出PNG格式的输入,写入一个JPEG的输出:

$ go build gopl.io/ch3/mandelbrot
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
Input format = png

注意到其中_ "image/png"的空白导入,如果没有慈航,程序编译和连接正常但是不能识别和解码PNG格式:

$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
jpeg: image: unknown format

查看它是如何工作的。标准库提供应用于GIF、PNG、JPEG的解码器,使用者也可以提供其他形式的,但是为了保持可执行文件体积小,解码器并没有被包含在应用中除非明确要求。image.Decode函数维护一个支持格式的表。表中的每一项定义四件事:格式的名字,所有使用此种编码格式的图片前缀字符串,用来探测此种编码;一个Decode函数用于解码;一个DecodeConfig函数用来解码图片的元数据,如大小、颜色空间。通过调用image.RegisterFormat,一个表项被加入到表中,通常是在所支持的包的初始化中完成,如image/png.

package png // image/png

func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)

func init() {
	const pngHeader = "\x89PNG\r\n\x1a\n"
	image.RegisterFormat("png", pngHeader, Decode, DecodeConfig) // 添加表项
}

这样应用程序当需要解码某个格式时,只需要空白导入此格式的包,就可以使image.Deocde函数能够解码此种格式。

database/sql使用相似的机制来使用户安装所需的数据库驱动:

import (
"database/mysql"
_ "github.com/lib/pq" // enable support for Postgres
_ "github.com/go-sql-driver/mysql" // enable support for MySQL
)
db, err = sql.Open("postgres", dbname) // OK
db, err = sql.Open("mysql", dbname) // OK
db, err = sql.Open("sqlite3", dbname) // returns error:
unknown driver "sqlite3"

10.6 包和命名 Packages and Naming

本章节,我们将提供一些建议,如何遵循go独特的转换来用于命名包名和它的成员。

创建包名要短,但不要短到语义不明。标准包中最常用的包名为bufio,bytes,flag,fmt,http,io.json,os,sort,sync,time.

Be descriptive and unambiguous where possible. 尽可能的描述性和清楚性。例如,当命名一个utility性质的包时,如果有类似imageutil和ioutil这样简明的名字,就不要命名为util这样含糊不清的名字。避免选择常用于本地变量名称的名字作为包名,否则使用者将需要将包名重命名。

包名字通常为单数形式。标准包的bytes,errors,strings使用复数形式是为了避免掩盖预定义的类型,而go/types则为了避免和关键字冲突。

避免使用已经有其他含义的名字作为包名。例如,我们之前使用temp来作为temperature conversion包名,但是这不持久的。因为temp通常代码temporary。We went through a brief period with the name temperature, but that was too long and didn’t say what the package did。最后,使用tempconv,足够短也和strconv保持一致。

现在查看包成员的命名。因为每个其他包成员的使用指定的标识符,如fmt.Println,引用包成员时同时指定了包名。在Println中不需要关注formatting的概念,因为包的名字fmt已经告诉我们了。当设计一个包时,应该考虑这两部分的整体如何,而不是只看成员名称。例如:
bytes.Equal flag.Int http.Get json.Marshal

可以指定一些通用名称样式。strings包提供一些操作string的独立函数:

package strings
func Index(needle, haystack string) int
type Replacer struct{ /* ... */ }
func NewReplacer(oldnew ...string) *Replacer
type Reader struct{ /* ... */ }
func NewReader(s string) *Reader

单词string在函数名中不会出现。使用者采用strings.Index,strings.Replacer的形式调用他们。

另外一些包可能只会有一种类型,例如html/template和math/rand,对象暴露一个数据类型再加它的方法,通常有一个New函数用来创造实例。

package rand // "math/rand"
type Rand struct{ /* ... */ }
func New(source Source) *Ran

这将导致重名,就像template.Template或者rand.Rand, 这也是为什么这些包名短的原因。

另一个极端,有些包想net/http有很多方法,但结构少,因为他们执行非常复杂的任务。尽管有超过20种类型和更多的函数,但包的成员名称简单:Get, Post, Handle, Error, Client, Server.

10.7 go 工具 GO Tool

余下的章节介绍go too, 它被用于 downloading, querying, formatting , building, testing , and installing packages of Go code.

go tool将一系列工具特性整合到命令集合中。它是一个包管理(类似apt和rpm),用于查看包清单、查看包依赖,从远程版本控制系统下载包。它是一个build system, 能够查看包依赖,包含编译器、汇编器、链接器,比make工具稍微少一点完备性。它是一个测试驱动,就像11章节看到的那样。

它的命令行接口使用“Swiss army knife”(瑞士军刀)形式,包含需要子命令,例如get、run、build和fmt。可以使用go help查看,这里我们列出最常用命令:

$ go
...
	build compile packages and dependencies
	clean remove object files
	doc show documentation for package or symbol
	env print Go environment information
	fmt run gofmt on package sources
	get download and install packages and dependencies
	install compile and install packages and dependencies
	list list packages
	run compile and run Go program
	test test packages
	version print Go version
	vet run go tool vet on packages
Use "go help [command]" for more information about a command.

为了最小化配置需求,go tool严重依赖各种约定。例如,给出geo源码文件的名字,工具能够找到它内部包裹的包,因为每个目录包含一个单独的包,一个包的import path与工作区的目录体系相关。给出一个包的import path, 工具能够找到存储目标文件的目录。它也能找到连接源码仓库的服务器的URL。

工作区 Workspace Organization

大多数使用者唯一需要配置的就是GOPATH这个环境变量,它定义了工作区的根目录。当切换到另一个不同的工作区,使用者更新GOPATH。例如,我们将GOPATH设置为$HOME/gobook:

export GOPATH=$HOME/gobook
go get gopl.io/...

当下载了书中的样例代码后,工作区如下所示:

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/
			...

GOPATH有三个子目录。src目录存储源码。每个包存储的目录依赖于$GOPATH/src,也就是包的导入路径,例如gopl.io/ch1/hellword。一个GOPATH工作区中,在src下面包含许多版本控制仓库,如gopl.io和golang.org。pkg子目录是build工具存储已编译好的包的地方。bin目录存放可执行文件。

另一个GOROOT环境变量,定义了Go distribution的root Go directory,它提供标准库中所有的包。GOROOT下的目录结构和GOPAT类似,列入fmt包源码存放在$GOROOT/src/fmt目录下。用户不需要设置GOROOT,因为默认go tool工具会使用它安装的位置。

go env命令与工具链相关的环境变量值, 包括遗漏的默认值(啥意思,就是没设置的值也显示)。GOOS描述操作系统(如android, linux, darwin, windows), GOARCH描述目标程序架构,例如amd64,386或者arm。尽管GOPATH是唯一设置的值,其他的值也会显示:

$ go env
GOPATH="/home/gopher/gobook"
GOROOT="/usr/local/go"
GOARCH="amd64"
GOOS="darwin"
...

下载包

当使用go tool, 一个包的import path不仅指明了如何在本地工作区查找此包,还指明了如果在Internet上查找此包,因此go get能够获取和更新此包。

go get命令可以下载单一一个包或者整个树或仓库(使用…符号)。工具同样会计算和下载包的所有依赖。
一旦go get下载了包,它build此包,install 库和命令。下面第一个命令获取golint工具,它用于检查源码中常用的样式问题。第二个命令运行golint这个工具,检查我们之前系的popcout源码。它会报告出我们忘记在包中写文档:

$ 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,能够向他们的版本控制系统发出合适的请求。其他网络,需要查看在import path中使用哪种版本控制协议。使用go help importpath查看更多内容。

go get创建的目录是远程仓库的真实fork,而不仅仅是文件的复制,因此可以使用版本控制命令来查看本地文件的变化或者更新到一个不同的吧版本:

$ cd $GOPATH/src/golang.org/x/net
$ git remote -v
origin https://go.googlesource.com/net (fetch)
origin https://go.googlesource.com/net (push)

import path中的域名,如golang.org,不是Git Server的真实域名–go.googlesource.com。这是go tool的一个特性,使得在import path中使用用户域名,而网络使用一个通用的服务如googlesource.com或github.com。
在https://golang.org/x/net/html写的HTML页面包含下面展示的元数据,这些元数据重定向go tool工具到git仓库作为真实的网络地址;

$ go build gopl.io/ch1/fetch
$ ./fetch https://golang.org/x/net/html | grep go-import
<meta name="go-import"
content="golang.org/x/net git https://go.googlesource.com/net">

如果指定-u参数,go get将会将包和他们的依赖在built和installed之前更新到最新版本。如果不指定,则本地已存在的包不会更新。

使用-u参数通过获取包的最新版本,当开启一个新项目时有用,但对于对版本控制严格的已部署项目则不太合适。通常的解决办法是vendor the code, 就是说,将所有需要的依赖做一个长期的本地备份,谨慎地更新这个备份。从Go1.5开始,这将改变包的import path,因此golang.org/x/net/html的备份包将变为gopl.io/vendor/golang.org/x/net/html。 最近版本的go tool直接支持vendoring,通过go help gopath中的Vendor Directories详细查看。

Building Packages

go build命令编译每一个参数包。如果包是一个库,结果被舍弃,这可以用于检查包是否能被正确编译。如果包名为main,go build包含链接器在当前目录创建一个可执行文件;文件名是包导入路径的最后一段。
因为每个目录包含一个包,每个可执行程序或者Unix命令都要求放入一个独立的目录中(就是说每个编译好的可执行文件,都必须放在一个独立的目录中)。这些目录有时会是一个名为cmd目录的子目录,如golang.org/x/tools/cmd/godoc 命令,通过web接口提供go包文档。

包可以通过import path指定,也可以通过一个相对目录名指定,目录名以.或者..开头。如果没有参数指定,则相对于当前目录。因此下面的命令build相同的包,尽管他们将可执行文件写入go build运行的目录:

$ cd $GOPATH/src/gopl.io/ch1/helloworld
	$ go build
and:
	$ cd anywhere
	$ go build gopl.io/ch1/helloworld
and:
	$ cd $GOPATH
	$ go build ./src/gopl.io/ch1/helloworld
but not:
	$ 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"]

特别地,对于用后即弃的程序,像如下所示,我们希望在build后就立即运行它,go run命令会组合这两个步骤:

$ go run quoteargs.go one "two three" four\ five
["one" "two three" "four five"]

第一个不以为.go结尾的参数将会作为可执行程序参数列表的起始位置。

go build默认build此包和它的所有依赖,丢弃掉除了可执行文件外的所有编译代码。依赖分析和编译都会很快,但是随着项目增长到数十个包和数万行代码,重新依赖的时间消耗会变得很可观,有时需要几秒钟,即使这些依赖项没有发生变化(啥意思,依赖项不变,编译时间会变长?)。

go install命令和go build很相似,但它保存了编译代码和命令,而不是丢弃他们。已经编译的包位于$GOPATH/pkg目录下,与保存源码的src目录相关联,命令行执行程序存放在bin目录(用户常将GOPATH/bin目录添加到可执行文件搜索路径中)。因此,go build和go install不会为那些没有变化的包和命令重新编译,将会使编译加快。为了方便,go build -i 安装编译目标的依赖包。

因为编译好的包依赖于平台和架构,因此go install i将他们保存在和GOOS和GOARCH这两个环境变量所存储指的目录中。例如golang.org/x/net/html包被编译后安装在 $GOPATH/pkg/darwin_amd64下的golang.org/x/net/html.a。

跨平台编译go程序很简单,那意味着构建一个用于不同平台和cpu的可执行程序。仅需要在编译时设置GOOS和GOARCH这两个变量。这个跨平台程序会打印出它是为那个平台和架构编译的:

gopl.io/ch10/cross
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 tool工具则只会在编译成此架构上的文件时才使用此文件。还有一些特殊的注释称为build tags,会进行更详细的控制。例如:
// +build linux darwin 这个注释在包声明和文档注释之前,go build也只会在为linux和Mac OS构建时才编译此文件,像下面则表示从不编译此文件:
// +build ignore 查看go/build包的文档中Build Constraints部分访问更多内容:
$ go doc go/build