首页 » Go语言程序设计 » Go语言程序设计全文在线阅读

《Go语言程序设计》第9章 包

关灯直达底部

Go语言的标准库里包含大量的包,提供了大量、广泛而且富有创意的各种功能。另外,在Go Dashboard(godashboard.appspot.com/project)上还有很多第三方的包可以使用。

除了这些,我们还能创建自己的包,安装到标准库里面,或者只是保留在我们自己的Go语言目录树里,也就是GOPATH路径。

在这一章我们将描述如何创建和导入一个自定义的包或者第三方的包,然后简略地了解下gc编译器的一些命令行参数,最后,我们来看一下Go语言的标准库,避免重复造轮子。

9.1 自定义包

到目前为止,我们见过的所有例子都是以一个包的形式存在的,也就是 main 包。在 Go语言里,允许我们将同一个包的代码分隔成多个小块来单独保存,只需要将这些文件放在同一个目录即可。例如,第8章的invoicedata例子,虽然有6个独立的文件(invoicedata.go、gob.go、inv.go、jsn.go、txt.go和xml.go),但是每个文件的第一条语句都是包(main),表明它们都是同属于一个包的,也就是main包。

对于更大的应用程序,我们可能更喜欢将它的功能性分隔成逻辑的单元,分别在不同的包里实现。或者将一些应用程序通用的那一部分剖离出来。Go语言里并没有限制一个应用程序能导入多少个包或者一个包能被多少个应用程序共享,但是将这些应用程序特定的包放在当前应用程序的子目录下和放在GOPATH源码目录下是不大一样的。我们所说的GOPATH源码目录是一个叫src的目录,每一个在GOPATH环境变量里的的目录都应该包含这个目录,因为Go语言的工具链就是要求这样做的。我们的程序和包都应该在这个src目录下的子目录里。

我们也可以将我们自己的包安装到Go语言包目录树下,也就是GOROOT下,但是这样没有什么好处而且可能会不太方便,因为有些系统是通过包管理系统来安装 Go语言的,有些是通过安装包,有些是手动编译的。

9.1.1 创建自定义的包

我们创建的自定义的包最好就放在GOPATH的src目录下(或者GOPATH src的某个子目录),如果这个包只属于某个应用程序,可以直接放在应用程序的子目录下,但如果我们希望这个包可以被其他的应用程序共享,那就应该放在GOPATH的src目录下,每个包单独放在一个目录里,如果两个不同的包放在同一个目录下,会出现名字冲突的编译错误。

作为惯例,包的源代码应放在一个同名的文件夹下面。同一个包可以有任意多个文件,文件的名字也没有任何规定(但后续名必须是.go),在这本书里我们假设包名就是.go的文件名(如果一个包有多个.go文件,则其中会有一个.go文件的文件名和包名相同)。

第1章(1.5节)的stacker例子由一个主程序(在stacker.go文件里)和一个自定义的stack包(在文件stack.go里)组成,源码目录的层次结构如下:

aGoPath/src/stacker/stacker.go

aGoPath/src/stacker/stack/stack.go

GOPATH环境变量是由多个目录路径组成且路径之间以冒号(Windows上是分号)分隔开的字符串,这里的aGoPath就是GOPATH路径集合中的其中一个路径。

我们在stacker目录里执行go build命令,就会得到一个stacker的可执行文件(在Windows 系统上是 stacker.exe)。但是,如果我们希望生成的可执行文件放到 GOPATH的bin 目录里,或者想将 stacker/stack 包共享给其他的应用程序使用,这就必须使用 go install来完成。

当执行go install命令创建stacker程序时,会创建两个目录(如果不存在就会创建):aGoPath/bin和aGoPath/pkg/linux_amd64/stacker,前者包含了stacker可执行文件,后者包含了stack包的静态库文件(至于linux_amd64等会根据不同的系统和硬件体系结构而变化,例如在32位的Windows系统上是windows_386)。

需要在 stacker 程序中使用 stack 包时,在程序源文件中使用导入语句 import"stacker/stack"即可,也就是绝对路径(Unix风格)去除aGoPath/src这部分。事实上,只要这个包放在GOPATH下,都可以被别的程序或者包导入,GOPATH下的包没有共享和专用之分。

又比如第6章(6.5.3节)实现的有序映射是在omap包里,它被设计为可由多个程序使用。为了避免包名的冲突,我们在GOPATH(如果GOPATH有多个路径,任意一个路径都可以)路径下创建了一个具有唯一名字(这里用了域名)的目录,结构如下:

aGoPath/src/qtrac.eu/omap/omap.go

这样其他的程序,只要它们也在某个 GOPATH 目录下面,都可以通过使用 import"qtrac.eu/omap"来导入这个包。如果我们还有其他的包需要共享,则将它们放到aGoPath/src/qtrac.eu路径下即可。

当使用 go install 安装 omap 包的时候,它创建了 aGoPath/pkg/linux_amd64/qtrac.eu目录(如果不存在的话),保存了omap包的静态库文件,其中linux_amd64是根据不同的系统和硬件体系结构而变化的。

如果我们希望在一个包里创建新的包,例如,在my_package 包下面创建两个新的包pkg1和pkg2,可以这么做:在aGoPath/src/my_package下创建两个子目录,例如aGoPath/src/my_package/pkg1和aGoPath/src/my_package/pkg2,对应的包文件是 aGoPath/src/my_package/pkg1/pkg1.go和aGoPath/src/my_package/pkg2/pkg2.go。之后,假如想导入pkg2,使用import my_package/pkg2即可。Go语言标准库的源码树就是这样的结构。当然,my_package 目录可以有它自己的包,如 aGoPath/src/my_package/my_package.go文件。

Go语言中的包导入的搜索路径是首先到GOROOT(即$GOROOT/pkg/${GOOS}_${GOARCH},比如/opt/go/pkg/linux_amd64),然后是GOPATH 环境变量下的目录。这就意味这可能会有名字冲突。最简单的方法就是确保GOPATH里包含的每个路径都是唯一的,例如之前我们以域名来作为omap的包的目录。

在Go程序里使用标准库里的包和使用GOPATH路径下的包是一样的,下面几个小节我们来讨论一些平台特定的代码。

9.1.1.1 平台特定的代码

