Skip to content

打包和工具链

在 Go 语言里,包是个非常重要的概念。其设计理念是使用包来封装不同语义单元的功能。
这样做,能够更好地复用代码,并对每个包内的数据的使用有更好的控制。

所有 Go 语言的程序都会组织成若干组文件,每组文件被称为一个
这样每个包的代码都可以作为很小的复用单元,被其他项目引用。
让我们看看标准库中的 http 包是怎么利用包的特性组织功能的:

1
2
3
4
5
6
7
8
9
net/http/
    cgi/
    cookiejar/
        testdata/
    fcgi/
    httptest/
    httputil/
    pprof/
    testdata/

这些目录包括一系列以.go为扩展名的相关文件。
这些目录将实现 HTTP 服务器、客户端、测试工具和性能调试工具的相关代码拆分成功能清晰的、小的代码单元。
以 cookiejar 包为例,这个包里包含与存储和获取网页上的 cookie 相关的代码。
每个包都可以单独导入和使用,以便开发者可以根据自己的需要导入特定功能。
例如,如果要实现 HTTP 客户端,只需要导入 http 包就可以。

所有的.go文件,除了空行和注释,都应该在第一行声明自己所属的包。每个包都在一个单独的目录里。
不能把多个包放到同一个目录中,也不能把同一个包的文件拆到多个不同目录中。
这意味着,同一个目录下的所有.go文件必须声明同一个包名

每个包都对应一个独立的名字空间。例如,在 image 包中的 Decode 函数和在 unicode/utf16 包中的 Decode函数是不同的。
要在外部引用该函数,必须显式使用 image.Decodeutf16.Decode 形式访问。

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。
在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。

为了演示包基本的用法,先假设我们的温度转换软件已经很流行,我们希望到Go语言社区也能使用这个包。我们该如何做呢?

包名惯例

给包命名的惯例是使用包所在目录的名字。这让用户在导入包的时候,就能清晰地直到包名。
我们继续以net/http包为例,在 http 目录下的所有文件都属于 http 包。
给包及其目录命名时,应该使用简洁、清晰且全小写的名字,这有利于开发时频繁输入包名。
例如,net/http包下面的包,如 cgi、httputil 和 pprof,名字都很简洁

记住,并不需要所有包的名字都与别的包不同,因为导入包时是使用全路径的,所以可以区分同名的不同包。
一般情况下,包被导入后会使用你的包名作为默认的名字,不过这个导入后的名字可以修改。
这个特性在需要导入不同目录的同名包时很有用。

main 包

在 Go 语言里,命名为 main 的包具有特殊的含义。
Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。
所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。

当编译器发现某个包的名字为 main 时,他一定也会发现名为 main() 的函数,否则不会创建可执行文件。
main() 函数时程序的入口,所以,如果没有这个函数,程序就没有办法开始执行。
程序编译时,会使用声明 main 包的代码所在的目录的目录名作为二进制可执行文件的文件名。

命令和包

Go 文档里经常使用命令(command)这个词来指代可执行程序,如命令行应用程序。这会让新手在阅读文档时产生困惑。
记住,在 Go 语言里,命令是指任何可执行程序。作为对比,包更常用来指语义上可导入的功能单元。

获取包的文档

别忘了,可以访问https://golang.org/pkg/fmt/或者在终端输入godoc fmt来了解更多关于 fmt 包的细节。

导入

我们已经了解如何把代码组织到包里,现在让我们来看看如何导入这些包,以便可以访问包内的代码。
import 语句告诉编译器到磁盘的哪里去找想要导入的包。
导入包需要使用关键字 import,它会告诉编译器你想引用该位置的包内的代码。
如果需要导入多个包,习惯上是将 import 语句包装在一个导入块中

例:

1
2
3
4
import (
    "fmt"
    "strings"  // strings 包提供了很多关于字符串的操作,如查找、替换或者变换。可以通过访问 https://golang.org/pkg/strings/ 或者在终端运行 godoc strings 来了解更多关于 strings 包的细节
)

编译器会使用 Go 环境变量设置的路径,通过引入的相对路径来查找磁盘上的包。
标准库中的包会在安装 Go 的位置找到。Go 开发者创建的包会在 GOPATH 环境变量指定的目录里查找。
GOPATH 指定的这些目录就是开发者的个人工作空间。

举个例子。如果 Go 安装在/usr/local/go,并且环境变量 GOPATH 设置为/home/myproject:/home/mylibraries,编译器就会按照下面的顺序查找net/http包:

1
2
3
/usr/local/go/src/pkg/net/http    这就是标准库源代码所在的位置
/home/myproject/src/net/http
/home/mylibraries/src/net/http

一旦编译器找到一个满足 import 语句的包,就停止进一步查找。
有一件重要的事需要记住,编译器会首先查找 Go 的安装目录,然后才会按顺序查找 GOPATH 变量里列出的目录

如果编译器查遍 GOPATH 也没有找到要导入的包,那么在试图对程序执行 run 或者 build 的时候就会出错。

远程导入

目前的大势所趋是,使用分布式版本控制系统(Distributed Version Control Systems, DVCS)来分享代码,如 GitHub、Launchpad 还有 Bitbucket。
Go 语言的工具链本身就支持从这些网站及类似网络获取源代码。
Go 工具链会使用导入路径确定需要获取的代码在网络的什么地方。

例如:

1
import "github.com/spf13/viper"

