在golang标准库中,有那么一类包,它们用于处理go项目目录结构、源码、语法、基本操作等。一般程序中可能用不到这些包,但在go工具链源码中用到了,之所以学习这些标准库,是为了更好的看go工具链的源码。首先我们来看收集go包信息的库:go/build
一、build包概述
该包文档中首先介绍了Go Path。如果对该部分还不清楚,可以看下文档的说明;或者官方其他文档;或者看 Go项目的目录结构。
如果你看过go源码,应该见到过类似这样的包注释:+build ignore。这是编译约束条件(Build Constraints),可以理解为条件编译。关于这部分的更多内容,稍后详细介绍。
二、类型和函数
1、ToolDir变量
var ToolDir = filepath.Join(runtime.GOROOT(), "pkg/tool/"+runtime.GOOS+"_"+runtime.GOARCH)
该变量的值是go工具链的路径。6g/6l之类的工具,就在这个路径下
2、ArchChar函数
获得架构的字符表示。在之前的文章中介绍过。比如:x86 32bit用8表示;amd64用6表示等。该函数通过传入goarch,获得对应的架构字符。如:build.ArchChar(runtime.GOARCH)
3、IsLocalImport函数
判断是否为“本地导入“,类似”.”, “..”, “./foo”或者”../foo”。正式项目,一般不建议用本地导入,使用本地导入很多人会说找不到包。
4、Context类型
该类型为构建(build)指定上下文环境。比如:当前操作系统、架构、Go根目录、GOPATH等
该类型除了提供变量成员,还提供了函数成员,如果函数成员是nil,则会使用其他库提供的函数。
5、Package类型
描述Go包
三、主要类型的方法(包括实例化)
1、Context的实例化和方法
var Default Context = defaultContext()
这是默认实现,go build工具使用的就是这个默认实现。Default会使用GOARCH、GOOS、GOROOT和GOPATH环境变量,如果没设置,则使用安装runtime中的值。
1)Import方法
func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Package, error)
导入一个包,返回Package类型指针。path参数跟在代码中import path的path一样。srcDir是源码所在路径;而ImportMode类型,build包中提供了两种:FindOnly和AllowBinary。AllowBinary可以在包源码不存在的时候,编译好的包对象文件直接被引用。
之前,在论坛(关于go的代码组织)中讨论过这样一个问题:go build 的时候,如果依赖的包源码不存在,编译不成功,有一个解决办法是通过go tool 6g这种方式编译。现在,在你知道了AllowBinary参数之后,应该可以通过修改go工具源码来解决这个问题。在src/cmd/go目录中的pkg.go中225有这样的代码:
1
2
3
|
/ / TODO :
After Go 1 , decide when to
pass build.AllowBinary here. / / See issue
3268 for mistakes to
avoid. bp , err
: = buildContext.Import ( path ,
srcDir , 0 ) |
通过查看Import的源码,可以知道包安装的细节,比如安装到哪里。
当目录不包含源码,如果出错,则返回NoGoError错误。
另外,build包提供了Import方法的一个简便方法,即:Import函数,默认调用Default的Import方法
2)ImportDir方法
内部实现:return ctxt.Import(“.”, dir, mode)
3)SrcDirs方法
列出GOROOT和GOPATH中的源码目录。比如,我没有设置GOPATH,执行结果如下:
fmt.Println(build.Default.SrcDirs()) // [ c:\Go\src\pkg]
2、Package的实例化和方法
Context的Import和ImportDir都会返回Package实例(*Package),当然,也可以直接实例化。
Package提供了一个方法,判断一个Package是否是命令,也就是是否是main包
func (p *Package) IsCommand() bool
3、关于Context和Package的字段
由于这两种类型字段很多,包文档中每个字段都有注释,在此不一一解释。
四、构建约束(build constraints)
或者叫条件编译(编译条件)
1、使用说明
在go源码中(src/pkg或src/cmd)搜索+build,发现有不少文件的开头有这样的注释
+build xxx
构建约束是一行以+build开始的注释。在+build之后列出了一些条件,在这个条件成立的时,该文件应该包含在包中(也就是应该被编译进包文件),约束可以出现在任何源文件中,也就是不限于go源文件。不过,这些条件必须在文件最顶部(正是代码的前面,也就是说,+build之前可以有其他注释),在+build注释之后,应该有一个空行(这是为了和package doc区分开)。
语法规则:
1)只允许是字母数字或_
2)多个条件之间,空格表示OR;逗号表示AND;叹号(!)表示NOT
3)一个文件可以有多个+build,它们之间的关系是AND。如:
// +build linux darwin
// +build 386
等价于
// +build (linux OR darwin) AND 386
4)预定义了一些条件:
runtime.GOOS、runtime.GOARCH、compiler(gc或gccgo)、cgo、context.BuildTags中的其他单词
5)如果一个文件名(不含后缀),以 *_GOOS, *_GOARCH, 或 *_GOOS_GOARCH结尾,它们隐式包含了 构建约束
6)当不想编译某个文件时,可以加上// +build ignore。这里的ignore可以是其他单词,只是ignore更能让人知道什么意思
更多详细信息,可以查看go/build/build.go文件中shouldBuild和match方法。
2、应用实例
除了*_GOOS这种预定义的应用,我们看一个实际的应用。
比如,项目中需要在测试环境输出Debug信息,一般通过一个变量(或常量)来控制是测试环境还是生产环境,比如:if DEBUG {},这样在生产环境每次也会进行这样的判断。在golang-nuts邮件列表中有人问过这样的问题,貌似没有讨论出更好的方法(想要跟C中条件编译一样的效果)。下面我们采用Build constraints来实现。
1)文件列表:main.go logger_debug.go logger_product.go
2)在main.go中简单的调用Debug()方法。
3)在logger_product.go中的Debug()是空实现,但是在文件开始加上// + build !debug
4)在logger_debug.go中的Debug()是需要输出的调试信息,同时在文件开始加上// + build debug
这样,在测试环境编译的时传递-tags参数:go build/install -tags “debug” logger。生产环境:go build/install logger就行了。
对于生产环境,不传递-tags时,为什么会编译logger_product.go呢?因为在go/build/build.go中的match方法中有这么一句:
1 if strings.HasPrefix(name, "!") { // negation
2 return len(name) > 1 && !ctxt.match(name[1:])
3 }
也就是说,只要有!(不能只是!),tag不在BuildTags中时,总是会编译。
完整源码在github上
有疑问加站长微信联系(非本文作者)