Appearance
The Go Programming Language - 包
1. 概述
Go 语言有超过 100 个的标准包(可以用 go list std | wc -l 命令查看标准包的具体数目),标准库为大多数的程序提供了必要的基础构件。在 Go 的社区,有很多成熟的包被设计、共享、重用和改进,目前互联网上已经发布了非常多的 Go 语言开源包,它们可以通过 http://godoc.org 检索。
在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。
2. 包命名
下面是一些关于 Go 语言独特的包和成员命名的约定:
- 当创建一个包,一般要用短小的包名,但也不能太短导致难以理解;标准库中最常用的包有
bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。 - 包名一般采用单数的形式;标准库的
bytes、errors和strings使用了复数形式,这是为了避免和预定义的类型冲突,同样还有go/types是为了避免和type关键字冲突。
要避免包名有其它的含义。例如,一个关于温度转换的包最初可能使用了 temp 包名,但这是一个糟糕的尝试,因为 temp 也经常用来表示一个临时变量。或者可以使用 temperature 作为包名,但是该名字并没有表达包的真实用途。其实我们可以参考 strconv 标准包,将其修改为类似的 tempconv 的包名,这个名字比之前的就好多了。
我们除了需要关注包的命名,还需要注意如何命名包的成员。由于是通过包的导入名字引入包里面的成员,例如 fmt.Println,同时包含了包名和成员名信息。因此,我们一般并不需要关注 Println 的具体内容,因为 fmt 包名已经包含了这个信息。当设计一个包的时候,需要考虑包名和成员名两个部分如何很好地配合。下面有一些例子:
Text
bytes.Equal flag.Int http.Get json.Marshal有一些包,可能只描述了单一的数据类型,例如 html/template 和 math/rand 等,只暴露一个主要的数据结构和与它相关的方法,还有一个以 New 命名的函数用于创建实例:
Go
package rand // "math/rand"
type Rand struct{ /* ... */ }
func New(source Source) *Rand1
2
3
4
2
3
4
3. 包注释
在每个源文件的包声明前紧跟着的注释是包注释。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(译注:如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的 doc.go 文件中。
4. 导入包
一个导入路径代表一个目录中的一个或多个 Go 源文件。
按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如 gopl.io/ch2/tempconv 包的名字一般是 tempconv。
5. 内部包
当我们计划将一个大的包拆分为很多小的更容易维护的子包,但是我们并不想将内部的子包结构也完全暴露出去,同时,我们可能还希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。
为了满足这些需求,Go 语言的构建工具对包含 internal 名字的路径段的包导入路径做了特殊处理。这种包叫 internal 包,一个 internal 包只能被和 internal 目录有同一个父目录的包所导入。例如,net/http/internal/chunked 内部包只能被 net/http/httputil 或 net/http 包导入,但是不能被 net/url 包导入。不过 net/url 包却可以导入 net/http/httputil 包。
6. 包的初始化
包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:
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 }1
2
3
4
5
2
3
4
5
如上方的 b 变量,我们可以使用表达式对其初始化。除此之外我们还可以使用 init 初始化函数,而且每个文件都可以包含多个 init 初始化函数:
Go
func init() { /* ... */ }init 初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的 init 初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
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))])
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
还可以通过将初始化逻辑包装为一个匿名函数处理,像下面这样:
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
}()1
2
3
4
5
6
7
2
3
4
5
6
7
7. 包的匿名导入
如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包的一些特性:它会计算包级变量的初始化表达式和执行导入包的 init 初始化函数。这时候我们需要抑制 “unused import” 编译错误,我们可以用下划线 _ 来重命名导入的包。像往常一样,下划线 _ 为空白标识符,并不能被访问。
Go
import _ "image/png" // register PNG decoder这个被称为包的匿名导入。它通常是用来实现一个编译时机制,然后通过在 main 主程序入口选择性地导入附加的包。首先,让我们看看如何使用该特性,然后再看看它是如何工作的。
标准库的 image 图像包包含了一个 Decode 函数,用于从 io.Reader 接口读取数据并解码图像,它调用底层注册的图像解码器来完成任务,然后返回 image.Image 类型的图像。使用 image.Decode 很容易编写一个图像格式的转换工具,读取一种格式的图像,然后编码为另一种图像格式:
Go
// 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})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
如果我们将一个 PNG 格式的文件导入到这个程序的标准输入,它将解码输入的 PNG 格式图像,然后转换为 JPEG 格式的图像输出。
Bash
$ go build gopl.io/ch3/mandelbrot
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
Input format = png1
2
3
4
2
3
4
要注意 image/png 包的匿名导入语句。如果没有这一行语句,虽然程序依然可以编译和运行,但是它将不能正确识别和解码 PNG 格式的图像:
Bash
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
jpeg: image: unknown format1
2
3
2
3
下面的代码演示了它的工作机制。标准库还提供了 GIF、PNG 和 JPEG 等格式图像的解码器,用户也可以提供自己的解码器,但是为了保持程序体积较小,很多解码器并没有被全部包含,除非是明确需要支持的格式。image.Decode 函数在解码时会依次查询支持的格式列表。每个格式驱动列表的每个入口指定了四件事情:
- 格式的名称;
- 一个用于描述这种图像数据开头部分模式的字符串,用于解码器检测识别;
- 一个
Decode函数用于完成解码图像工作; - 一个
DecodeConfig函数用于解码图像的大小和颜色空间的信息;
每个驱动入口是通过调用 image.RegisterFormat 函数注册,一般是在每个格式包的 init 初始化函数中调用,例如 image/png 包是这样注册的:
Go
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)
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
最终的效果是,主程序只需要匿名导入特定图像驱动包就可以用 image.Decode 解码对应格式的图像了。
数据库包 database/sql 也是采用了类似的技术,让用户可以根据自己需要选择导入必要的数据库驱动。例如:
Go
import (
"database/sql"
_ "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"1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9