自定义包

在目前为此,所有的例子都是在单个main包。对于任何包,我们可以将它的代码分隔到多个文件中,只要这些文件在相同的目录中。比如,第八章的invoicedata例子就是使用的单个main包,它是由六个单独的文件组成(invoicedata.go, gob.go, inv.go, jsn.go, txt.go, and xml.go), 并且在每个文件的第一条语句为 package main.

对于大型的应用程序,我们可能想要将应程序的功能按照逻辑单元划分成不同的包。同样,我们创建的包会被多个相似的应用程序使用。单个应用程序使用的包和跨应用程序使用的包没有什么差别,但是,我们可以创建一个隐式的差别,将单个应用程序专用包放到我们应用程序的子目录下,而多个程序共享包可以放到GOPATH的source directory. GOPATH的原文件目录称为src; 在GOPATH下的每一个目录都应该包含一个src的目录,因为这是Go的工具(commands)所期望的。我们程序的原代码和包都应该保在GOPATH src目录下的子目录。

还可以直接将我们自定义的包安装到Go目录树中(i.e.,GOROOT目录下), 但最好不要这么做,如果Go是通过操作系统以包管理系统安装的(yum)或者installer安装的,那么会很不方便,即使是通过手动创建的。

创建自定义包

创建自定义包最好是在GOPATH src目录下(或者GOPATH src下的其中一个子目录)。单个应有程序专用包可以在应用程序的目录中创建,而分享类型的包应当直接在GOPATH src目录中创建。

按照约定,一个包的源代码是放在跟包名称相同的目录中。我们按自已喜欢的方式,将源代码划分到多个文件中。这些文件的名字可以是任意的(只要它们以.go结尾)。

第一章中的stacker例子中,包含了应用程序(在stacker.go)和一个应用程序专用包(stack in stack.go), 并用使用以下的目录结构

aGoPath/src/stacker/stack/stack.go

在这里,aGoPath是GOPATH的目录(如果只有一个),或才是环境变量划分隔符号(windows下是分号)分隔的GOPATH环境变量。

如果我们在stacker目录中,并且执行go build命令,我们将获得一个可执行文件stacker(windows下为stacker.exe). 可是,如果我们想在GOPATH bin目录中生成可执行文件,或者如果我们想让其它应用程序也可以使用stacker/stack包,我们必须使用go install.

当使用go install编译stacker程序,它将创建两个目录(如果不存在的情况下): GoPath/bin包含可执行的stacker程序,和aGoPath/pkg/linux_amd64/stacker, 它个目录包含了静态的stack包的二进制库文件。(windows下可能为windows_amd64, 这决定于操作系统和CPU架构)。

stack包可以通过在stacker程序使用import "stacker/stack"导入,这里给定的是完整的路径(Unix-style), 除了aGoPath/Src部分。实际上,GOPATH下的任何程序或者包都可以使用这种导入,因为Go不会区会应用程序专用的包和共享的包。

第六章的map排序(omap.go)在omap包中,它可以用于多个程序程序共享。为了避免命名冲突,我们创建了一个唯一命字的目录(在这里我们是使用域名), 以下是目录的结构

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

我们任何程序(只要它们在GOPATH路径下)都可以通过导入"qtrac.eu/omap"来使用ordered map package. 如果我们还有其它想要共享的包,我们都可以放在aGoPath/src/qtrac.eu目录里。

当使用go install编译omap包时,将创建一个aGoPath/pkg/linux_amd64/qtrac.eu目录(如果不存在),创建的这个目录会包含omap包的二进制库文件。

如果我们要想在其它包中创建一个包,首先我们需要创建一个包的目录,比如,aGopath/src/my_package. 然后在这个目录下,我们为每一个包的子目录,比如,aGoPath/src/my_package/pkg1和aGoPath/src/my_package/pkg2, 以及它们想应的文件aGoPath/src/my_package/pkg1/pkg1.go and aGoPath/src/my_package/pkg2/pkg2.go. 然后在导入阶段,比如想要导入pkg2, 我们可以使用"my_package/pkg2". Go源代码中的archive包就是使用的这种方式。这个包自已也可以有文件(比如我们这个例子,aGoPath/src/my_package/my_package.go).

Go包的导入可以从GOROOT($GOROOT/pkg/${GOOS}_${GOARCH}, e.og., /opt/go/pkg/linux_amd64),或者从GOPATH环境变量下的目录。这就意味着可能会导致命名冲突,最简单的避免命名冲突是在GOPATH路径中使用唯一目录,比如omap包中使用的域名命名。

特定平台的代码

在某些情况下,我们需要不同平台的代码。比如,在类Unix的系统的shell中可以使用通配符,所以命令行中的*.txt,可以在程序接受,如["README.txt", "INSTALL.txt"]保存在os.Args[1:] slice. 但是在Windows系统,程序仅获得["*.txt"]。我们在Windows上使用filepath.Glob()函数达到同样的通配符功能。

在程序运行时决定是否使用filepath.Glob()的解决方案是通过runtime.GOOS == "windows" 来判断当前运行时的操作系统为windows,更多的使用可以查看cgrep1/cgrep.go. 另一个解决方案是不同平台的代码放到对应的文件中。比如,cgrep3程序是由三个文件组成,cgrep.go,unix_linux.go, util_windows.go. 在util_linux.go只有一个函数的定义

func commandLineFiles(files []string) []string { return files }

很清楚的,这个函数没有任何文件通配符,这是因为在linux中不需要。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) // Invalid pattern
        } else if matches != nil { // At least one match
            args = append(args, matches...)
        }
    }
    return args
}

