logo

罕见的 Go CLI 命令 (1/5):如何控制构建环境

Go 提供了丰富的 CLI 命令,使我们可以在开发、编译、执行等过程对程序进行更细粒度的控制。本系列文章主要从如何控制构建环境 (build environment)如何构建共享库 (shared libraries)如何修改运行环境 (runtime envrionment)如何管理项目 (manage projects)如何检查项目 (inspect projects) 五个部分对 Go CLI 命令进行分类总结。虽然我们日常开发中未必会需要这些知识,但了解它们的存在在某些特殊情况下可能会有帮助。

本篇主要从以下几个方面概览如何控制 Go 的构建环境:

一般情况下我们可能经常使用类似 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。添加这一行之后,根据运行方式的不同会得到不同的结果:

这里有几点提示:

4. -pgo

PGO 是一种编译器优化技术,通过使用程序的运行时性能数据来指导编译器生成更加优化的代码,以提高程序的性能和效率。

使用 -pgo 标志进行编译时,需要进行两次构建:第一次构建会生成程序的性能数据文件。然后,使用这些性能数据来指导下一次编译,以生成更加优化的代码。

这个 04-pgo 的实例代码相比 -gcflags 有两处改动:

-pgo 标志使用 auto 值时会使用查找当前目录内的 default.pgo 文件,所以可以通过以下步骤验证 -pgo 标志:

一般情况下,PGO 可以提高约 4% - 7% 的性能。

我的第一次测试,即上面的数据,性能提升约 1.5%;第二次测试,优化前执行时间 Elapsed: 144.716118ms,优化后 Elapsed: 122.102817ms,提升约 16%。

总结

Go 提供了几十种不同的方式来进一步控制构建环境,本文的目的不是介绍 -gcflags-ldflags 等某一种标志的使用,而是通过这几个例子来说明方法。如果把本文浓缩成一句话,那就是 Go 提供了细粒度的控制构建环境的方式