包和工具
包的概念
package概念是为大型程序的可维护性设计的,而且代码模块化方便共享和重用。
package一般都有它的命名空间,从而可以给类型、函数选择简短的名字,也不至于和其他package产生名字冲突,因为使用时可以带上包前缀限定。
package可以控制包内名字的可见性和是否导出来实现封装特性,这样API设计者可以隐藏具体实现,可以在不影响包的使用者的前提下调整包的内部实现,只要导出的部分对外的服务保持一致。
golang对于导入的包,必须在每个文件开头显式声明,从而编译器只需要根据这个信息来判断依赖关系。golang禁止环状依赖,包的依赖关系是有向无环图,每个包可以被独立编译,多个包可以并发编译。golang目标文件既记录了包本身的导出信息(其他使用该包的执行代码可以找到函数地址等),还记录了包的依赖关系(编译一个包,只需要读取每个直接导入包的目标文件,不需要遍历所有依赖的文件,既间接依赖的文件不需要被涉及,对一个包修改后重新编译一下就非常快)。
导入路径
每个包用一个全局唯一的字符串所标识的导入路径来定位,也就是 import 语句中的字符串。golang 语言规范没有规定导入路径的具体规则,导入路径具体含义是构建工具负责解释的。
使用模块方式,go.mod 文件登记了导入的包(go.sum则记录了这些包的校验和)。对于外部包,https://pkg.go.dev 上登记了,常见的repository是github,标准库或本地包,则类似目录路径。
为了便于分享和发布,最好把自己的包的导入路径设置为全球唯一,常见做法是用所在组织的互联网域名作为前缀。对于个人开发者,可以用 github.com (或者 gitee.com)作为前缀,后面就是项目名。
包声明
一般最终的可执行文件对应包声明为 package main,而其他的根据对应功能声明,如 math/rand 包的每个源文件开头都会声明 package rand,意思是这些文件都隶属于 rand包,或者说这些文件都在rand命名空间下,使用方只需要 import "math/rand" 后,就可以 rand.Int、rand.Float64 等方式访问包的成员。
通常,默认包名就是包导入路径名的最后一段,这样,即使两个包的导入路径不同,它们依然可能具有相同的包名,例如 math/rand 包 和 crypto/rand 包。
不符合上述规则的有3中情况:
第一种就是前面说的main包,声明package main会提示构建工具需要调用连接器生成可执行程序。
第二种是包所在的目录中可能有 _test.go 为后缀的 go源码,并且源文件声明的包名也是以 _test 为后缀名的。这种目录其实包含了两种包:一种是普通包,另一种则是测试的外部扩展包。所有以 _test 为后缀包名的测试外部扩展包都由 go test 命令独立编译,普通包和测试的外部扩展包是相互独立的。
第三种是导入路径最后一段包含了追加的版本号信息,例如 gopkg.in/yaml.v2,这时候,包名是不包含版本号后缀的,所以包名是 yaml 而非 yaml.v2。
简言之,例外的3中情况是 main、测试、带版本号。
导入声明
前面的两个包名都是 rand 的情况,必须为其中一个使用别名,例如
import (
"crypto/rand"
mrand "math/rand" // alternative name mrand avoids conflict
)
别名机制除了可以应对包名冲突,还可以应对包名和本地普通变量的冲突,以及简化过于笨重的包名。
如果产生循环导入,golang构建工具将报告错误。
包的匿名导入
golang不允许变量声明了但不使用,也不允许包导入了但不使用。所谓包的匿名导入,就是导入包时将包重命名为_,然后不显式使用该包(实际上_为空白标识符,也无法访问该包)。
import _ "image/png" // register PNG decoder
匿名导入包的作用是一种编译时机制,相当于导入该包,该包的 init() 初始化函数会自动得到执行,init()里面通常是注册一些驱动之类,这样,相应的功能就被支持。前面的匿名导入语句可以让image包支持png格式,从而image.Decode可以解码png格式。
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)
}
数据库包 database/sql 也是采用了类似的技术
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"
go-mysql-driver/mysql 的 driver.go 文件包含下述代码
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
// ...............
}
func init() {
sql.Register("mysql", &MySQLDriver{})
}
感觉这用法就是一种工厂模式。
包和命名
包名在不影响前提下要选择短小的包名,包名应当有描述性且无歧义。例如,温度转换的包名,用temperature不够准确,而且太长,用 temp 则很容易和常用的临时变量命名混淆,用 tempconv就可以比较短小的同时描述清楚包的作用。不能做到所有名字短小,特别是名字很多时,一般让最常用的导出成员名字尽量短小。
包名应该避开常用的局部变量名字,不然经常要出现重命名情况。包名一般用单数,而标准库用了一些复数,如 bytes、errors、strings,是为了避免和预定义的类型冲突。
工具
GOPATH环境变量指定了当前工作目录,要切换到不同工作区,只要更新GOPATH就可以了。
$ export GOPATH=$HOME/gobook
$ go get gopl.io/...
设置了GOPATH,再用 go get gopl.io/... 下载了该书源码后,工作区目录结构类似
GOPATH/
src/
gopl.io/
.git/
ch1/
helloworld/
main.go
dup/
main.go
...
golang.org/x/net/
.git/
html/
parse.go
node.go
...
bin/
helloworld
dup
pkg/
darwin_amd64/
...
环境变量 GOROOT 指定了 Go的安装目录以及自带的标准库包的位置。GOROOT的目录结构类似GOPATH。GOROOT一般是安装的时候自动确定的,用户不需要考虑。
用 go env 可以查看所有环境变量,例如此时使用的 win7 下面 go env 输出的其中两行是
set GOPATH=C:\Users\zime\go
set GOROOT=C:\Program Files\Go
用 go get 下载包,如果用 ... 可以下载整个子目录里面的每个包(参考前面的go get gopl.io/...)。如果go get 下载的是 git管理的包,那么它相当于 git clone 了一份代码,完全可以使用 git 来管理本地仓库的。
导入路径包含的网站域名和真实的git仓库对应的远程服务地址有可能不同,只要导入的网站页面的元数据中包含了真实地址的指向,例如 https://golang.org/x/net 指向git仓库 https://go.googlesource.com/net
$ go build gopl.io/ch1/fetch
$ ./fetch https://golang.org/x/net/html | grep go-import
<meta name="go-import"
content="golang.org/x/net git https://go.googlesource.com/net">
go get 时,如果包在本地已经存在某个版本,就不会再下载,要确保下载最新版本,可以使用 -u 选项,它将确保所有包和依赖的包的版本都是最新的,然后重新编译和安装它们。一般第一次下载包用最新版是合适的,对于已经发布的程序,有可能产生依赖问题。
构建包是用 go build,可以指定路径(用导入路径,是相对于 $GOPATH/src 的),不指定就是当前目录对应的包。
go install 命令则保存每个包的编译成果,保存到 $GOPATH/pkg 目录下,可执行程序保存到 $GOPATH/bin 目录。将 $GOPATH/bin 添加到 PATH,相当于用户扩展的命令了。go build -i 命令在构建的同时会安装每个目标依赖的包。
golang 是支持交叉编译的,只需要 go build 命令前用 GOARCH 指定架构
GOARCH=386 go build gopl.io/ch10/cross
可以用下述代码了解 go程序针对的操作系统和架构
func main() {
fmt.Println(runtime.GOOS, runtime.GOARCH)
}
一些文件会用后缀指示它是针对哪种操作系统或者处理器架构的,例如 net_linux.go、asm_amd64.s,构建工具将只在对应平台编译这些文件。另外,可以用特殊的构建注释参数提供更多控制。
良好的包风格应该包含良好的包文档。包中每个导出的成员和包声明前都应该包含目的和用法,如果包的说明太长了,可以放入单独的源文件中,通常是 doc.go。go doc 命令可以打印某个包,如time(或包中某个实体,例如 time.Since)的声明和文档注释。 godoc 工具则用网页方式展示文档。
内部包是介于完全导出和完全私有之间的状态。一个 internal包,它的路径中是包含了 internal名字的,golang构建工具对包含internal名字的路径段的包导入路径会特殊处理,即一个internal包只能被和internal目录有同一个父目录的包所导入。简单说,下面的包中,chunked是http的“儿子”,http可以用“儿子” chunked 的东西,url 不能用 chunked,但 url 可以用 httputil。
net/http
net/http/internal/chunked
net/http/httputil
net/url
go list 命令可以查询包,用 ... 可以通配。