当我们使用go build编译cgrep3时,在linux的机器上会,util_linux.go文件将被编译,而util_windows.go文件将会被忽略——反过来在windows机器是则是相反的。

在Mac OS X和FreeBSD系统上,util_linux.go和util_windows.go都将不会被编译,所以整个程序编译失败。由于这两个系统上的shell都有globbing的功能,我们可以做util_linux.go的软链接,或者复制util_linux.go为util_darwin.go和util_freebsd.go(Go支持的其它平台也是类似).

包的文档

包,特别是对于打算分享的包,需要像样的文档。Go提供了一个文档工具,godoc, 它可以用来在控制台显示包的文档和函数,或者作为一个web服务器,使得文档以网页的形式显示。如果这个包在GOPATH下,godoc将自动的找到它,并且在网页的左侧提供一个到这个包的链接。如果这个包没有在GOPATH下,可以运行godoc -path选项,将包的路径赋值给它。

什么是好的文档是一个有争议的话题,所以在这个子章节中,我们将只纯粹的关注构造包文档结构。

默认情况下,godoc只显示导出的types, classes, constants以及variables. 所以这些都应该被文档化。文档是直接写在源文件中的。

// 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语句之前添加整个包的描述,而第一句话是整个包的摘要。这个例子是摘取的omap包(在文件qtrac.eu/omap/omap.go, 这个包在第六章中讲过)

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

这个文档用于导出的类型,必须写在type语句之前,并且指名这个类型的零值是否有效。

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

这段文档用于函数和方法,它们必须写在函数的第一条语句之前,

图9.2展示了godoc之后的函数文档效果。这个图也向我们说明了,注释中的缩进会成渲染为HTML的code。可是,在写这本书的时候,godoc还不支持任何的标记(比如bold, italic, links).

// NewCaseFoldedKeyed returns an empty Map that accepts case-insensitive
// string keys.
func NewCaseFoldedKeyed() *Map {

这面这段展示了一个便利的构造函数的文档

// 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中有一个约定是函数和方法的文档,都是在这个函数或者方法的名称开始, 并且不要加上圆括号。

###包的单元测试和基准测试

Go的标准库中提供了testing包,用于做单元测试。设置一个包的单元测试是一件很简单的事情,只需要在跟包的相同目录中创建一个测试文件。这个文件的名称应该以包的名字开始,而以_test.go结尾。比如omap包的测试文件为omap_test.go.

在本书的例子中,测试文件都放在它们自己的包中(e.g., omap_test), 同时在这些测试文件中导入它们要测试的包和testing package,和其它任何它们需要的包。这就约束我们使用黑盒测试。但有时,有的程序员更喜欢使用白盒测试。这可以简单的通过将测试文件放到要测试的包中来完成。后一种方法意味着非导出类型也可以被测试,同时方法也可以添加到非导出的具体类型,以便测试。

测试文件有点不同,它没有main()函数,而是由一个或者多个导出的函数组成,这些函数的名字以Test开头,接收单个的\*testing.T类型的参数, 并且没有返回值。我们可以添加任意的辅助函数,只是名字不需要以Test开头。

```go
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()方法遍历map中所有的键值对,并且将它的值添加到一个string slice中。最后,我们这个slice连结成一个字符串,并且看它是否与我们期望的字符串相等。如果不相等,我们调用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

如果我们改变字符串常量,强制要求测试失败,并且在次运行这个测试(默认情况,没有详细信息verbose), 以下是它的输出

$ 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)
    }
}

这个函数开始时停止了计时器,是因为我们不想在创建和填充omap.Map时进行计时,然后我们创建一个空的omap.Map,并且向它添加了100万的键值对。

默认时,go test不会进行基准测试,所以如果我们想要进行基准测试,必须使用-test.bench选项,并且向它提供一个正则表达式,告诉它我们想要进行的基准测试的名字。正则表达式 .*匹配所有的基准测试,同样纯 . 也是一样匹配所有的基准测试函数。

$ go test -test.bench=.
PASS qtrac.eu/omap
PASS
BenchmarkOMapFindSuccess-4 1000000 1380 ns/op
BenchmarkOMapFindFailure-4 1000000 1350 ns/op

输出的结果显示了两个基准测试运行1000000循环的遍历,并且给定了每次操作所花的时间纳秒(nanoseconds, 十亿分之一秒)。我们也可以使用-test.benchtime选项,以秒为单位,大概的表示每个benchmark所花的时间。

包的导入

Go允午我们对包取别名。这个特性非常方便和实用——比如,使它容易在一个包的两个不同实现之间进行切换。例如,我们可以使用import bio "bio_v1"导入一个包,所以在我们的代码中,可以使用bio访问bio_v1包,而不是bio_v1. 之后当这个包有更加成熟的实现时,我们可以修改import语句为import bio "bio_v2"到新的版本。如果bio_v1和bio_v2提供了相同的API,则之后的代码都可以不需要进行修改。另一方面,最好避免对标准库中的包进行别名,因为这会导致之后的代码的维护者产生混乱。

如我们在第5章中讲到的,当一个包被导入后,它的init()函数会被执行(如果有的话)。在有些情况下,我们不想明确的使用一个包,但是想要init()函数运行。

比如,如果我们在处理图片时,我们可能想要注册所有Go支持的图片格式,但实际上不会使用这个图片格式包提供了任何功能。以下是imagetag1程序的导入语句(Chapter 7中的练习imagetag1.go).

import (
"fmt"
"image"
"os"
"path/filepath"
"runtime"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)

这里我们导入了image/gif, image/jpeg, and image/png包后只是执行init()函数,init()函数会为image包注册它们的格式。这些包的别名都空白标识符。