Go语言自定义包

到目前为止,我们见过的所有例子都是以一个包的形式存在的,也就是 main 包。在 Go语言里,允许我们将同一个包的代码分隔成多个小块来单独保存,只需要将这些文件放在同一个目录即可。

对于更大的应用程序,我们可能更喜欢将它的功能性分隔成逻辑的单元,分别在不同的包里实现。或者将一些应用程序通用的那一部分剖离出来。

Go语言里并没有限制一个应用程序能导入多少个包或者一个包能被多少个应用程序共享,但是将这些应用程序特定的包放在当前应用程序的子目录下和放在 GOPATH 源码目录下是不大一样的。

我们所说的 GOPATH 源码目录是一个叫 src 的目录,每一个在 GOPATH 环境变量里的的目录都应该包含这个目录,因为 Go语言的工具链就是要求这样做的。我们的程序和包都应该在这个 src 目录下的子目录里。

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

创建自定义的包

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

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

在前面讲解的 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 下的包没有共享和专用之分。

又比如实现的有序映射是在 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 路径下的包是一样的,下面几个小节我们来讨论一些平台特定的代码。

平台特定的代码

在某些情况下,我们必须为不同的平台编写一些特定的代码。例如,在类 Unix 的系统上,通常 shell 都支持通配符(也叫 globbing),所以在命令行输入 *.txt,程序就能够从 os.Args[1:] 切片里读取到比如 ["README.txt", "INSTALL.txt"] 这些值。

但是在 Windows 平台上,程序只会接收到我们可以使用 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 平台的程序了。

文档化相关的包

如果我们想共享一些包,为了方便其他的开发者使用,需要编写足够的文档才行。Go语言提供了非常方便的文档化工具 godoc,可以在命令行显示包的文档和函数,也可以作为一个 Web 服务器启动,如下图所示。

omap 包的文档
图:omap 包的文档

godoc 会自动搜索 GOPATH 路径下的所有包并将它显示出来,如果某些包不在 GOPATH 路径下,可以使用 -path 参数(除此之外还要有 -http 参数)来指定。

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

在默认情况下,只有可导出的类型、类、常量和变量才会在 godoc 里出现,因此全部这些内容应该添加合适的文档。文档都是直接包含在源文件里。这里以 omap 包为例。

// 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, p := a.(Point), b.(Point)
//                 if a.X != p.X {
//                     return a.X < p.X
//                 }
//                 return a.Y < p.Y
//             })
func New(less func(interface{}, interface{}) bool) *Map {

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

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

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


omap 包中 New ()函数的文档
图: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语言里的文档描述通常是以函数或者方法的名字开头的,这是一个惯例,还有(这个不是惯例)就是在文档中引用函数或者方法时不使用圆括号。

导入包

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 的超集,这样其余所有的的代码都不需要做任何改动。另外,最好就不要对官方标准库的包使用别名,因为这样可能会导致一些混淆或激怒后来的维护者。

当导入一个包时,它所有的 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语言不会发出导入了某个包但是没有使用的警告。