罕见的 Go CLI 命令 (1/5):如何控制构建环境
Go 提供了丰富的 CLI 命令,使我们可以在开发、编译、执行等过程对程序进行更细粒度的控制。本系列文章主要从如何控制构建环境 (build environment)、如何构建共享库 (shared libraries)、如何修改运行环境 (runtime envrionment)、如何管理项目 (manage projects)、如何检查项目 (inspect projects) 五个部分对 Go CLI 命令进行分类总结。虽然我们日常开发中未必会需要这些知识,但了解它们的存在在某些特殊情况下可能会有帮助。
本篇主要从以下几个方面概览如何控制 Go 的构建环境:
- 改变代码的编译方式
- 修改链接器的行为
- 使用构建约束改变被编译的源代码
- 使用 Profile Guided Optimization (PGO) 优化构建
一般情况下我们可能经常使用类似 GOOS=linux go build .
的命令构建二进制可执行文件。但通过 go help build
命令,我们会发现 Go 对于构建环境提供了非常丰富的控制方式。这里列出几个典型标志的用法,以供在此基础上进一步探索。
1. -gcflags
go help build
中对 -gcflags
的解释是 arguments to pass on each go tool compile invocation。顾名思义,-gcflags
标志用来控制 Go 编译器的行为,例如优化级别、垃圾回收方式等。
如这里的 关于 gcflags 的 demo 程序,这个程序读取一个 CSV 文件,按天获取当天的最大、最小以及平均降雪量,以日期升序排列。
在 01-gcflags
目录内执行 go run -gcflags="-m" .
(或者 go build -gcflags="-m" . && ./demo
),可以得到如下输出:
# demo
./main.go:99:2: can inline createRecords.deferwrap1
./main.go:99:15: inlining call to sync.(*WaitGroup).Done
./main.go:84:3: can inline processCSV.gowrap1
./main.go:86:5: can inline processCSV.func1
./main.go:86:2: can inline processCSV.gowrap2
./main.go:159:9: inlining call to sync.(*WaitGroup).Done
./main.go:125:5: can inline processRecords.func1.gowrap1
...
这里的输出展示的正是 Go 编译器在编译这个程序时所做出的优化决策,-m
的意思是 print optimization decisions,即打印编译器优化决策。-m
以及更多 -gcflags
可用的参数都来自 go tool compile
。
2. -ldflags
-ldflags
的解释是 arguments to pass on each go tool link invocation, 是 Go 编译器的一个标志,用于传递链接器的参数。通过 -ldflags
标志,可以向链接器传递额外的信息,例如设置程序的版本信息、构建时间等。这个标志可以帮助开发人员在构建可执行文件时注入自定义的信息,以便在程序中访问这些信息。
关于 ldflags 的 demo 程序其实和上面的 -gcflags
程序一模一样。
在 02-ldflags
目录内执行 go run -ldflags="-v" .
(或者 go build -ldflags="-v" . && ./demo
),可以得到如下输出:
# demo
build mode: pie, symbol table: off, DWARF: off
HEADER = -H1 -T0x1001000 -R0x1000
101339 symbols, 28409 reachable
45934 package symbols, 38593 hashed symbols, 13795 non-package symbols, 3017 external symbols
108577 liveness data
其中 -v
表示 print link trace,来自 go tool link
。
3. 构建约束
在写程序时,有时候我们希望某些代码在某些约束条件下运行,如希望代码 “只在开发环境运行”,此时我们就可以使用 Go 的构建约束。假设我们有以下两个文件:
// main.go
package main
import "fmt"
var usernames []string
func main() {
fmt.Printf("%#v\n", usernames)
}
// main_dev.go
package main
func init() {
usernames = []string{"a", "b", "c", "d"}
}
代码位置:03-build-constraints
在代码目录内运行 go run .
会输出 []string{"a", "b", "c", "d"}
。
假设出于调试的目的,我们只希望在开发环境的构建中包含 main_dev.go
文件,那么我们可以在 main_dev.go
文件的顶部添加 //go:build dev
,虽然这看起来像是注释,但也的确是注释,不过有一个更专业的称呼叫 magic comment。添加这一行之后,根据运行方式的不同会得到不同的结果:
go run .
得到[]string(nil)
go run -tags dev .
得到[]string{"a", "b", "c", "d"}
这里有几点提示:
go:build
和//
之间不能有空格,即//go:build
是固定写法dev
是我们自定义的 tag,tag 可以有多个,以,
分割,在go help build
的-tags
部分有说明- 构建约束更丰富的用法可以参考
go help buildconstraint
,如可以使用linux
,ignore
等 tag - 也可以参考官方文档 Build constraints
4. -pgo
PGO 是一种编译器优化技术,通过使用程序的运行时性能数据来指导编译器生成更加优化的代码,以提高程序的性能和效率。
使用 -pgo
标志进行编译时,需要进行两次构建:第一次构建会生成程序的性能数据文件。然后,使用这些性能数据来指导下一次编译,以生成更加优化的代码。
这个 04-pgo 的实例代码相比 -gcflags
有两处改动:
- 增加了
-cpuprofile
命令行参数 - 输出程序的执行时间
-pgo
标志使用 auto
值时会使用查找当前目录内的 default.pgo
文件,所以可以通过以下步骤验证 -pgo
标志:
- 运行
go run .
,输出Elapsed: 125.158434ms
,即不使用优化技术时的程序运行时间 - 运行
go run . -cpuprofile default.pgo
在当前目录生成default.pgo
性能数据文件 - 运行
go run -pgo auto .
使用 PGO 进行编译器优化,输出Elapsed: 123.273793ms
一般情况下,PGO 可以提高约 4% - 7% 的性能。
我的第一次测试,即上面的数据,性能提升约 1.5%;第二次测试,优化前执行时间
Elapsed: 144.716118ms
,优化后Elapsed: 122.102817ms
,提升约 16%。
总结
Go 提供了几十种不同的方式来进一步控制构建环境,本文的目的不是介绍 -gcflags
,-ldflags
等某一种标志的使用,而是通过这几个例子来说明方法。如果把本文浓缩成一句话,那就是 Go 提供了细粒度的控制构建环境的方式。
- 命令
go help build
go tool compile
go tool link
go help buildconstraint
- 代码