在某些情况下,我们必须为不同的平台编写一些特定的代码。例如,在类Unix的系统上,通常shell都支持通配符(也叫globbing),所以在命令行输入*.txt,程序就能够从os.Args[1:]切片里读取到比如["README.txt","INSTALL.txt"]这些值。但是在Windows平台上,程序只会接收到["*.txt"),我们可以使用filepath.Glob函数来实现通配符的功能,但是这只需要在Windows平台上使用。

那如何决定什么时候才需要使用filepath.Glob函数呢,使用if runtime.GOOS =="windows" {...}即可,这也是本书中使用最广的方法,例如cgrep1/cgrep1.go程序等等。另一种办法就是使用平台特定的代码来实现,例如,cgrep3程序有3个文件,cgrep.go、util_linux.go、util_windows.go,其中util_linux.go定义了这么一个函数:

func commandLineFiles(files string) string { return files }

很明显,这个函数并没有处理文件名通配,因为在类 Unix 系统上没必要这么做。而util_windows.go文件则定义了另一个同名的函数。

func commandLineFiles(files string) string {

args := make(string, 0, len(files))

for _, name := range files {

if matches, err := filepath.Glob(name); err != nil {

args = append(args, name) // 无效模式

} else if matches != nil { // 至少有一个匹配

args = append(args, matches...)

}

}

return args

}

当我们使用go build来创建cgrep3程序时,在Linux机器上util_linux.go文件会被编译而 util_windows.go 则被忽略,而在Windows平台恰好相反,这样就确保了只有一个commandLineFiles函数被实际编译了。

在 Mac OS X 系统和FreeBSD 系统上,既不会编译 util_linux.go 也不会编译util_windows.go,所以go build会返回失败。但是我们可以创建一个软链接或者直接复制util_linux.go到util_darwin.go或者util_freebsd.go,因为这两个平台的shell也是支持通配符的,这样就能正常构建Mac OS X和FreeBSD平台的程序了。

9.1.1.2 文档化相关的包

如果我们想共享一些包,为了方便其他的开发者使用,需要编写足够的文档才行。Go语言提供了非常方便的文档化工具 godoc,可以在命令行显示包的文档和函数,也可以作为一个Web服务器启动,如图9-1所示[1]。godoc会自动搜索GOPATH路径下的所有包并将它显示出来,如果某些包不在GOPATH路径下,可以使用-path参数(除此之外还要有-http参数)来指定(我们在关于Go语言官方文档的部分讨论了godoc,参见1.1节)。

图9-1 omap包的文档

好的文档应该怎么写,这是一个一直争论不休的问题,因此在这一节我们只是纯粹地来了解一下Go语言的文档化机制。

在默认情况下,只有可导出的类型、类、常量和变量才会在godoc里出现,因此全部这些内容应该添加合适的文档。文档都是直接包含在源文件里。这里以omap包为例(omap包我们之前已经在第6章讲过了)。

// Package omap implements an efficient key-ordered map.

//

// Keys and values may be of any type, but all keys must be comparable

// using the less than function that is passed in to the omap.New

// function, or the less than function provided by the omap.New*

// construction functions.

package omap

对于一个包来说,在包声明语句(package)之前的注释被视为是对包的说明,第一句是一行简短的描述,通常以句号结束,如果没有句号,则以换行符号结束。

// Map is a key-ordered map.

// The zero value is an invalid map! Use one of the construction functions

// (e.g., New), to create a map for a specific key type.

