文章目录
包简介

任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。
当我们修改了一个源文件时,必须重新编译该源文件对应的包和所有依赖该包的包。即使是从头开始构建,Go 的编译器的编译速度也明显快于其他编译型语言,这得益于三个语言特性:
- 所有导入的包必须在每个文件的开头显示声明(
import
语句),这样编译器就没必要读取和分析整个源文件来判断包的依赖关系; - 禁止包的循环依赖,包的依赖关系将形成一个有向无环图,每个包都可以被独立编译,并且很有可能被并发编译;
- 编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。
导入路径
每个包是由一个全局唯一的字符串来标识导入路径:
go
import (
"fmt"
"math/rand"
"encoding/json"
"golang.org/x/net/html"
"github.com/go-sql-driver/mysql"
)
包声明
在每个 Go 源文件开头,必须有包声明语句(.go
文件第一行的package ...
)。包声明语句的主要目的是确定当前包被其他包导入时的默认标识符(也称为包名)。
例如,math/rand
包每个源文件开头的都包含package rand
包声明语句,所以我们导入了这个包之后,可以通过rand.Int/rand.Float64
来访问包内的成员。
go
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Int())
}
通常来说,默认的包名就是包路径的最后一段,比如math/rand
和crypto/rand
的包名都是rand
。在后面我们将会看到如何同时导入两个有相同包名的包。
默认包名一般采用导入路径的最后一段,但也有三种例外情况:
- 包对于一个可执行程序,也就是 main 包,此时 main 包本身的导入路径无关紧要。名为 main 的包是给
go build
构建命令的一个信息,这个包编译之后必须调用连接器生成一个可执行程序; - 包所在的目录中可能有一些文件以
xxx_test.go
为后缀的 Go 源文件,且这些源文件声明的包名以_test
为后缀。这种目录可能包含两种包:一种是普通包,一种是测试的外部拓展包。所有以xxx_test
为后缀包名的测试外部拓展包都由go test
命令独立编译,普通包和测试的外部拓展包相互独立; - 一些依赖版本号的管理工具会在导入路径和追加版本号信息,例如
"gopkg.in/yaml.v2"
,该情况下包名不包含版本号后缀,仅为yaml
。
导入声明
以下两种导入形式等价,后者更为常见:
go
import "fmt"
import "os"
import (
"fmt"
"os"
)
导入包之间通过添加「空行」来分组,通常来自不同组织的包独立分组。但包的导入顺序以及是否分组其实无关紧要,只是方便开发者对包进行管理。
go
import (
"fmt"
"html/template"
"os"
"golang.org/x/net/html"
"golang.org/x/net/ipv4"
)
方才提到,对于math/rand
和crypto/rand
,它们的包名都是rand
,那么导入声明就必须至少为一个同名包指定新的包名,以避免冲突:
go
import (
"crypto/rand"
mrand "math/rand" // alternative name mrand avoids conflict
)
导入包的重命名只影响当前的源文件。
包的匿名导入
如果仅仅导入包而不在代码当中使用,Go 的编译器会报编译错误。但有时候我们只想利用导入包产生的副作用:它会产生包级变量的初始化表达式,并执行导入包的init
初始化函数(一个典型的例子就是在使用sqlx
这个包的时候,需要显式地匿名导入与mysql
数据引擎相关的包来完成数据引擎初始化)。
可以使用_
进行包的重命名来完成包的匿名导入:
go
import _ "image/png"
工具
Go 语言的工具箱集合了是一个集合了一系列功能的命令集。它可以被视为一个包管理器,用于包查询、计算包的依赖、从远程版本控制系统下载它们等待的任务。它同时是一个构建系统,用于计算文件之间的依赖关系,然后调用编译器、汇编器和链接器构建程序。
Go 工具箱中最常用的命令如下:
plain
$ 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 get
可以下载一个单一的包,或调用...
下载整个子目录当中的每个包。
在 Goland 这个 IDE 当中,更简单的方式是直接在 Go 文件的 import 中写入我们要使用的包的字符串,然后导入依赖即可。
构建包
go build
命令编译命令行参数指定的包。如果包是一个库,则忽略输出结果,这可以用于检测包是否可以正确编译。如果包名是 main,则go build
调用链接器在当前目录生成一个可执行程序,以导入路径的最后一段作为可执行程序的名字。
包文档
Go 的编码风格鼓励为每个包提供良好的文档。包中每个导出的成员和包声明前都应该包含目的和用法说明的注释。
下例是fmt.Fprintf
的文档注释:
go
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
内部包
在 Go 当中,包是最重要的封装机制。基于类型、函数、结构成员以及结构方法的名称的首字母是否大写,Go 语言来确定这个对象在当前包中是否是导出的。
只有导出的类型(包括结构成员)或函数(方法)才是对包外可见的。
有时候我们计划把一个大包拆分为若干个更容易维护的子包,但是我们不希望内部的子包结构完全暴露出去。同时,我们还希望在内部子包之间共享一些通用的处理包,或许我们只是想试验一个新包的尚不稳定的接口,暂时只暴露给部分用户使用。
为了满足上述需求,Go 的构建工具对保护internal
名字的路径段的包导入路径做了特殊处理,这种包叫做 internal 包,它只能被和internal
目录有着相同父目录的包导入。例如:
plain
net/http
net/http/internal/chunked
net/http/httputil
net/url
net/http/internal/chunked
内部包只能被net/http/httputil
或net/http
包导入,但是不能被net/url
包导入。不过net/url
包却可以导入net/http/httputil
包。