用导入路径编译程序时,go build 命令会使用 GOPATH 的设置,在磁盘上搜索这个包。
事实上,这个导入路径代表一个 UR,指向 GitHub 上的代码库。
如果路径包含 URL,可以使用 Go 工具链从 DVCS 获取包,并把包的源代码保存在 GOPATH 指向的路径里与 URL 匹配的目录里。
这个获取过程使用 go get 命令完成。go get 将获取任意指定的 URL 的包,或者一个已经导入的包所依赖的其他包。
由于 go get 的这种递归特性,这个命令会扫描某个包的源码树,获取能找到的所有依赖包。

命名导入

如果要导入的多个包具有相同的名字,会发生什么?
例如,既需要 network/convert 包来转换从网络读取的数据,又需要 file/convert 包来转换从文本文件读取的数据时,就会同时导入两个名叫 convert 的包。
这种情况下,重名的包可以通过命名导入来导入。
命名导入是指,在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。

例如,若用户已经使用了标准库里的 fmt 包,现在要导入自己项目里名叫 fmt 的包,就可以通过如下代码所示的命名导入方式,在导入时重新命名自己的包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
    "fmt"
    myfmt "mylib/fmt"
)

func main() {
    fmt.Println("Standard Library")
    myfmt.Println("mylib/fmt")
}

当你导入了一个不在代码里使用的包时,Go 编译器会编译失败,并输出一个错误。
Go 开发团队认为,这个特性可以防止导入了未被使用的包,避免代码变得臃肿。
虽然这个特性会让人觉得很烦,但 Go 开发团队仍然花了很大的力气说服自己,决定加入这个特性,用来避免其他编程语言里常常遇到的一些问题,如得到一个塞满未使用库的超大可执行文件。
很多语言在这种情况会使用警告做提示,而 Go 开发团队认为,与其让编译器告警,不如直接失败更有意义。
每个编译过大型 C 程序的人都知道,在浩如烟海的编译器警告里找到一条有用的信息是多么困难的一件事。
这种情况下编译失败会更加明确。

有时,用户困难需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_来重命名这个导入。

空白标识符

下划线字符(_)在 Go 语言里称为空白标识符,有很多用法。
这个标识符用来抛弃不想继续使用的值,如给导入的包赋予一个空名字,或者忽略函数返回的你不感兴趣的值。

函数 init

每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。
所有被编译器发现的 init 函数都会安排在 main 函数之前执行。
init 函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。

以数据库驱动为例,database 下的驱动在启动时执行 init 函数会将自身注册到 sql 包里,因为 sql 包在编译时并不知道这些驱动的存在,等启动之后 sql 才能调用这些驱动。

1
2
3
4
5
6
7
8
9
package postgres

import (
    "database/sql"
)

func init() {
    sql.REgister("postgres", new(PostgresDriver))   // 创建一个 postgres 驱动的实例。这里为了展现 init 的作用,没有展现其定义细节
}

这段示例代码包含在 PostgreSQL 数据库的驱动里。
如果程序导入了这个包,就会调用 init 函数,促使 PostgreSQL 的驱动最终注册到 Go 的 sql 包里,成为一个可用的驱动。

在使用这个新的数据库驱动写程序时,我们使用空白标识符来导入包,以便新的驱动会包含到 sql 包。
如前所述,不能导入不使用的包,为此使用空白标识符重命名这个导入可以让 init 函数发现并调度运行,让编译器不会因为包未被使用而产生错误。

现在我们可以调用 sql.Open 方法来使用这个驱动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
    "database/sql"

    _"github.com/goination/code/chapter3/dbdriver/postgres"   // 使用空白标识符导入包,避免编译错误
)

func main() {
    sql.Open("postgres", "mydb")  // 调用 sql 包提供的 Open 方法。该方法能工作的关键在于 postgres 驱动自己的 init 函数将自身注册到了 sql 包
}

使用 Go 的工具

在命令行提示符下, 不带参数直接键入 go 这个命令:

1
$ go

go 这个工具提供了很多功能

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         add dependencies to current module and install them
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

    buildconstraint build constraints
    buildmode       build modes
    c               calling between Go and C
    cache           build and test caching
    environment     environment variables
    filetype        file types
    go.mod          the go.mod file
    gopath          GOPATH environment variable
    gopath-get      legacy GOPATH go get
    goproxy         module proxy protocol
    importpath      import path syntax
    modules         modules, module versions, and more
    module-get      module-aware go get
    module-auth     module authentication using go.sum
    module-private  module configuration for non-public modules
    packages        package lists and patterns
    testflag        testing flags
    testfunc        testing functions

Use "go help <topic>" for more information about that topic.

通过输出的列表可以看到,这个命令包含一个编译器,这个编译器可以通过 build 命令启动。
正如预料的那样,build 和 clean 命令会执行编译和清理的工作。

Go Modules 包管理工具的理解与使用

https://www.infoq.cn/article/xyjhjja87y7pvu1iwhz3

下载依赖包: go mod download

开发一个项目的流程示例

初始化项目,地址可以换成自己的地址:

1
go mod init github.com/YangzhenZhao/account-verify

安装 github 上的包,例如安装cobra:

1
go get -u github.com/spf13/cobra

也可以安装指定版本,使用 @版本号 例如:

1
go get -u github.com/swaggo/swag/cmd/swag@v1.6.5

go module 使用本地包

1
2
3
mkdir testmodule
cd testmodule
go mod init example.com/testmodule
1
2
3
├── go.mod
└── test
    └── test.go

test.go:

1
2
3
4
5
package test

func TestLocal() {
    println("test local...")
}

在另外一个项目的 go.mod 中使用 replace

1
2
require "example.com/testmodule" v0.0.0
replace "example.com/testmodule" => "../Desktop/testmodule"

然后新建 main.go:

1
2
3
4
5
6
7
package main

import "example.com/testmodule/test"

func main() {
    test.TestLocal()
}

输出:

1
test local...