type Map struct {

可导出类型声明的文档必须紧接在该类型声明之前,而且必须总是描述该类型的零值是否有效。

// New returns an empty Map that uses the given less than function to

// compare keys.For example:

//  type Point { X, Y int }

//    pointMap := omap.New(func(a, b interface{}) bool {

//      α, β := a.(Point), b.(Point)

//      ifα.X != β.X {

//        returnα.X < β.X

//      }

//      returnα.Y < β.Y

//    })

func New(less func(interface{}, interface{}) bool) *Map {

函数或者方法的文档必须紧接在它们的第一行代码之前。上面这个例子是对 omap 包的New构造函数的注释。

图9-2以Web的方式展示了一个函数的文档是什么样的,同时注释里缩进的文本会被视为代码显示在HTML页面上。但是在我写这本书的时候,godoc还不支持任何标记,例如bold、italic、links等。

// NewCaseFoldedKeyed returns an empty Map that accepts case-insensitive

// string keys.

func NewCaseFoldedKeyed *Map {

图9-2 omap包中New函数的文档

上面这段文档是描述了一个出于便捷方面考虑而提供的辅助构造函数,基于一个预定义的比较函数。

// Insert inserts a new key-value into the Map and returns true; or

// replaces an existing key-value pair's value if the keys are equal and

// returns false.For example:

//  inserted := myMap.Insert(key, value).

func (m *Map) Insert(key, value interface{}) (inserted bool) {

这段是Insert方法的文档。可以注意到Go语言里的文档描述通常是以函数或者方法的名字开头的,这是一个惯例,还有(这个不是惯例)就是在文档中引用函数或者方法时不使用圆括号。

9.1.1.3 包的单元测试和基准测试相关的包

Go语言标准库的testing包对单元测试提供了很好的支持。对一个包进行单元测试是一件很简单的事情,只需要在这个包的根目录下创建一个测试文件即可。单元测试的文件名格式为包名_test.go。例如,omap包的单元测试文件为omap_test.go。

在我们这本书里,单元测试文件都是放在一个独立的包(如 omap_test)下面,然后导入需要被测试的包、testing包以及其他一些测试依赖的包。这实际上限制我们只能进行黑盒测试。但是也有些 Go语言程序员会倾向于白盒测试。这很容易做到,只需将测试文件和包源代码放在一起即可,这种情况下我们不需要导入被测试的包,而且还可以测试那些非导出的数据类型,甚至为它们增加一些新的方法以方便测试。

单元测试文件比较特殊的一点是,它没有main函数。取而代之的是,它有一些以Test开头的函数,并且必须只有一个*testing.T类型的参数,没有返回值。我们还可以增加任意其他的辅助函数,当然,这些函数不能以Test开头。

func TestStringKeyOMapInsertion(t *testing.T) {

wordForWord := omap.NewCaseFoldedKeyed

for _, word := range string{"one", "Two", "THREE", "four", "Five"} {

wordForWord.Insert(word, word)

}

var words string

wordForWord.Do(func(_, value interface{}) {

words = append(words, value.(string))

})

actual, expected := strings.Join(words, ""), "FivefouroneTHREETwo"

if actual != expected {

t.Errorf("%q != %q", actual, expected)

}

}

这是 omap_test.go 文件里的一个单元测试。首先创建一个空的omap.Map,然后插入一些字符串类型的键和值(注意键名是区分大小写的)。然后使用Map.Do方法遍历,将得到的每个值追加到一个字符串切片里去。最后,将这个字符串切片组合成一个字符串,检查结构是否是我们所期望的。如果结果不对,则调用testing.T.Errorf方法报告详细的失败的原因。如果错误或者失败方法没有被调用,我们就可以假定测试已经通过了。

测试通过的结果类似如下。

$ go test

ok    qtrac.eu/omap

PASS

如果测试的时候使用 -test.v选项,则会输出更详细的信息。

$ go test -test.v

ok     qtrac.eu/omap

=== RUN TestStringKeyOMapInsertion-4

--- PASS: TestStringKeyOMapInsertion-4 (0.00 seconds)

=== RUN TestIntKeyOMapFind-4

--- PASS: TestIntKeyOMapFind-4 (0.00 seconds)

=== RUN TestIntKeyOMapDelete-4

--- PASS: TestIntKeyOMapDelete-4 (0.00 seconds)

=== RUN TestPassing-4

--- PASS: TestPassing-4 (0.00 seconds)

PASS

如果测试不通过,会得到如下信息(这里我们人为地修改了常量的字符串值,强制它失败),如果指定了-test.v选项,得到的信息可能更多。

$ go test

FAIL      qtrac.eu/omap

--- FAIL: TestStringKeyOMapInsertion-4 (0.01 seconds)

omap_test.go:35: "FivefouroneTHREETwo" != "FivefouroneTHREEToo"

FAIL

另外,这个例子里用到了Errorf方法,testing包的*testing.T还有很多其他的方法可以使用,如testing.T.Fail、testing.T.Fatal等。利用这些方法我们可以实现测试的调试级别。

此外,testing 包还支持基准测试,和其他的测试函数一样,基准测试也是放在package_test.go文件里的,唯一不同的就是基准测试的函数名必须以Benchmark开头,并且必须有一个*testing.B类型的参数,没有返回值。

func BenchmarkOMapFindSuccess(b *testing.B) {

b.StopTimer // Don't time creation and population

intMap := omap.NewIntKeyed

for i := 0; i < 1e6; i++ {

intMap.Insert(i, i)

}

b.StartTimer // Time the Find method succeeding

for i := 0; i < b.N; i++ {

intMap.Find(i % 1e6)

}

}

函数一开始是就执行 b.StopTimer来停止计时器,因为我们不希望将创建和生成omap.Map的时间也计算在内。我们创建一个空的omap.Map,然后插入一百万条记录。

默认情况下 go test 不会执行基准测试,所以如果我们需要基准测试的时候必须指定-test.bench 选项,还需要有一个正则表达式字符串,来匹配我们需要执行的基准测试函数名。例如.*表示所有的基准测试函数都会被执行(只有一个.也行)。

$ go test -test.bench=.

PASS     qtrac.eu/omap

PASS

BenchmarkOMapFindSuccess-4  1000000  1380ns/op

BenchmarkOMapFindFailure-4  1000000  1350ns/op

从这个结果我们可以看出,两个函数都遍历了一百万次,还给出了每次操作的时间消耗。至于遍历多少次是由go test来决定的,也就是b.N,不过我们可以使用-test.benchtime选项来指定我们希望每个基准测试的执行时间为多少秒。

本书中还有其他的一些例子,也是使用package_test.go作为测试文件的。

9.1.2 导入包

Go语言允许我们对导入的包使用别名来标识。这个特性是非常方便和有用的,例如,可以在不同的实现之间进行自由的切换。举个例子,假如我们实现了bio包的两个版本bio_v1和bio_v2,现在在某个程序里使用了import bio "bio_v1",如果需要切换到另一个版本的实现,只需要将bio_v1改成bio_v2即可,即import bio "bio_v2",但是需要注意的是,bio_v1和bio_v2的API必须是相同的,或者bio_v2是bio_v1的超集,这样其余所有的的代码都不需要做任何改动。另外,最好就不要对官方标准库的包使用别名,因为这样可能会导致一些混淆或激怒后来的维护者。

我们之前在第5章提到过(见5.6.2节),当导入一个包时,它所有的init函数就会被执行。有些时候我们并非真的需要使用这些包,仅仅是希望它的init函数被执行而已。

举个例子,如果我们需要处理图像,通常会导入 Go 标准库支持的所有相关的包,但是并不会用到这些包的任何函数。下面就是imagetag1.go程序的导入语句部分。

import (

"fmt"

"image"

"os"

"path/filepath"

"runtime"

_ "image/gif"

_ "image/jpeg"

_ "image/png"

)

这里导入了image/gif、image/jpeg和image/png包,纯粹是为了让它们的init函数被执行(这些init函数注册了各自的图像格式),所有这些包都以下划线作为别名,所以Go语言不会发出导入了某个包但是没有使用的警告。

9.2 第三方包

Go语言的工具链几乎贯穿全书了,我们使用它来创建程序和包,如omap包等。除此之外,我们还可以用来下载、编译和安装第三方的包。当然,前提必须是我们的计算机能够连接网络。godashboard.appspot.com/project上面维护了一系列第三方的包。(另外一种方法就是通过下载源码,通常是通过版本控制系统来下载,然后本地编译。)

需要安装Go Dashboard的包的话,首先点击它的链接到包的主页,然后找到有go get命令的地方,通常那就是介绍如何下载和安装包的了。

举个例子,我们点击Go Dashboard页面上的freetype-go.googlecode.com/hg/ freetype链接,然后它会将我们带到code.google.com/p/freetype-go/主页,这个页面上有如何安装的相关介绍,在我写这本书的时候,这个命令是go get freetype-go.google- code.com/hg/freetype。

毕竟这个包是来自于第三方的,go get 还必须将它安装到我们计算机上的某个地方。默认情况下会安装到GOPATH环境变量的第一个路径,如果没法将这个包保存到那里,就自动安装到GOROOT目录。如果我们想强制go get默认使用GOROOT目录,可以在go get运行之前清空GOPATH环境变量中的路径集合。

执行go get 之后,就自动开始下载、创建和安装包了。如果想了解最新安装的包的文档,可以以Web服务的方式来运行godoc,例如godoc -http=:8000,这样就可以查看这个包的文档了。

为了避免名字上的冲突,第三方的包通常使用域名来确保唯一性。举个例子,假如我们想使用FreeType这个包的话,可以这样导入:

import "freetype-go.googlecode.com/hg/freetype"

当然,使用这个包的函数我们只需要最后一部分即可,也就是freetype,比如font, err :=freetype.ParseFont(fontdata)。如果很不幸地连最后一部分也产生名字冲突了,我们还可以使用别名,例如,import ftype "freetype-go.googlecode.com/hg/freetype",然后在我们的代码里这样写font, err := ftype.ParseFont(fontdata)。

第三方的包通常都可以在Go 1上使用,但是有些需要更新的Go版本,或者提供多个版本的下载。比如,有一些需要在最新的开发版上才能使用。通常情况下,最好只使用稳定版的Go (目前是Go 1)和与该版本兼容的第三方包。

9.3 Go命令行工具简介

安装Go的gc编译器自然也就包括了编译器和连接器(6g、6l等),还有其他的一些工具。最常用的就是go,既可以用来创建我们自己的程序和包,又可以下载和安装第三方的程序和包,还可以用来执行单元测试和基准测试,如我们之前在9.1.1.3节中见到的一样。如果需要更多的帮助可以使用go help命令,go help会显示一个命令列表,当然文档化的godoc工具也在其中。

除了我们这本书所用到的工具,还有其他的一些工具和go tool命令,这里我们会介绍一些。其中一个就是go vet命令,它可以检查Go程序的一些简单错误,特别是fmt包的打印函数。

另一个命令就是 go fix,有时候 Go语言的新发行版会包含一些语言上的变更,或者更多的是标准库API的修改,这样会导致我们写好的代码编译不过。这种情况下可以在我们代码的根目录下执行go fix命令来进行自动的升级。我们强烈推荐你使用版本控制系统来管理你的.go 文件,这样所有的修改都会记录下来,或者在运行 go fix之前至少也做一个备份。这样做的原因是go fix有可能会破坏我们现有的代码,如果真的发生了,至少我们还可以恢复它。我们还可以使用带有-diff选项的go fix命令,这样可以看到go fix将要修改哪些地方,但并不会真地修改它们。

最后一个要介绍的命令就是gofmt。它能以一种标准化的方式来格式化Go代码,这也是Go的开发者强烈推荐使用的。使用gofmt的最大好处就是,你不需要考虑哪种编排方式最好, gofmt 可以让你所有的代码看起来都是同一种风格。我们这本书所有的代码都是经过 gofmt格式化的,不过超过75个字符的代码行会被自动折行以适合本书的页宽。

9.4 Go标准库简介

Go标准库里包含大量的包,功能非常丰富。我们这里只是做一个很简单的介绍,因为标准库的内容是随时都有可能有改动的,最好的方式就是浏览官方在线版的标准库(golang.org/pkg/),或者使用本地的godoc,这两种方式都能看到最新的信息并且可以更好地理解每个包提供了什么样的功能。

其中 exp(experimental)包包含一些将来有可能(也可能不)被增加到标准库里面去的实验性包,所以除非我们想参与标准库的开发,否则就不要使用这个包。在以编译源代码方式安装Go的时候通常会带有这个exp包,但通常不会被包含在Go语言安装包中。这里介绍的所有包都可以使用,尽管在我写这本书的时候有些包还不是很完整。

9.4.1 归档和压缩包

Go语言提供了用于读写tar包文件和.zip文件的包archive/tar和archive/zip,如果需要压缩tar包,还可以使用compress/gzip和compress/bzip2,这本书第8章的pack和unpack例子就涵盖了这些功能的用法(参见8.2节)。

其他的压缩格式也支持,例如LZW格式。compress/lzw包主要是用来处理.tiff图像和.pdf文件。

9.4.2 字节流和字符串相关的包

bytes和strings这两个包有很多函数是一样的,只不过前者是处理byte类型的值,而后者是处理string 类型的值。对字符串来说,strings 包提供了大部分常用的功能,例如查找子串、替换子串、切割字符串、过滤字符串、改变大小写(参见3.6.1节),等等。还有,利用strconv包可以很方便地将数值和布尔型类型的值转换成字符串,反过来也可以(参见3.6.2节)。

fmt包提供了很多非常有用的打印函数和扫描函数。打印函数在第3章已经讲过,扫描函数在表8-2也列出来了,后面还紧接着有一些用例。

unicode 包可以用来判断字符的属性,比如一个字符是否是可打印的,或者是否是一个数字(参见3.6.4节)。unicode/utf8和unicode/utf16这两个包主要用来编码和解码rune (也就是Unicode码点),其中unicode/utf8参见3.6.3节,部分还在第8章的utf16-to-utf8练习中出现过。

text/template和html/template包可以用来创建模板,借助模板可以很容易地输出比如HTML等这些文档,只要将一些数据填充进去即可。下面是一个非常简单的text/template包的例子。

type GiniIndex struct {

Country string

Index float64

}

gini := GiniIndex{{"Japan", 54.7}, {"China", 55.0}, {"U.S.A.", 80.1}}

giniTable := template.New("giniTable")

giniTable.Parse(

'<TABLE>' +

'{{range.}}' +

'{{printf "<TR><TD>%s</TD><TD>%.1f%%</TD></TR>".Country.Index}}'+

'{{end}}' +

'</TABLE>')

err := giniTable.Execute(os.Stdout, gini)

<TABLE>

<TR><TD>Japan</TD><TD>54.7%</TD></TR>

<TR><TD>China</TD><TD>55.0%</TD></TR>

<TR><TD>U.S.A.</TD><TD>80.1%</TD></TR>

</TABLE>

template.New函数根据给定的模板名创建了一个新的*template.Template 类型的值。模板名用于在模板嵌套的时候标识特定模板。template.Template.Parse函数解析一个模板(通常是一个.html文件)以备使用。template.Template.Execute函数执行一个模板,并从它的第二个参数读取数据,最后将结果发送到给定的io.Writer 里去。在这个例子里,从 gini 切片读取数据,gini 是一个 GiniIndex 结构体,然后将结果输出到os.Stdout。(为了清晰起见,我们将输出结果分成一行一行地显示。)

模板里的所有动作都是在一个双大括号{{...}}里的,还可以使用{{range}}...{{end}}来遍历一个切片里的所有项,这里我们使用点号(.)来表示 GiniIndex 切片里的每一项,也就是,这个点号可以理解为当前的项。我们可以使用字段名来访问一个结构体的可导出的字段,当然,点号表示当前的项。{{printf}}动作和fmt.Printf函数是一样的,只是使用空格符号来表示圆括号和参数分隔符。

text/template和html/template包支持原始的模板语言和很多动作,包括遍历和条件分支,支持变量和方法调用,还有许多。此外,html/template包还可以安全地防止代码注入。

9.4.3 容器包

Go语言里的切片是一种非常高效的集合类型,但有些时候自定义一些特别的集合类型是有用的,或者是必需的。大部分情况下用内置的map就能解决很多问题,不过Go语言还是提供了container包来支持更多的容器类型。

我们可以使用container/heap包提供的函数来操作一个堆,前提是这个堆上的元素的类型必须满足 heap包中 heap.Interface接口的定义。堆(严格来说是最小堆)维护了一个有序数组,保证堆上的第一个元素肯定是最小的(如果是最大堆,则第一个元素是最大的),这是大家熟知的堆的特性。heap.Interface 接口嵌入了 sort.Interface接口,并增加了 Push和Pop方法(其中 sort.Interface 我们在 4.2.4 节和5.7节讲解过)。

要创建一个满足heap.Interface接口定义的堆还蛮简单的,这是一个例子。

ints := &IntHeap{5, 1, 6, 7, 9, 8, 2, 4}

heap.Init(ints) // 将其转换成堆

ints.Push(9)  // IntHeap.Push并未保持堆的属性

ints.Push(7)

ints.Push(3)

heap.Init(ints) // 堆被打破后必须重新将其转换成堆

for ints.Len > 0 {

fmt.Printf("%v ", heap.Pop(ints))

}

fmt.Println // 打印1 2 3 4 5 6 7 7 8 9 9

下面是一个完整的自定义堆实现。

type IntHeap int

func (ints *IntHeap) Less(i, j int) bool {

return (*ints)[i] < (*ints)[j]

}

func (ints *IntHeap) Swap(i, j int) {

(*ints)[i], (*ints)[j] = (*ints)[j], (*ints)[i]

}

func (ints *IntHeap) Len int {

return len(*ints)

}

func (ints *IntHeap) Pop interface{} {

x := (*ints)[ints.Len-1]

*ints = (*ints)[:ints.Len-1]

return x

}

func (ints *IntHeap) Push(x interface{}) {

*ints = append(*ints, x.(int))

}

大多时候实现一个这样的堆能够解决很多问题了。为了让代码的可读性更高一些,我们将这个堆定义为IntHeap struct { ints int },这样我们就可以在方法里引用ints.ints而不是*ints。

container/list 包提供了双向链表的支持,可以将一个值以 interface{}的类型添加到链表里去。从list里得到的项的类型是list.Element,可以使用list.Element.Value来访问我们添加进去的值。

items := list.New

for _, x := range strings.Split("ABCDEFGH", "") {

items.PushFront(x)

}

items.PushBack(9)

for element := items.Front; element != nil; element = element.Next {

switch value := element.Value.(type) {

case string:

fmt.Printf("%s ", value)

case int:

fmt.Printf("%d ", value)

}

}

fmt.Println // 打印H G F E D B A 9

在这个例子里我们将8个字母依次添加到链表里的最前端,并同时添加一个int型值在最后端。然后我们遍历列表里的元素将每个元素的值打印出来,这里我们不需要使用类型开关,因为我们可以使用fmt.Printf("%v ", element.Value)。但如果我们不仅仅是只打印出它的值,还有其他的用途,这时候就得做类型开关。当然,如果所有的类型都是一样的话,我们可以使用一个类型断言,例如 element.Value.(string)可以用来判断字符串。(关于类型开关我们在5.2.2.2节讲过,类型断言则在5.1.2节。)

除了上面我们刚介绍过的,list.List类型还提供了很多方法,包括Back、Init (用来清空一个列表)InsertAfter、InsertBefore、Len、MoveToBack、PushBackList(将一个列表添加到另一个列表的末尾)。

标准库还提供了container/ring包,实现了一个环形的单向列表。[2]

因为这些容器类型的所有数据都是保存在内存的,如果数据量很大,可以考虑使用标准库里提供的database/sql包来将数据存储到数据库里,database/sql实现了一个SQL数据库的通用接口。实际使用的时候还必须安装数据库的相关驱动才行,这些和很多其他的容器包一样,可以从Go Dashboard(godashboard.appspot.com/project)里下载。还有就是之前我们看到的,本书包含了一个有序映射omap.Map类型,它基于左倾红黑树(left-leaning red-black Tree,参见6.5.3节)。

9.4.4 文件和操作系统相关的包

标准库里提供了很多包来支持文件和目录相关的处理,以及一些和操作系统交互的系统调用。大多数情况下这些包都提供了一个平台无关的抽象层,方便我们写出跨平台的代码。

os(operating system)包提供了很多操作系统相关的函数,例如更改当前工作目录,修改文件权限和所有者,读取和设置环境变量,还有创建、删除文件和目录。另外,os包还提供了创建和打开文件(os.Create和os.Open)、获取文件属性(例如通过os.FileInfo类型)相关的函数,这些在我们之前的章节里都全部用过了(参见7.2.5、第8章)。

一旦文件被打开了,特别是文本文件,经常需要用到一个缓冲区来访问它的内容(例如读取一行字符串而不是一个字节切片)。我们可以通过使用bufio包来实现这个功能,之前就有好些例子是这么用的了。除了使用bufio.Reader和bufio.Writer 来读写字符串,我们还可以读取(或者倒退)rune、单个字节、多个字节,还可以写一个rune、一个或者多个字节。

io包提供了大量的与输入输出相关的函数,用来处理io.Reader和io.Writer。(*os.File类型的值能同时满足这两个接口的定义。)比如我们可以使用io.Copy函数来将数据从一个reader复制到一个writer里去(参见8.2.1节)。另外,这个包还能用来创建内存中的同步管道。

io/ioutil 包提供了一些高级的辅助函数,例如,可以使用 ioutil.ReadAll函数来将一个io.Reader的所有数据读取到一个byte中。ioutil.ReadFile函数也是同样的功能,只是参数必须是字符串,也就是文件名,而不是一个 io.Reader。ioutil.TempFile函数用来创建一个临时文件,返回*os.File 类型的值,还有ioutil.WriteFile函数可以将一个byte写到指定的文件里去。

path包用来操作Unix风格的路径,例如Linux和Mac OS X路径、URL路径、git引用、FTP文件,等等。还有path/filepath包提供了和path相同的函数(当然还有其他的),其目的是提供平台无关的路径处理。这个包还提供了filepath.Walk函数用来遍历读取一个给定路径下的所有文件和目录信息,如我们之前在7.2.5节看过的。

runtime包含一些函数和类型,可以用来访问Go语言的运行时系统。大部分都是一些高级功能,很多时候我们用不到。不过有两个常量还是经常有用的,就是 runtime.GOOS和runtime.GOARCH,两个都是字符串,前者的值可能是“Darwin”、“freebsd”、“linux”或者“windows”,后者可以是“386”、“amd64”、“arm”等。runtime.GOROOT函数返回GOROOT环境变量的值(如果为空则返回Go安装环境的根目录),还有runtime.Version函数返回当前Go语言的版本(字符串),之前我们在第7章还见过runtime.GOMAXPROCS和runtime.NumCPU函数来让Go语言使用机器上所有的处理器。

文件格式相关的包

Go语言标准库对文本文件(7位的ASCII编码、UTF-8或者UTF-16)的支持是非常优秀的,对二进制文件的支持也一样。Go语言提供了一些单独的包来处理JSON和XML文件,还包括它自己的高效简便的Go语言二进制格式(gob)。(这些格式,包括自定义的二进制格式,我们在第8章都讲过了。)

另外,Go语言还提供了csv包用来读取.csv文件(csv是“comma-separated values”的缩写,即用逗号分隔的值)。这个包将文件当成是数据记录处理,也就是每一行被认为是一条记录,包含多个逗号分隔的字段值。这个包具有很大的通用性,例如,我们可以改变它的分隔符(用缩进或者其他的字符来取代逗号),还可以修改它对记录和字段的读写方式。

encoding包里有好几个子包。其中一个就是encoding/binary,我们用来读写二进制数据(参见 8.1.5 节)。还有其他的一些子包用来编码和解码一些比较常见的格式,例如, encoding/base64包用来编码和解码URL,因为它经常会使用这种编码。

9.4.5 图像处理相关的包

Go语言的image包提供了一些高级的函数和数据类型,用来创建和保存图像数据。还包括一些用来对常见图像格式进行编解码的包,例如image/jpeg和image/png。其中一些我们在之前的章节已经讨论过了,比如9.1.2节和第7章的一个练习。

image/draw 包提供了一些基本的画图功能,例如我们之前在第 6 章见过的。第三方的freetype 包为画图增加了一些功能,它可以使用特定的TrueType 字体来画文本,还有freetype/raster包可以画行,画立方体,甚至是二次曲线。(我们在9.2节已经讲过如何获取和安装freetype包。)

9.4.6 数学处理包

math/big包可以创建没有大小限制(仅受内存大小限制)的整数(bit.Int)和有理数(big.Rat)。这些已经在之前的2.3.1.1 节已经介绍过。math/big 还提供了big.ProbablyPrime函数。

math包提供了所有标准的数学处理函数,这些函数都是基于float64类型的,还有一些标准的常量,可以参见表2-8、表2-9和表2-10。

math/cmplx包提供了常见的复数相关函数(基于complex128类型),参见2.11节。

9.4.7 其他一些包

除了以上这些大致归类在一起的包以外,标准库也包含了一些相对有点独立的包。

crypto包提供了MD5、SHA-1、SHA-224、SHA-256、SHA-384、SHA-512等哈希算法。每一个哈希算法都以一个独立的包存在,例如 crypto/sha512。此外,我们还可以使用crypto包来进行加密和解密,例如使用AES算法、DES算法等,分别对应crypto/aes和crypto/des包。

exec包可用来运行外部的程序,当然这也可以使用os.StartProcess函数来完成,但是exec.Cmd类型相对来说更加易用一些。

flag包是一个命令行解析器,可以接收X11风格的选项(例如,-width,而不是GNU风格,比如-w和--width)。这个包只能打印一条非常基本的用法帮助信息,并且没有提供任何输入值的合法检查相关的能力。(所以我们可以指定一个 int 选项,但无法指定什么范围的值是可接受的。)一些替代性的包在Go Dashboard(godashboard.appspot.com/project)上可以找到。

log 包可以用来做日志记录(默认输出到 os.Stdout),可在程序退出或抛出异常的同时产生一条日志。可以使用log.SetOutput函数将log包的输出目标更改成任意的io.Writer。日志的输出格式为时间戳和消息体,不想显示时间戳的话可以在第一条 log输出前调用log.SetFlags(0)。我们还可以使用log.New来创建自定义的logger实例。

math/rand包可以生成伪随机数。rand.Int返回随机的int型值,rand.Intn(n)返回一个在区间[0,n)范围内的int型值。crypto/rand包还提供了函数用来产生加解密用途的更高强度的伪随机数。

regexp包实现了一个非常快而强大的正则表达式引擎,支持RE2引擎语法。我们这本书就有好几个地方是用到这个包的,虽然为了不跑题我们只是简单地用了一下正则的功能,并没用到这个包全部的特性。这个包之前也介绍过了(参见3.6.5节)。

sort包可以很方便地对切片进行排序,包括int型的、float64型的和string类型的,并且提供了基于已排序的切片上的快速查找功能(基于二分查找)。还提供了通用的sort.Sort函数和sort.Search函数用来处理自定义的数据类型(参见4.2.4节的例子和表4-2以及5.6.7节)。

time 包主要包括计时和日期时间解析及格式化相关的函数。time.After函数可以在指定时间间隔(就是我们传入的纳秒值参数)之后往该函数返回的通道里发送一个当时的时间值,我们在之前的例子里有介绍过它(参见7.2.2节)。time.Tick和time.NewTicker函数同样返回一个信道,不同的是我们可以定期地从这个信道里得到一个“嘀嗒”。time.Time结构实现了一些方法,例如,可以提供当前时间,格式化日期/时间为一个字符串,解析日期/时间。(我们在第8章看过time.Time的用法。)

9.4.8 网络包

Go标准库里还有很多包支持网络相关的编程。比如net包提供了一些通信相关的函数和数据类型,包括Unix域、网络套接字,以及TCP/IP和UDP通信等。还有一些函数用来进行域名解析。

net/http包使用了net包,提供了解析HTTP请求和响应的功能,并提供了一个基础的HTTP客户端。除此之外,还包含了一个易于扩展的HTTP服务器,就像我们在第2 章和第3章的练习见到的那样。net/url包提供了URL解析和查询字符串的转义。

标准库里还有其他一些高级的网络包,其中一个就是net/rpc(远程过程调用),可以让客户端远程调用服务器上某些对象的导出方法。另一个就是net/smtp包(简单邮件传输协议),用来发送邮件。

9.4.9 反射包

反射包可以提供运行时的反射(reflection)功能(也叫类型检视,introspection),我们可以在运行时访问和操作任意类型的值。

这个包也提供了一些非常有用的功能。例如 reflect.DeepEqual函数可以用来比较两个值,例如不能直接使用==或者!=操作符进行比较的两个切片。

Go语言里每一个值都有两个属性:实际的值和它的类型。reflect.TypeOf函数能告诉我们任意值的类型。

x := 8.6

y := float32(2.5)

fmt.Printf("var x %v = %v/n", reflect.TypeOf(x), x)

fmt.Printf("var y %v = %v/n", reflect.TypeOf(y), y)

var x float64 = 8.6

var y float32 = 2.5

这里我们使用反射功能输出两个var声明的浮点变量和它们的类型。

调用reflect.ValueOf函数可以得到一个reflect.Value结构,这个结构保存了传入的值,但并不是传入的那个值本身。如果我们需要访问那个传入的值,必须使用 reflect.Value的方法。

word := "Chameleon"

value := reflect.ValueOf(word)

text := value.String

fmt.Println(text)

Chameleon

reflect.Value类型实现了很多方法可以用来提取底层类型的实际值,包括reflect.Value.Bool、reflect.Value.Complex、reflect.Value.Float、reflect.Value.Int和reflect.Value.String。

同样,reflect包还可以用于集合类型,例如切片、映射以及结构体。它甚至能访问结构体的标签文本(tag text)。(如我们在第8章见到的,json和xml的编码器和解码器使用了这个功能。)

type Contact struct {

Name string "check:len(3,40)"

Id int "check:range(1,999999)"

}

person := Contact{"Bjork", 0xDEEDED}

personType := reflect.TypeOf(person)

if nameField, ok := personType.FieldByName("Name"); ok {

fmt.Printf("%q %q %q/n", nameField.Type, nameField.Name, nameField.Tag)

}

"string" "Name" "check:len(3,40)"

如果一个被reflect.Value保存的底层类型的值是可设置的,那么我们可以改变它。这种可设置的能力可以使用reflect.Value.CanSet来检查,它返回一个bool类型的值。

presidents := string{"Obama", "Bushy", "Clinton"}

sliceValue := reflect.ValueOf(presidents)

value = sliceValue.Index(1)

value.SetString("Bush")

fmt.Println(presidents)

[Obama Bush Clinton]

尽管在Go语言里字符串是不可修改的,但在一个string里任意一个项都可以被其他的字符串替换,这也是我们这里所做的。(自然地,在这个例子里我们可以更直接地使用presidents[1] = "Bush"来修改它,没必要使用反射。)

因为无法修改不可修改的值的内容,我们可以在得到原始值的地址后直接用另一个值来替换它。

count := 1

if value = reflect.ValueOf(count); value.CanSet {

value.SetInt(2) // 会抛出异常,不能设置一个int值

}

fmt.Print(count, " ")

value = reflect.ValueOf(&count)

// 不能调用SetInt,因为值是一个*int而不是int

pointee := value.Elem

pointee.SetInt(3) // 成功。可以替换一个指针指向的值

fmt.Println(count)

1 3

从这段代码可以看出,如果条件判断失败,则条件语句的主体部分不会被执行。尽管我们不能设置不可修改的值,如int、float64和string,但我们可以使用reflect.Value.Elem方法来获得reflect.Value的值,这样实质上就允许我们修改一个指针指向的值了,也就是我们这块代码所做的。

同样我们可以用反射来调用任意的函数和方法。下面就是一个例子,它调用了两次一个自定义的TitleCase函数(代码没有展示出来),一次是直接调用,一次使用了反射。

caption := "greg egan's dark integers"

title := TitleCase(caption)

fmt.Println(title)

titleFuncValue := reflect.ValueOf(TitleCase)

values := titleFuncValue.Call(reflect.Value{reflect.ValueOf(caption)})

title = values[0].String

fmt.Println(title)

Greg Egan's Dark Integers

Greg Egan's Dark Integers

reflect.Value.Call方法传入和返回参数都是一个reflect.Value切片。在这个例子中我们传入了单个值(也就是一个长度为1的切片),并且得到一个结果值。

类似地,我们还能调用方法。实际上,我们甚至可以查询某个方法是否存在,进而再决定是否调用它。

a := list.New               // a.Len == 0

b := list.New

b.PushFront(1)                // b.Len == 1

c := stack.Stack{}

c.Push(0.5)

c.Push(1.5)                 // c.Len == 2

d := map[string]int{"A": 1, "B": 2, "C": 3} // len(d) == 3

e := "Four"                 // len(e) == 4

f := int{5, 0, 4, 1, 3}          // len(f) == 5

fmt.Println(Len(a), Len(b), Len(c), Len(d), Len(e), Len(f))

0 1 2 3 4 5

这里创建了两个列表(使用container/list包),其中一个列表我们添加了一个项进去。我们也创建一个栈(使用在1.5节创建的自定义的stacker/stack包),然后往里面增加两个项。接着我们还创建了一个映射、一个字符串和一个int切片。所有这些值的长度都不同。

func Len(x interface{}) int {

value := reflect.ValueOf(x)

switch reflect.TypeOf(x).Kind {

case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:

return value.Len

default:

if method := value.MethodByName("Len"); method.IsValid {

values := method.Call(nil)

return int(values[0].Int)

}

}

panic(fmt.Sprintf("'%v' does not have a length", x))

}

这个函数返回传入值的长度,如果传入的这个值的类型不支持获得它的长度,就抛出一个异常。

首先我们获得一个reflect.Value值(后面会用到)。然后我们使用switch语句,根据这个值的reflect.Kind 来进行条件处理。如果这个值的类型是支持内置 len函数的Go语言内置类型,我们直接调用 reflect.Value.Len函数。否则,这个类型要么不支持获得长度,要么没有实现Len方法。我们使用reflect.Value.MethodByName方法来获得某个指定的方法,可能得到一个不合法的reflect.Value值,如果这个方法是合法的,我们就调用它。这里我们不需要传入任何参数,因为Len方法本身是不带参数的。

我们调用reflect.Value.MethodByName方法得到的reflect.Value值同时保存了方法和值,所以当我们调用 reflect.Value.Call时,这个值可以直接作为接收者(receiver)。

reflect.Value.Int方法返回一个int64类型的值,我们还必须将它转换成int型以匹配Len函数的返回值类型。

如果我们传入一个不支持内置 len函数的值,也没有 Len方法,那么就会抛出一个异常。当然我们也可以使用其他的方式来处理这些错误。例如,返回-1表明没有可用的长度,或者返回一个int和一个error值。

毫无疑问,Go语言的反射包是极其灵活的,允许我们在运行状态做很多事情。但是,引用Rob Pike的一句话[3]:“对于这种强大的工具,我们应当谨慎地使用它们,除非有绝对的必要。”

9.5 练习

本章有3个相互关联的练习,第一个练习要求创建一个自定义的包,第二个练习要求为这个包创建一个测试用例,第三个练习就是利用这个包来写一个程序。这3个练习的难度不断增加,尤其最后一个,很具有挑战性。

(1)创建一个包,比如叫my_linkutil(在文件my_linkutil/my_linkutil.go里)。同时这个包必须提供两个函数,第一个是 LinksFromURL(string) (string, error),给定一个URL字符串(如http://www.qtrac.eu/index.html),然后返回这个页面上消重后的所有链接(也就是标签的href属性值)和nil(或者返回nil和一个error,如果有错误发生的话)。第二个函数是LinksFromReader(io.Reader) (string, error),也是做相同的事情,只是从 io.Reader 里读取数据,比如可能是文件,或者一个 http.Response.Body。LinksFromURL函数可以调用LinksFromReader来完成大部分功能。

参考答案在linkcheck/linkutil/linkutil.go文件里,第一个函数大约11行代码,使用了 net/http 包的http.Get函数,第二个函数大概是 16 行代码,使用了regexp.Regexp.FindAllSubmatch函数。

(2)Go标准库提供了HTTP测试的支持(例如net/http/httptest包),不过我们这个练习只需测试第一题开发的my_linkutil.LinksFromReader函数。为该目的,请创建一个测试文件,比如 my_linkutil/my_linkutil_test.go,包含一个测试用例 TestLinksFromReader (*testing.T)。该测试从本地文件系统上读取一个HTML文件和一个包含该HTML文件所有唯一链接的链接文件,然后对比my_linkutil.LinksFromReader 函数分析HTML文件的结果和这个链接文件中的链接。

可以复制 linkcheck/linkutil/index.html 文件和linkcheck/linkutil/index.links文件到my_linkutil目录,用来当测试程序的数据文件。

参考答案在 linkcheck/linkutil/linkutil_test.go 里,答案里的测试函数大约 40行代码左右,使用sort.Strings函数对找到的结果进行排序,并使用reflect.DeepEqual函数将结果与预期的结果进行对比。如果测试失败,会列出不匹配的链接,方便测试人员测试。

(3)编写一个程序,比如叫my_linkcheck,从命令行读取一个URL(可以有http:// 前缀也可以没有),然后检查每个链接是否是有效的。程序可以使用递归,检查每个链接到的页面,但是不检查非HTTP链接、非HTTP文件及外部网站的链接。应该使用一个独立的goroutine来检查一个页面,这样能实现并发的网络访问,比顺序的一个一个来要快得多了。自然地,可能有多个页面都包含相同的链接,但我们只需要检查一次。这个程序应该使用第一道练习开发的my_linkutil包。

参考答案在linkcheck/linkcheck.go里,大约150行代码。为了避免检查重复的链接,参考答案里使用了一个映射来维护所有检查过了的URL 列表。这个映射在一个独立的goroutine里维护,并使用3个信道来和它通信:一个用于增加URL,一个用于查询URL是否存在,一个用于返回查询结果。(另外一种方法就是使用第7章的safemap。)下面是一个从命令行输入linkcheck www.qtrac.eu的结果(其中有些行已经被全部或部分的删除)。

+ read http://www.qtrac.eu

...

+ read http://www.qtrac.eu/gobook.html

+ read http://www.qtrac.eu/gobook-errata.html

...

+ read http://www.qtrac.eu/comparepdf.html

+ read http://www.qtrac.eu/index.html

...

+ links on http://www.qtrac.eu/index.html

+ checked http://ptgmedia.pearsoncmg.com/.../python/python2python3.pdf

+ checked http://www.froglogic.com

- can't check non-http link: mailto:[email protected]

+ checked http://savannah.nongnu.org/projects/lout/

+ read http://www.qtrac.eu/py3book-errata.html

+ links on http://www.qtrac.eu

+ checked http://endsoftpatents.org/innovating-without-patents

+ links on http://www.qtrac.eu/gobook.html

+ checked http://golang.org

+ checked http://www.qtrac.eu/gobook.html#eg

+ checked http://www.informit.com/store/product.aspx?isbn=0321680561

+ checked http://safari.informit.com/9780321680563

+ checked http://www.qtrac.eu/gobook.tar.gz

+ checked http://www.qtrac.eu/gobook.zip

- can't check non-http link: ftp://ftp.cs.usyd.edu.au/jeff/lout/

+ checked http://safari.informit.com/9780132764100

+ checked http://www.qtrac.eu/gobook.html#toc

+ checked http://www.informit.com/store/product.aspx?isbn=0321774637

...


[1].文档截屏展示了本书写作时的godoc的HTML渲染结果,现在可能已经发生了改变。

[2].旧版本的Go语言还包含有container/vector包。现在这个包被废弃了,可以使用切片和内置的append函数。

[3].关于Go语言的反射,Rob Pike写了一篇有趣而实用的博客,见blog.golang.org/2011/ 09/laws-of-reflection.html。