今天一个适度大小的程序可能包含有1万个函数。然而,其作者只需要考虑其中的少数几个,并且设计得更少,因为绝大多数函数是由其他人编写的,并通过包的形式实现重用。
Go附带了超过100个标准包,为大多数应用程序提供了基础。Go社区是一个蓬勃发展的包设计、分享、重用和改进的生态系统,已经发布了许多其他包,您可以在http://godoc.org找到一个可搜索的索引。在本章中,我们将展示如何使用现有的包并创建新的包。
Go还配备了go工具,这是一个复杂但易于使用的命令,用于管理Go包的工作空间。自本书开始以来,我们一直展示如何使用go工具来下载、构建和运行示例程序。在本章中,我们将了解工具的基本理念,并进一步了解其功能,包括打印有关工作空间中包的文档和查询包的元数据。在下一章中,我们将探索其测试功能。
10.1 引言
任何包系统的目的都是通过将相关特性组合成可以轻松理解和更改的单元,使大型程序的设计和维护变得实用,而这些单元是独立于程序的其他包的。这种模块化允许包被不同的项目共享和重用,在组织内分发,或向更广泛的世界提供。
每个包定义了一个独特的命名空间,其中包含属于它的标识符。每个名称都与一个包相关联,这让我们可以选择对于类型、函数等使用最频繁的部分来选择简短、清晰的名称,而不会与程序的其他部分产生冲突。
包还通过控制哪些名称在包外可见或被导出来提供封装。限制包成员的可见性可以隐藏包的API后面的辅助函数和类型,使包维护者可以放心地更改实现,而不会影响包外的任何代码。限制可见性还会隐藏变量,这样客户端只能通过导出的函数来访问和更新它们,这些函数会保持内部不变性或在并发程序中强制执行互斥。
当我们改变一个文件时,我们必须重新编译文件的包,以及潜在地影响到依赖它的所有包。Go编译相对于大多数其他编译语言来说速度明显更快,即使是从零开始构建。编译器速度快有三个主要原因。首先,所有的导入必须明确地列在每个源文件的开头,因此编译器不必读取和处理整个文件来确定它的依赖关系。其次,包的依赖形成一个有向无环图,因为没有循环,因此可以单独编译包,也许还可以并行编译。最后,编译后的Go包的目标文件记录了导出信息,不仅仅是对于包本身,还包括了它的依赖关系。当编译一个包时,编译器必须读取每个导入的目标文件,但不需要查看这些文件之外的内容。
10.2 导入路径
每个包都由一个称为其导入路径的唯一字符串标识。导入路径是出现在导入声明中的字符串。
go
import (
"fmt"
"math/rand"
"encoding/json"
"golang.org/x/net/html"
"github.com/go-sql-driver/mysql"
)
正如我们在2.6.1节中提到的,Go语言规范没有定义这些字符串的含义或如何确定包的导入路径,而是将这些问题留给了工具处理。在本章中,我们将详细介绍go工具如何解释它们,因为这是大多数Go程序员用于构建、测试等操作的工具。当然,还存在其他工具。例如,使用谷歌内部多语言构建系统的Go程序员遵循不同的规则来命名和定位包,指定测试等,这些规则更符合该系统的约定。
对于你打算分享或发布的包,导入路径应该是全局唯一的。为了避免冲突,除了来自标准库的包之外,所有其他包的导入路径都应以拥有或托管该包的组织的互联网域名开头;这也使得能够找到这些包。例如,上面的声明导入了由Go团队维护的HTML解析器和一个流行的第三方MySQL数据库驱动程序。
10.3 包的声明
每个Go源文件的开头都需要一个包声明。其主要目的是当它被另一个包导入时确定该包的默认标识符(称为包名)。
例如,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。我们稍后将看到如何在同一个程序中同时使用它们。
有三个主要的例外情况,不符合"最后一部分"的惯例。第一个例外是定义命令(可执行的Go程序)的包始终具有名称main,而不管包的导入路径如何。这是给go build (10.7.3节)的一个信号,告诉它必须调用链接器来生成可执行文件。
第二个例外是,如果文件名以_test.go结尾,该目录中的一些文件的包名可能具有后缀_test。这样的目录可能定义了两个包:通常的包,和一个称为外部测试包的包。包名中的_test后缀向go test发出信号,它需要构建两个包,且包名中的_test后缀指示哪些文件属于哪个包。外部测试包用于避免测试中依赖关系导致的导入图的循环;我们会在11.2.4节中更详细地介绍这些内容。
第三个例外是,某些依赖管理工具会在包的导入路径后追加版本号后缀,例如"gopkg.in/yaml.v2"。包名不包括后缀,因此在这种情况下,它只是yaml。
10.4 导入声明
一个Go源文件可能在包声明之后,第一个non-import的声明之前包含零个或多个导入声明。每个导入声明可以指定单个包的导入路径,或者在括号中列出多个包。下面的两种形式是等价的,但第二种形式更常见。
go
import "fmt"
import "os"
import (
"fmt"
"os"
)
导入的包可以通过引入空行进行分组;这样的分组通常表示不同的domain。顺序并不重要,但按照惯例,每个分组的行通常按字母顺序排序(gofmt和goimports都可进行分组和排序)。
go
import (
"fmt"
"html/template"
"os"
"golang.org/x/net/html"
"golang.org/x/net/ipv4"
)
如果我们需要将两个名称相同的包,例如math/rand和crypto/rand,导入到第三个包中,那么导入声明必须为它们中至少一个指定一个替代名称,以避免冲突。这被称为重命名导入。
go
import (
"crypto/rand"
mrand "math/rand" // alternative name mrand avoids conflict
)
替代名称只影响当前文件。其他文件,即使是与当前文件同一个包中的文件,也可以使用默认名称(即包名)或不同名称导入包。
即使没有冲突,重命名导入也可能很有用。如果导入包的名称很冗长,如有时对于自动生成的代码,缩写的名称可能更方便。应一致地使用相同的简短名称以避免混淆。选择替代名称可以帮助避免与常见的本地变量名称发生冲突。例如,在具有许多命名为path的本地变量的文件中,我们可能将标准的 "path" 包导入为pathpkg。
每个导入声明都建立了从当前包到导入包的依赖关系。如果这些依赖关系形成循环,go build工具会报告错误。
10.5 空导入
将包导入文件中但在该文件中不引用其中定义的名称是一个错误。然而,有时我们必须仅仅为了导入包的副作用而导入它:对其包级别变量的初始化表达式求值和执行其init函数(2.6.2节)。为了避免"未使用导入"错误,我们必须使用一个重命名导入,其中替代名称是 _,即空白标识符。与通常情况一样,空白标识符不能被引用。
go
import _ "image/png" // register PNG decoder
这被称为空导入。它最常用于实现编译时机制,使主程序可以通过空白导入额外的包来启用可选功能。首先我们将看到如何使用它,然后我们将看到它是如何工作的。
标准库的image包导出了一个Decode函数,该函数从io.Reader中读取字节,确定使用哪种图像格式对数据进行编码,调用适当的解码器,然后返回生成的image.Image。使用image.Decode,可以很容易地构建一个简单的图像转换器,它可以读取一个格式的图像并将其写入另一个格式:
go
// The jpeg command reads a PNG image from the stardard 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})
}
如果我们将gopl.io/ch3/mandelbrot(3.3节)的输出输入到转换器程序中,它会检测到PNG输入格式,并将图3.3的JPEG版本写入。
请注意image/png的空白导入。没有这一行,程序会像往常一样编译和链接,但无法识别或解码PNG格式的输入。
这是它的工作原理:标准库提供了GIF、PNG和JPEG的解码器,用户也可以提供其他格式的解码器,但为了保持可执行文件的大小,除非显式请求,否则解码器不会包含在应用程序中。image.Decode函数会查询一个支持的格式表。表中的每个条目都指定了四个内容:格式的名称;一个字符串,该字符串是所有以这种方式编码的图像的前缀,用于检测编码方式;一个解码编码图像的函数Decode;另一个函数DecodeConfig,仅解码图像的元数据,例如其大小和颜色空间。通过调用image.RegisterFormat将条目添加到表中,通常是在该格式的supporting package的包初始化器中为每种格式调用,就像在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)
}
结果是,一个应用程序只需要空白导入所需格式的包,使得image.Decode函数能够解码它。
database/sql包使用类似的机制,让用户安装他们需要的数据库驱动程序。例如:
go
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // enable support for MySQL
_ "github.com/lib/q" // enable support for Postgres
)
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"
10.6 包及其命名
在本节中,我们将提供一些关于包和其中成员的Go的独特命名规范的建议。
创建包时,保持其名称简短,但不要太短以至于晦涩难懂。标准库中最常用的包的名称包括bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time。
尽可能地有描述性和明确。例如,当一个名称如ioutil具体而又简洁时,就不要给一个工具包取名为util。避免选择常用的本地变量的包名称,否则你可能会迫使包的客户端使用重命名导入,就像path包一样。
包名称通常采用单数形式。标准包bytes、errors和strings使用复数形式,以避免覆盖相应的预声明类型,并且可使用go/types避免与关键字冲突。
避免具有其他含义的包名称。例如,我们最初在第2.5节中将温度转换包命名为temp,但那并没有持续太久。那是一个糟糕的想法,因为"temp"几乎是"temporary"的通用同义词。我们曾经使用名称temperature进行了一段时间,但那太长了,而且并没有说明包的功能。最后,它变成了tempconv,这更短,并且与strconv平行。
现在让我们转向包成员的命名。由于对另一个包的成员的每个引用都使用类似fmt.Println这样的限定标识符,因此描述包成员的负担由包名和成员名共同承担。对于成员Println,我们无需提及格式化的概念,因为包名fmt已经做了。设计包时,需考虑限定标识符的两部分是如何协同工作的,而不仅仅是成员名字。以下是一些特征性的例子:
现在我们可以识别一些常见的命名模式。strings包提供了一系列独立的函数来操作字符串。
go
package strings
func Index(needle, haystack string) int
type Replacer struct { /* ... */ }
// NewReplacer接受零个或多个string参数
func NewReplacer(oldnew ...string) *Replacer
type Reader struct { /* ... */ }
func NewReader(s string) *Reader
这些函数的名称中没有出现单词"string"。客户端将它们称为strings.Index、strings.Replacer等等。
还有我们描述为单类型包的包,例如html/template和math/rand,会暴露一个主要的数据类型以及它的方法,通常还会提供一个New函数来创建实例。
go
package rand // "math/rand"
type Rand struct { /* ... */ }
func New(source Source) *Rand
像template.Template或rand.Rand一样会导致包名和成员名重复,这就是为什么这类包的名称通常特别简短。
在另一个极端,有些包像net/http这样,拥有大量的名称但结构不太明确,因为它们执行的任务比较复杂。尽管拥有超过二十种类型和更多的函数,但该包中最重要的成员的名称却是最简单的:Get、Post、Handle、Error、Client、Server。
10.7 go工具
本章的其余部分涉及到go工具,它用于下载、查询、格式化、构建、测试和installing packages of Go code。
go工具将各种工具的功能合并到一个命令集中。它是一个包管理器(类似于apt或rpm),用于回答关于其包存货的查询,计算它们的依赖关系,并从远程版本控制系统下载它们。它是一个构建系统,用于计算文件的依赖关系,并调用编译器、汇编器和链接器,尽管它没有标准Unix的make完备。同时,它也是一个测试驱动程序,我们将在第 11 章中看到。
它的命令行界面采用了"瑞士军刀"风格,有超过十几个子命令,其中一些我们已经见过,比如 get、run、build和fmt。你可以运行go help来查看其内置文档的索引,但是作为参考,下面列出了最常用的命令:
为了尽量减少配置的需求,go工具严重依赖于惯例。例如,给定一个Go源文件的名称,工具可以找到它所在的包,因为每个目录都只包含一个包,并且包的导入路径对应于工作区中的目录层次结构。给定一个包的导入路径,工具可以找到相应的目录,其中存储了目标文件。它还可以找到托管源代码仓库的服务器的URL。
10.7.1 工作空间的组织
大多数用户唯一需要的配置就是GOPATH环境变量,它指定了工作区的根目录。当切换到不同的工作区时,用户更新GOPATH的值。例如,在编写本书时,我们将GOPATH设置为 H O M E / g o b o o k 。 ! [ 在这里插入图片描述 ] ( h t t p s : / / i m g − b l o g . c s d n i m g . c n / d i r e c t / c 1 f b 8 a f 291 b 04041 b 1 e e 65619 c e e 1 d 49. p n g ) 在使用上述命令下载本书的所有程序后,您的工作区将包含类似于以下结构的层次结构。 ! [ 在这里插入图片描述 ] ( h t t p s : / / i m g − b l o g . c s d n i m g . c n / d i r e c t / e 566 d 1 d c a f f 44 d 81 b a d e a 04048518 c 1 e . p n g ) G O P A T H 有三个子目录。 s r c 子目录存放源代码。每个包都位于 s r c 中的一个目录中,其相对于 ' HOME/gobook。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/c1fb8af291b04041b1ee65619cee1d49.png) 在使用上述命令下载本书的所有程序后,您的工作区将包含类似于以下结构的层次结构。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e566d1dcaff44d81badea04048518c1e.png) GOPATH有三个子目录。src子目录存放源代码。每个包都位于src中的一个目录中,其相对于` HOME/gobook。![在这里插入图片描述](https://img−blog.csdnimg.cn/direct/c1fb8af291b04041b1ee65619cee1d49.png)在使用上述命令下载本书的所有程序后,您的工作区将包含类似于以下结构的层次结构。![在这里插入图片描述](https://img−blog.csdnimg.cn/direct/e566d1dcaff44d81badea04048518c1e.png)GOPATH有三个子目录。src子目录存放源代码。每个包都位于src中的一个目录中,其相对于'GOPATH/src`的名称就是包的导入路径,例如gopl.io/ch1/helloworld。注意,单个GOPATH工作区下包含多个版本控制仓库,如gopl.io或golang.org。pkg子目录是构建工具存储编译后包的地方,而bin子目录则存放可执行程序,例如helloworld。
第二个环境变量GOROOT指定了Go发行版的根目录,该目录提供了标准库的所有包。在 GOROOT下的目录结构类似于GOPATH,因此,例如fmt包的源文件位于$GOROOT/src/fmt目录下。用户无需设置GOROOT,因为默认情况下,go工具将使用它安装的位置。
go env命令打印与工具链相关的环境变量的有效值,包括缺失变量的默认值。GOOS 指定目标操作系统(例如android、linux、darwin或windows),而GOARCH指定目标处理器架构,例如amd64、386或arm。虽然GOPATH是必须设置的唯一变量,但其他变量偶尔也会在我们的讲解中出现。
10.7.2 包的下载
在使用go工具时,包的导入路径不仅指示了在本地工作区中找到它的位置,还指示了在互联网上找到它的位置,以便go get可以检索和更新它。
go get命令可以使用...符号下载单个包、整个子树或仓库,就像前面的章节中那样。该工具还会计算并下载初始包的所有依赖项,这就是为什么在前面的示例中golang.org/x/net/html包出现在工作区中的原因。
一旦go get下载了包,它就会构建它们,然后安装库和命令。我们将在下一节中查看详细信息,但一个示例将展示这个过程是多么简单直接。下面的第一个命令获取了golint工具,该工具用于检查 Go 源代码中的常见风格问题。第二个命令在第2.6.2节中的gopl.io/ch2/popcount上运行golint。它友好地报告我们忘记为包写文档注释了:
go get命令支持流行的代码托管网站,如GitHub、Bitbucket和Launchpad,并且可以向它们的版本控制系统发送适当的请求。对于不太知名的网站,你可能需要在导入路径中指定要使用的版本控制协议,如Git或Mercurial。运行go help importpath获取详细信息。
go get创建的目录是远程仓库的真实客户端,而不仅仅是文件的副本,因此您可以使用版本控制命令查看您所做的本地修改的差异,或者更新到不同的修订版本。例如,golang.org/x/net目录是一个Git客户端:
请注意,包的导入路径中的父域名golang.org与Git服务器的实际域名go.googlesource.com不同。这是go工具的一个特性,允许包在其导入路径中使用自定义域名,同时由像googlesource.com或github.com这样的通用服务托管。https://golang.org/x/net/html下的HTML页面包含下面显示的元数据,它将go工具重定向到实际托管站点的Git仓库。
如果您指定了-u标志,go get将确保访问的所有包,包括依赖项,在构建和安装之前都更新到它们的最新版本。如果没有该标志,本地已存在的包将不会被更新。
go get -u命令通常检索每个包的最新版本,这在开始使用时很方便,但在部署的项目中可能不合适,因为精确控制依赖关系对发布卫生至关重要。解决这个问题的常见方法是供应(vender)代码(做法是将你的代码包裹一个叫vender的目录),即在本地创建所有必需依赖项的持久副本,并仔细和谨慎地更新此副本。在Go 1.5之前,这需要更改这些包的导入路径,因此我们的golang.org/x/net/html的副本将变成gopl.io/vendor/golang.org/x/net/html。尽管我们没有在这里展示细节,但最近版本的go工具直接支持vender代码目录。请参阅go help gopath命令输出中的Vendor Directories。
虽然Vendor目录在过去是Go依赖管理的主要方式,但是在Go 1.11版本之后,Go引入了Go Modules的功能,它提供了更加强大和灵活的依赖管理机制。因此,Go Modules已经成为了更加推荐的依赖管理方式。
10.7.3 包的构建
go build命令编译每个指定的包。如果包是一个库(即不是main包),结果会被丢弃,这仅仅是检查包是否没有编译错误。如果包被命名为main,go build将调用链接器在当前目录中创建一个可执行文件;可执行文件的名称取自包的导入路径的最后一个部分。由于每个目录包含一个包,每个可执行程序,或者在Unix术语中称为命令,都需要自己的目录。这些目录有时是一个名为cmd的子目录,比如golang.org/x/tools/cmd/godoc命令,它通过web接口提供Go包的文档服务。
包可以通过它们的导入路径指定,就像我们上面看到的那样,也可以通过相对目录名来指定,该相对目录名必须以.或...开头,即使有时这不是必需的。如果没有提供参数,则假定当前目录。因此,以下命令构建了相同的包,尽管每个命令将可执行文件写入运行go build的目录中:
但不能是这样:
包也可以指定为文件名列表,尽管这通常只用于小型程序和一次性实验。如果包名为main,则可执行文件的名称来自第一个.go文件的basename(即去掉后缀后的文件名)。
上图中,%q表示会对要输出的字符串用引号括起来,并对其中的字符进行转义,如将显示为空白的水平制表符显示为\t。
特别是对于像这样的临时程序,我们希望在构建后立即运行可执行文件。go run命令将这两个步骤结合在一起:
第一个不以.go结尾的参数被假定为传递给Go可执行文件的参数列表的开头。
默认情况下,go build命令会构建请求的包及其所有依赖项,然后丢弃所有已编译的代码,除非有最终的可执行文件。依赖分析和编译都非常快速,但是随着项目增长到数十个包和数十万行代码,重新编译依赖项所需的时间可能会变得明显,甚至可能几秒钟,即使这些依赖项根本没有发生变化。
go install命令与go build非常相似,只是它保存每个包和命令的编译代码,而不是丢弃它。编译后的包保存在$GOPATH/pkg
目录下,其在pkg目录中的路径与源代码在src中的路径相同,而命令可执行文件保存在$GOPATH/bin
目录下(许多用户将$GOPATH/bin加入到可执行文件搜索路径中)。此后,如果包和命令未发生变化,go build和go install将不会为它们运行编译器,从而使后续构建速度更快。为方便起见,go build -i安装了构建目标的依赖包。
由于编译后的包因平台和架构而异,go install将它们保存在一个子目录下,该子目录的名称包含了GOOS和GOARCH环境变量的值。例如,在Mac上,golang.org/x/net/html包被编译并安装为名为golang.org/x/net/html.a的文件,它被放在$GOPATH/pkg/darwin_amd64目录下。
跨编译Go程序非常简单,即构建一个针对不同操作系统或CPU的可执行文件。只需在构建过程中设置GOOS或GOARCH变量。以下cross程序将打印它是为那个操作系统和架构而构建的:
go
func main() {
fmt.Println(runtime.GOOS, runtime.GOARCH)
}
某些包可能需要为特定的平台或处理器编译不同版本的代码,以处理低级别的可移植性问题或提供重要例程的优化版本。如果文件名包含操作系统或处理器架构名称,例如net_linux.go或asm_amd64.s,则当构建针对该目标时,go工具将仅编译该文件。称为构建标签的特殊注释可以提供更精细的控制。例如,如果一个文件包含了这样的注释:
在包声明之前(及其文档注释之前),如果有这样的注释,go build仅在构建Linux或Mac OS X时编译它,而这个注释表示永远不要编译这个文件:
有关更多详细信息,请参阅go/build包文档中的Build Constraints部分。
10.7.4 包的文档化
Go风格强烈鼓励对包API进行良好的文档编写。每个导出的包成员的声明以及包声明本身应该立即前置一个注释,解释其目的和用法。
Go文档注释始终是完整的句子,第一句通常是一个以被声明的名称开头的摘要。函数参数和其他标识符不带引号或标记。例如,这是fmt.Fprintf的文档注释:
Fprintf的格式详细信息在与fmt包本身关联的文档注释中有解释。直接位于包声明之前的注释被视为整个包的文档注释。虽然可能会出现在任何文件中,但只能有一个。较长的包注释可能需要一个单独的文件;fmt的文档注释超过300行。这个文件通常称为doc.go。
良好的文档不一定要很详细,文档最好是简洁的。实际上,Go的惯例倾向于在所有事情中都保持简洁和简单,因为文档与代码一样需要维护。许多声明可以用一句话来解释,如果行为真的很明显,则不需要注释。
在整本书中,只要空间允许,我们都会在声明之前加上文档注释,但是标准库中有更好的例子。有两个工具可以帮助查看这些注释。
go doc工具打印命令行上指定实体的声明和文档注释,该实体可以是一个包:
或者是一个包成员:
或者是一个方法:
该工具不需要完整的导入路径或正确的标识符大小写。此命令打印encoding/json包中 (*json.Decoder).Decode 的文档:
第二个工具,令人困惑地被命名为godoc,提供交叉链接的HTML页面,提供与go doc相同的信息以及更多内容。位于https://golang.org/pkg的godoc服务器涵盖了标准库。图 10.1显示了 time 包的文档,而在第11.6节中我们将看到godoc对示例程序的交互式显示。位于https://godoc.org的godoc服务器具有可搜索的数千个开源包的索引。
如果你想浏览自己的包,也可以在你的workspace中运行一个godoc实例。在运行此命令时,通过浏览器访问http://localhost:8000/pkg:
它的-analysis=type和-analysis=pointer标志通过高级静态分析的结果增强了文档和源代码。
10.7.5 内部包
包是Go程序中封装的最重要机制。未导出的标识符仅在同一包内可见,而导出的标识符对外可见。
然而,有时中间地带可能会很有帮助,即定义仅对一小组受信任的包可见的标识符,而不是对所有人都可见。例如,当我们将一个大包拆分为更易管理的部分时,我们可能不希望将这些部分之间的接口透露给其他包。或者我们可能希望在整个项目的几个包之间共享实用函数,而不会将它们更广泛地暴露出去。又或者,我们可能只是想要尝试一个新包,而不想过早地承诺其API,通过将其"试用"于一组有限的客户端来进行实验。
为了满足这些需求,go build工具特别对待一个包,如果其导入路径包含一个名为internal的路径段,则会对其进行处理。这样的包称为内部包。内部包只能被位于internal目录所在目录内的另一个包导入。例如,给定下面的包,net/http/internal/chunked可以从net/http/httputil或net/http导入,但不能从net/url导入。但是net/url可以导入net/http/httputil。
10.7.6 包的查询
go list工具提供有关可用包的信息。在其最简单的形式中,go list会检查包是否存在于工作空间中,并在这种情况下打印其导入路径:
go list的参数可以包含...
通配符,它匹配包导入路径的任何子字符串。我们可以使用它来枚举Go工作空间中的所有包:
或枚举一个指定子树内的所有包:
或者枚举某个主题相关的包:
go list命令获取每个包的完整元数据,而不仅仅是导入路径,并以多种格式将此信息提供给用户或其他工具。-json标志导致go list以JSON格式打印每个包的完整记录:
-f 标志允许用户使用包text/template(4.6节)的模板语言自定义输出格式。该命令打印strconv包的传递依赖项,以空格分隔:
而以下命令打印标准库的compress子树中每个包的直接导入项:
go list命令对于单次交互式查询,构建以及测试自动化脚本都很有用。我们将在第11.2.4节再次使用它。有关更多信息,包括可用字段及其含义,请参阅go help list命令的输出。
在本章中,我们已经解释了go工具的所有重要子命令,除了一个。在下一章中,我们将看到go test命令如何用于测试Go程序。