好好学习,天天向上

译|Go 的工具箱综述

原文:An Overview of Go's Tooling


偶尔会有人问我,“为什么你会喜欢用 Go 呢?” 在我的回答中,有一个我经常提到的原因,那就是和 Go 一起存在的那些好用的工具,并且它们是作为 go 命令的一部分存在的。其中有一些工具我每天都在用,例如 go fmtgo build。而其他像 go tool pprof 这样的工具,则只在解决特定问题的时候会用到。但是无论如何,我很感激它们,让我的项目管理和维护变得更加容易。

在这篇文章中,我希望说明一些我认为最有用的工具的背景和使用场景。更重要的是,解释清楚可以如何将其用于典型项目的工作流程中。如果你刚接触 Go,那么,我希望这篇文章可以给你一个良好的开始。

或者,如果你已经使用 Go 一段时间了,那么你可能已经对这些东西很熟悉了,但是,我希望你仍旧可以从本文中发现一个从前不知道的命令或者标志 : )

本文基于 Go 1.12,并且假设你的项目已经 启用了 modules

安装工具

在这篇文章中,我将主要关注作为 go 命令一部分的工具。但是,我还会提到那些不属于标准的 Go 1.12 版本的工具。

要在使用 Go 1.12 的情况下安装这些工具,首先,你需要确保当前位置_不在_启用 module 的目录下(通常,我会切到 /tmp)。然后,你就可以使用 GO111MODULE=on go get 命令来安装工具了。例如:

1
2
3
$ cd /tmp
$ GO111MODULE=on go get golang.org/x/tools/cmd/stress

上面的操作将会下载相关的包和依赖项,构建可执行文件,然后将其添加到你的 GOBIN 目录下。如果没有显式设置 GOBIN 目录,那么,可执行文件将会被添加到 GOPATH/bin 目录下。无论何种方式,都应该保证系统路径中有相应的目录。

注意:这个过程有点笨重,希望在未来的 Go 版本中有所改进。Issue 30515 正在跟踪关于这件事的讨论。

查看环境信息

可以使用 go env 工具来展示当前 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
$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/alex/.cache/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/alex/go"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build245740092=/tmp/go-build -gno-record-gcc-switches"

如果你对特定值感兴趣,那么可以将其作为参数传给 go env。例如:

1
2
3
4
5
$ go env GOPATH GOOS GOARCH
/home/alex/go
linux
amd64

想要显示所有 go env 变量和值的文档,可以运行:

1
2
$ go help environment

开发

运行代码

开发过程中,go run 工具是一种试用代码的便捷方式。它其实是一个快捷方式,用于一步编译代码,在 /tmp 目录下创建可执行的二进制文件,然后运行该二进制文件。

1
2
3
$ go run .          # Run the package in the current directory
$ go run ./cmd/foo # Run the package in the ./cmd/foo directory

注意:从 Go 1.11 起,你可以像上面一样,将 一个包的路径 传给 go run。这意味着,不再需要使用像 go run *.go 通配符扩展这样的变通方式,就能运行多个文件了。我超爱这项改进!

获取依赖项

假设启用了 modules,那么,当使用 go run(或者与此有关的 go test 或者 go build)时,任何外部依赖将会自动(递归)地下载,以满足代码中的 import 语句。默认情况下,会下载依赖项的最新标志版本。如果没有可用的标志版本,则会下载最新提交的。

如果事先知道需要特定版本的依赖项(而不是 Go 默认会获取的那个版本),那么可以在使用 go get 的时候带上相关的版本号或者提交哈希值。例如:

1
2
3
$ go get github.com/foo/bar@v1.2.3
$ go get github.com/foo/bar@8e1b8d3

如果提取的依赖项带有 go.mod 文件,那么,它的依赖项_将不会被列入_你的 go.mod 文件中。相反,如果你在下载的依赖项没有 go.mod 文件,那么,它的依赖项_将_被列入_你的_ go.mod 文件中,并且在其之下会有一个 // indirect 注释。

这意味着,你的 go.mod 文件不一定会在一个地方显示项目的所有依赖项。相反,可以像这样使用 go list 工具来查看所有依赖项:

1
2
$ go list -m all

有时,你可能会想知道_为什么它会是一个依赖项?_此时,可以使用 go mod why 命令来解惑。这个命令将显示从主模块中的包到给定依赖项的最短路径。例如:

1
2
3
4
5
6
$ go mod why -m golang.org/x/sys
# golang.org/x/sys
github.com/alexedwards/argon2id
golang.org/x/crypto/argon2
golang.org/x/sys/cpu

注意:go mod why 命令将返回大多数(并非所有)依赖项之所以为依赖的原因。Issue 27900 正在跟踪此问题。

如果你对分析或者可视化应用的依赖项感兴趣,那么,你可能会想看看 go mod graph 工具。这里 有一个很棒的生成可视化的教程和示例代码。

最后,下载下来的依赖项会保存在位于 GOPATH/pkg/mod 的_模块缓存_中。如果需要清除模块缓存,那么可以使用 go clean 工具。但是请注意:这个工具将会移除机器上_所有项目_的已下载依赖项。

1
2
$ go clean -modcache

重构代码

可能你熟悉使用 gofmt 工具来自动格式化代码。但是,这个工具也支持_重写规则_,这样,就可以用它来重构代码了。我会证明给你看。

假设你有以下代码,然后你想将 foo 变量修改为 Foo 以便于将其导出。

1
2
3
4
5
6
7
var foo int

func bar() {
foo = 1
fmt.Println("foo")
}

为了达到这个目的,可以这样使用 gofmt :带上 -r 标志来实现一条重写规则,带上 -d 标志来显示变更的差异,并且带上 -w 标志来_就地_进行变更。就像这样:

1
2
3
4
5
6
7
8
9
10
$ gofmt -d -w -r 'foo -> Foo' .
-var foo int
+var Foo int

func bar() {
- foo = 1
+ Foo = 1
fmt.Println("foo")
}

注意到了吗?这是不是比查找替换更智能? foo 变量已经更改,但是 fmt.Println() 语句中的 "foo" 字符串则保持不变。另外需要注意的是,gofmt 命令是以递归的方式工作的,因此,上面这条命令将操作当前目录和子目录中的所有 *.go 文件。

如果想要使用此功能,我推荐首先_不带_ -w 标志运行重写规则,然后先检查差异,以确保对代码所做的修改正如你所愿。

我们来看个稍微复杂点的例子。假设你想要更新代码以使用新的 Go 1.12 strings.ReplaceAll() 的函数来替代 strings.Replace()。要进行更改,可以这样运行:

1
2
$ gofmt -w -r 'strings.Replace(a, b, c, -1) -> strings.ReplaceAll(a, b, c)' .

在重写规则里,单个小写字母会被用作匹配任意表达式的通配符,而这些表达式将会在替换过程中被替换。

查看 Go 文档

你可以通过在终端使用 go doc 工具,查看标准库包的文档。我经常在开发的时候使用这个工具来快速检查某些东西,例如某个特定函数的名字或者函数签名。我发现,这比浏览基于 web 的文档 更快,并且总是可以离线使用。

1
2
3
4
5
6
$ go doc strings            # View simplified documentation for the strings package
$ go doc -all strings # View full documentation for the strings package
$ go doc strings.Replace # View documentation for the strings.Replace function
$ go doc sql.DB # View documentation for the database/sql.DB type
$ go doc sql.DB.Query # View documentation for the database/sql.DB.Query method

还可以使用 -src 标志来展示相关的 Go 源代码。例如:

1
2
$ go doc -src strings.Replace   # View the source code for the strings.Replace function

测试

运行测试

可以像这样,使用 go test 工具来运行项目中的测试:

1
2
3
4
$ go test .          # Run all tests in the current directory
$ go test ./... # Run all tests in the current directory and sub-directories
$ go test ./foo/bar # Run all tests in the ./foo/bar directory

通常,我会在启用 Go 的竞争检测器 的情况下运行测试。这有助于帮助我找出在实际使用的过程中可能会出现的_一些_数据竞争。就像这样:

1
2
$ go test -race ./...

必须注意的是,启用竞争检测器将会增加测试的总体运行时间。所以,如果经常在 TDD 工作流程中运行测试,那么,你可能更愿意只在进行预提交测试的时候使用竞争检测器。

从 1.10 开始,Go 会缓存包级别的测试结果。如果在两次测试运行期间都没有发生改变,并且在使用 go test 时带了相同的可缓存标志,那么,就会展示缓存的测试结果,并且旁边会有一个 "(cached)"。对于大型代码库,这大大加速了测试运行时间。如果要强制运行所有测试(并避免缓存),那么,可以是有 -count=1 标志,或者通过使用 go clean 工具来清除所有缓存的测试结果。

1
2
3
$ go test -count=1 ./...    # Bypass the test cache when running tests
$ go clean -testcache # Delete all cached test results

注意:缓存的测试结果与缓存的构建结果一并存储在 GOCACHE 目录下。如果不确定它在你机器上的哪个位置,可以运行 go env GOCACHE 来检查。

可以通过使用 -run 标志来限制 go test 运行特定的测试(和自测试)。这个标志的参数是一个正则表达式,只有名字匹配到这个正则表达式的用例才会运行。我喜欢将它与 -v 标志(启用详细模式)结合使用,这样,就会显示运行中的测试和子测试的名字。这个一种很有用的方式,它确保了我没有写错这个正则表达式,并且期待运行的测试实际上确实在运行!

1
2
3
4
$ go test -v -run=^TestFooBar$ .          # Run the test with the exact name TestFooBar
$ go test -v -run=^TestFoo . # Run tests whose names start with TestFoo
$ go test -v -run=^TestFooBar$/^Baz$ . # Run the Baz subtest of the TestFooBar test only

值得注意的其他几个标志是 -short(用来跳过长时间运行的测试)和 -failfast(在碰到第一个失败后会停止进一步的测试)。注意,-failfast 将不会缓存测试结果。

1
2
3
$ go test -short ./...      # Skip long running tests
$ go test -failfast ./... # Don't run further tests after a failure.

分析测试覆盖率

你可以在运行测试的时候,通过使用 -cover 标志来启用覆盖率分析。这将会输出每一个包的测试所覆盖的代码百分比,类似于:

1
2
3
$ go test -cover ./...
ok github.com/alexedwards/argon2id 0.467s coverage: 78.6% of statements

你还可以使用 -coverprofile 标志来生成一份_覆盖程序剖析文件_,然后像这样使用 go tool cover -html 命令,在 web 浏览器中查看:

1
2
3
$ go test -coverprofile=/tmp/profile.out ./...
$ go tool cover -html=/tmp/profile.out

这将为你提供所有测试文件的可导航列表,其中,测试覆盖到的代码显示为绿色,而没被覆盖到的则显示为红色。

如果想更进一步,可以设置 -covermode=count 标志,让覆盖程序剖析文件记录测试期间每个语句被执行的确切_次数_。

1
2
3
$ go test -covermode=count -coverprofile=/tmp/profile.out ./...
$ go tool cover -html=/tmp/profile.out

在浏览器中查看时,执行得更频繁的语句会以更饱和的绿色阴影显示,类似于:

注意:如果测试中使用了 t.Parallel() 命令,那么,应该使用标志 -covermode=atomic 来替代 -covermode=count,以确保准确计数。

最后,如果没有可供查看覆盖剖析文件的 web 浏览器,那么,可以在终端使用如下命令,按函数或者方法,查看测试测试覆盖率的细分信息:

1
2
3
4
5
$ go tool cover -func=/tmp/profile.out
github.com/alexedwards/argon2id/argon2id.go:77: CreateHash 87.5%
github.com/alexedwards/argon2id/argon2id.go:96: ComparePasswordAndHash 85.7%
...

压力测试

可以使用 go test -count 命令来连续多次运行一个测试。这在想要检查偶发或者间歇性失败的时候很有用。例如:

1
2
$ go test -run=^TestFooBar$ -count=500 .

在这个例子中,TestFooBar 测试将连续重复 500 次。但是,必须注意的是,测试将_串行_重复(即使它包含了 t.Parallel() 指令)。所以,如果你的测试在做一些相对较慢的事情,例如与数据库、硬盘或者互联网进行一次交互,那么,运行大量测试可能需要相当长的一段时间。

在这种情况下,你也许希望使用 stress 工具来以_并行_的方式多次执行相同的用例。可以像这样安装它:

1
2
3
$ cd /tmp
$ GO111MODULE=on go get golang.org/x/tools/cmd/stress

使用 stress 工具,首先需要为想要测试的特定包编译_测试二进制文件_。这可以使用 go test -c 命令来完成。例如,在当前目录下的包创建一个测试二进制文件:

1
2
$ go test -c -o=/tmp/foo.test .

在这个例子中,测试二进制文件将会输出为 /tmp/foo.test。稍后,你就可以像这样,使用 stress 工具来为这个测试二进制文件执行特定的测试了:

1
2
3
4
5
$ stress -p=4 /tmp/foo.test -test.run=^TestFooBar$
60 runs so far, 0 failures
120 runs so far, 0 failures
...

注意:在上面的例子中,我用了 -p 标志来限制 stress 使用的并行进程的个数为 4。如果不带这个标志,该工具默认会使用个数为 runtime.NumCPU() 的进程。

测试所有依赖

在为发布或者部署可执行文件,或者是公开分发代码之前,你可能会想要运行 go test all 命令:

1
2
$ go test all

这个命令将会对模块中的所有包和所有依赖项运行测试(包括测试_测试依赖_以及必要的_标准库包_)。并且它能够帮助验证所使用的依赖项的确切版本是否彼此兼容。这可能需要运行相当长的时间,但是,结果会得到缓存,因此,未来任何后续测试应该都会运行得比较快。如果需要的话,还可以使用 go test -short all,跳过任何长时间运行的测试。

预提交检查

格式化代码

Go 提供了两种工具来根据 Go 约定自动格式化代码:gofmtgo fmt。使用这些工具有助于让你的文件的项目中的代码保持一致。另外,如果在提交代码之前使用这些工具,那么就有助于在检查文件版本间差异的时候减少噪音。

我喜欢在使用 gofmt 工具的时候带以下标志:

1
2
3
$ gofmt -w -s -d foo.go  # 格式化 foo.go 文件
$ gofmt -w -s -d . # 递归格式化当前目录和子目录下的所有文件

在这些命令中,-w 标志指示工具就地重写文件;-s 标志指示工具在可能的情况下简化代码;而 -d 标志指示工具输出改动的差异(因为我很好奇更改了啥)。如果你只是想要看看改动了哪些文件而不是具体差异,那么,可以将 -d 标志改为 -l

注意:gofmt 命令以递归的方式工作。如果你给它传递一个目录,例如 . 或者 ./cmd/foo,那么,它只会格式化这个目录下的所有 .go 文件。

另一个格式化工具 go fmt 是对 gofmt 的一个封装,它基本上等同于对指定的文件或者目录调用了 gofmt -l -w。你可以像这样使用它:

1
2
$ go fmt ./...

执行静态分析

go vet 工具会对你的代码进行静态分析,然后对_可能_出现(编译器可能无法获取)的代码错误进行示警。像是无法访问的代码、不必要的赋值和错误格式的构建标记等问题。可以像这样使用它:

1
2
3
4
5
$ go vet foo.go     # Vet the foo.go file
$ go vet . # Vet all files in the current directory
$ go vet ./... # Vet all files in the current directory and sub-directories
$ go vet ./foo/bar # Vet all files in the ./foo/bar directory

在背后,go vet 会运行一系列不同的分析器(列在这里)。你可以根据具体情况禁用特定的分析器。例如,你可以这样禁用 composite 分析器:

1
2
$ go vet -composites=false ./...

golang.org/x/tools 中有一些实验分析器,或许你会想要试一试:nilness(检查冗余或者不可能为 nil 的比较)和 shadow(检查可能意外会出现的变量遮蔽)。如果想要使用它们,那么需要单独安装和运行这些工具。例如,要安装 nilness 工具,可以这样运行:

1
2
3
$ cd /tmp
$ GO111MODULE=on go get golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness

然后,就可以像这样使用它:

1
2
$ go vet -vettool=$(which nilness) ./...

注意:当使用了 -vettool 标志,go vet 将_只会_运行指定的分析器,这意味着将不会运行所有其他 go vet 分析器。

作为旁注,从 Go 1.10 开始,在运行任何测试之前,go test 工具将自动执行一个小的高可信的 go vet 测试子集。你可以在运行测试的时候关掉这种行为,就像这样:

1
2
$ go test -vet=off ./...

Linting Code

可以使用 golint 工具来识别代码中的_样式错误_。和 go vet 不一样,它不关系代码的_正确性_,而是帮助你将代码与 Effective Go 和 Go CodeReviewComments 中的样式约定对齐。

这个工具并不是标准库的一部分,因此,需要像这样安装它:

1
2
3
$ cd /tmp
$ GO111MODULE=on go get golang.org/x/lint/golint

然后,就可以像下面一样运行它了:

1
2
3
4
5
$ golint foo.go     # Lint the foo.go file
$ golint . # Lint all files in the current directory
$ golint ./... # Lint all files in the current directory and sub-directories
$ golint ./foo/bar # Lint all files in the ./foo/bar directory

整理和验证依赖关系

在提交代码任意改动之前,我建议你运行以下两个命令来整理和验证依赖项:

1
2
3
$ go mod tidy
$ go mod verify

go mod tidy 命令将会修剪 go.modgo.sum 文件中未使用的依赖项,然后更新文件,以包含用于所有可能的构建标签/操作系统/架构组合的依赖项(注意:go rungo testgo build 等都是“懒惰的”,它们只会获取当前构建标签/操作系统/架构所需的包)。在每次提交前运行这个命令,会让你更容易在查看版本控制历史时,确定哪些代码更改负责添加或者删除哪些依赖项。

我还推荐使用 go mod verify 命令来检查计算机上的依赖项自下载后没有被意外(或者故意)更改,并且它们匹配 go.sum 文件中的加密哈希值。运行此命令有助于确保所使用的依赖项确确实实是你所期望的,并且以后对该提交的任何构建都能重现。

构建和部署

构建可执行文件

你可以使用 go build 工具来编译 main 包,并创建一个可执行的二进制文件。通常,我会将其与 -o 标志结合使用,明确设置输出目录和二进制文件的名字,就像这样:

1
2
3
$ go build -o=/tmp/foo .            # 在当前目录下编译包
$ go build -o=/tmp/foo ./cmd/foo # 在 ./cmd/foo 目录下编译包

在这些示例中,go build 将会_编译_特定的包(和任何依赖包),然后调用_链接器_来生成一个可执行的二进制文件,并将其输出到 /tmp/foo

值得注意的是,从 Go 1.10 开始,go build 工具会将构建输出缓存到_构建缓存_中。这缓存的输出将在未来的构建中的适当时机重新使用。这就可以显著加快整体的构建时间。这种新的缓存行为意味着,“相比 go build,宁可 go install” 的“古训”不再适用。

如果不确定构建缓存位于何处,那么可以运行 go env GOCACHE 命令来检查:

1
2
3
$ go env GOCACHE
/home/alex/.cache/go-build

使用构建缓存时需要特别注意:它并不会检查使用 cgo 导入的 C 库的改动。因此,如果你的代码通过 cgo 来导入一个 C 库,并且自上次构建后又对其进行了更改,那么需要使用 -a 标志来强制所有的包重新构建。或者,可以使用 go clean 来清除缓存:

1
2
3
$ go build -a -o=/tmp/foo .     # 强制重新构建所有包
$ go clean -cache # 清除构建缓存

注意:运行 go clean -cache 将也会删除缓存的测试结果。

如果你对 go build 的幕后操作感兴趣,那么可能会想要使用以下命令:

1
2
3
$ go list -deps . | sort -u     # 列出所有用来构建可执行文件的包
$ go build -a -x -o=/tmp/foo . # 重构所有东西,并且显示所运行的命令

最后,如果在非 main 包上运行 go build 命令,那么将会在临时位置编译这个包,并且结果将会被存储在构建缓存中。不会生成任何可执行文件。

交叉编译

这是我最喜欢的 Go 功能之一。

默认情况下,go build 将会输出一个适用于当前操作系统和架构的二进制文件。但它也支持交叉编译,这样,你就可以生成适合在不同机器上使用的二进制文件了。如果你在一个操作系统上开发,却在另一个操作系统上部署,那么这特别有用。

可以分别设置 GOOSGOARCH 环境变量来指定希望为何种操作系统和架构创建二进制文件。例如:

1
2
3
$ GOOS=linux GOARCH=amd64 go build -o=/tmp/linux_amd64/foo .
$ GOOS=windows GOARCH=amd64 go build -o=/tmp/windows_amd64/foo.exe .

要查看所有支持的操作系统和架构组合列表,可以运行 go tool dist list

1
2
3
4
5
6
7
8
9
10
$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
...

提示:可以使用 Go 的交叉编译来创建 WebAssembly 二进制文件

有关交叉编译的更深入的信息,推荐阅读这篇优秀的博文

使用编译器和链接器标志

在构建自己的可执行文件时,可以使用 -gcflags 标志来更改编译器的行为,并查看执行的详细信息。可以通过运行以下命令,查看可用编译器标志的完整列表:

1
2
$ go tool compile -help

其中一个你可能会感兴趣的标志是 -m,带上了这个标志会触发打印编译期间所进行的优化决策。可以这样使用它:

1
2
$ go build -gcflags="-m -m" -o=/tmp/foo . # 输出优化决策信息

在上面的例子中,我用了 -m 标志两次,表示我想要打印量级深度的决策信息。你可以通过使用一次来获得简单点的输出。

此外,从 Go 1.10 开始,编译器标志只适用于传给 go build 的指定包,在上面的例子中就是当前目录下的包(由 . 表示)。如果你想打印所有包的优化决策(包括依赖项),那么可以用这个命令替代:

1
2
$ go build -gcflags="all=-m" -o=/tmp/foo .

从 Go 1.11 开始,你应该会发现比之前更容易调试优化过的二进制文件了。然而,如果有需要的话,仍然可以使用标志 -N 来禁用优化,使用 -l 来禁用内联。例如:

1
2
$ go build -gcflags="all=-N -l" -o=/tmp/foo .  # 禁用优化和内联

可以运行以下命令来查看可用的链接器标志列表:

1
2
$ go tool link -help

其中,可能最有名的就是 -X 标志了,它允许你将一个(字符串)值“烧入”应用里指定的变量。这通常用于添加版本号或者提交哈希。例如:

1
2
$ go build -ldflags="-X main.version=1.2.3" -o=/tmp/foo .

更多关于 -X 标志和示例代码的信息,请参阅这个 StackOverflow 问题 和本文以及这篇文章

你可能还有兴趣使用 -s-w 标志来删除二进制文件中的调试信息。这通常会让生成的二进制文件减少 25% 的大小。例如:

1
2
$ go build -ldflags="-s -w" -o=/tmp/foo .  # 删除二进制文件中的调试信息

注意:如果需要优化二进制文件大小,那么或许会想要使用 upx 来压缩它。更多信息,请参阅这篇文章

诊断问题和优化

运行和比较基准

Go 的一个很好的功能是,它让你可以轻松对代码进行基准测试。如果你不熟悉编写基准的一般过程,那么,这里这这里有不错的指南。

要运行基准测试,你需要使用 go test 工具,并且将 -bench 标志设置为一个可以匹配你想要执行的基准测试的正则表达式。例如:

1
2
3
4
$ go test -bench=. ./...                        # 运行所有基准测试和基本测试
$ go test -run=^$ -bench=. ./... # 运行所有基准测试(不运行基本测试)
$ go test -run=^$ -bench=^BenchmarkFoo$ ./... # 只运行 BenchmarkFoo 这个基准测试(不运行基本测试)

我几乎总用 -benchmem 标志来运行基准测试,这会强制输出内存分配统计信息。

1
2
$  go test -bench=. -benchmem ./...

默认情况下,每个基准测试都会运行至少一秒,并且只运行一次。可以使用 -benchtime-count 标志来更改此行为。

1
2
3
4
$ go test -bench=. -benchtime=5s ./...       # 每个基准测试至少运行 5 秒
$ go test -bench=. -benchtime=500x ./... # 每个基准测试运行 500 次迭代
$ go test -bench=. -count=3 ./... # 每个基准测试重复 3 次

如果进行基准测试的代码使用了并发,那么,可以使用 -cpu 标志来查看更改 GOMAXPROCS 值对性能产生的影响(实际上,是可以同时执行 Go 代码的 OS 线程数)。例如,将 GOMAXPROCS 设置为 1,4 和 8,运行基准测试:

1
2
$ go test -bench=. -cpu=1,4,8 ./...

要比较基准测试之间的变动,你可能需要使用 benchcmp 工具。这个工具不属于标准的 go 命令,所以需要像这样安装它:

1
2
3
$ cd /tmp
$ GO111MODULE=on go get golang.org/x/tools/cmd/benchcmp

然后,可以像这样使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ go test -run=^$ -bench=. -benchmem ./... > /tmp/old.txt
# make changes
$ go test -run=^$ -bench=. -benchmem ./... > /tmp/new.txt
$ benchcmp /tmp/old.txt /tmp/new.txt
benchmark old ns/op new ns/op delta
BenchmarkExample-8 21234 5510 -74.05%

benchmark old allocs new allocs delta
BenchmarkExample-8 17 11 -35.29%

benchmark old bytes new bytes delta
BenchmarkExample-8 8240 3808 -53.79%

分析和跟踪

Go 可以让你创建关于 CPU 使用、内存使用、goroutine 阻塞和互斥锁争用的诊断性_程序剖析文件_。你可以使用它们来深入挖掘应用,以确切了解应用是如何使用(或者等待)资源的。

有三种生成程序剖析文件的方法:

  • 如果是 web 应用,那么可以导入 net/http/pprof 包。这个操作将向 http.DefaultServeMux 注册一些处理函数。这样,你就可以为运行中的应用生成并下载程序剖析文件了。这篇文章 对其进行了很好的解释,并且提供了一些示例代码。
  • 对于其他类型的应用,则可以使用 pprof.StartCPUProfile()pprof.WriteHeapProfile() 函数来生成运行中的程序剖析文件。runtime/pprof 文档中有一些示例代码可供参考。
  • 或者,你可以在运行基准测试或者基础测试的时候生成程序剖析文件,只需像这样使用各种 -***profile 标志即可:
1
2
3
4
5
$ go test -run=^$ -bench=^BenchmarkFoo$ -cpuprofile=/tmp/cpuprofile.out .
$ go test -run=^$ -bench=^BenchmarkFoo$ -memprofile=/tmp/memprofile.out .
$ go test -run=^$ -bench=^BenchmarkFoo$ -blockprofile=/tmp/blockprofile.out .
$ go test -run=^$ -bench=^BenchmarkFoo$ -mutexprofile=/tmp/mutexprofile.out .

注意:运行基准测试或者基础测试的时候使用 -***profile 标志,当前目录下会生成一个测试二进制文件。如果你想将其输出到另一个地方,那么应该使用 -o 标志,就像这样:

1
2
$ go test -run=^$ -bench=^BenchmarkFoo$ -o=/tmp/foo.test -cpuprofile=/tmp/cpuprofile.out .

无论使用何种方式来生成程序剖析文件,当启用了程序剖析,Go 程序每秒将停止大约 100 次,并在停止的时刻创建程序快照。根据这些 样本 生成一份_程序剖析文件_,然后就可以使用 pprof 文件进行分析了。

我最喜欢的检查剖析文件的方式是,使用 go tool pprof -http 命令来将其在 web 浏览器中打开。例如:

1
2
$ go tool pprof -http=:5000 /tmp/cpuprofile.out

这个操作将会默认显示一个_图表_,展示了应用在采样情况下的执行树。这让你可以快速了解任何资源的使用“热点”。在上面的图中,我们可以看到,CPU 使用率方面的热点是来源于 ioutil.ReadFile() 的两个系统调用。

你还可以导航到剖析文件的其他_视图_,包括函数和源代码的最高使用情况。

如果信息量太大,那么,或许你会想要使用 --nodefraction 标志,忽略小于样本一定百分比的节点。例如,要忽略那些少于样本 10% 的节点,可以这样运行 pprof

1
2
$ go tool pprof --nodefraction=0.1 -http=:5000 /tmp/cpuprofile.out

这会让生成的图不那么“嘈杂”。此时,如果放大这张屏幕截图,那么就可以更清楚地看到并了解 CPU 使用热点在哪里。

分析和优化资源使用是一项大但是细微的主题,而这里我只涉及皮毛。如果你对此感兴趣,并想了解更多,那么建议你阅读以下文章:

另一个可以用来协助诊断问题的工具是_运行时执行跟踪器_。它可以让你了解 Go 是如何创建和调度 goroutine 运行的,垃圾收集器何时运行,以及关于阻塞系统调用/网络/sync 操作的信息。

同样的,你也可以从基础测试和基准测试中生成跟踪文件,或者使用 net/http/pprof 来创建并下载应用的跟踪文件。然后,就可以使用 go tool trace,像这样在 web 浏览器中查看输出了:

1
2
3
$ go test -run=^$ -bench=^BenchmarkFoo$ -trace=/tmp/trace.out .
$ go tool trace /tmp/trace.out

重要提示:目前,只能在 Chrome 或者 Chromium 中查看。

关于 Go 的执行跟踪器以及如何解释输出的更多信息,请看 Rhys Hiltner 的 dotGo 2016 演讲 和这篇优秀的博客

检查竞争条件

我前面谈过使用 go test -race,在测试过程中启用 Go 的竞争检测器。但是,你也可以在构建可执行文件的过程中,为运行中的程序启用它,就像这样:

1
2
$ go build -race -o=/tmp/foo .

非常值得注意的是,启用了竞争检测器的二进制文件将比正常情况下使用更多的 CPU 和内存。因此,在正常情况下,构建用于生产环境的二进制文件时,不应该使用 -race 标志。

但是,你可能希望在服务器池中的某台服务器上面部署启用了竞争检测器的二进制文件。或者用它来跟踪可疑的竞争条件,方法是使用负载测试工具,对启用了竞争检测器的二进制文件并发施压。

默认情况下,在二进制文件运行过程中,如果检测到了竞争,那么会对 stderr 写入一条日志。如果必要,你可以通过使用 GORACE 环境变量来改变这种行为。例如,要运行位于 /tmp/foo 的二进制文件,并将竞争日志写到 /tmp/race.<pid>,则可以这样:

1
2
$ GORACE="log_path=/tmp/race" /tmp/foo

管理依赖关系

可以使用 go list 工具来检查指定的依赖项是否有更新版本,如下所示:

1
2
3
$ go list -m -u github.com/alecthomas/chroma
github.com/alecthomas/chroma v0.6.2 [v0.6.3]

这将输出当前使用的依赖项的名字和版本,如果存在较新的版本,那么后面会跟着方括号 [],里面是最新的版本。还可以使用 go list 来检查所有依赖项(和子依赖项)的更新。如下所示:

1
2
$ go list -m -u all

可以升级(或者降级)一个依赖项至最新的版本,只需像这样在 go get 命令后指定发布标记或者提交哈希即可:

1
2
3
4
$ go get github.com/foo/bar@latest
$ go get github.com/foo/bar@v1.2.3
$ go get github.com/foo/bar@7e0369f

如果你正在更新的依赖项带有 go.mod 文件,那么,根据这份 go.mod 文件的信息,如果有需要的话,还将下载_子依赖项_的更新。如果使用 go get -u 标志,那么将忽略 go.mod 文件的内容,然后所有子依赖项将会被升级至最新的次要或者补丁版本(即使 go.mod 指定了不同的版本。)。

升级或者降级任何依赖项后,最好整理你的 mod 文件。可能你还会希望运行所有包的测试,从而检查兼容性。就像这样:

1
2
3
$ go mod tidy
$ go test all

有时,你可能想使用本地版本的依赖项(例如,在上游合并补丁之前,需要使用本地分支)。为此,可以使用 go mod edit 命令,将 go.mod 文件中的依赖项替代为本地版本。例如:

1
2
$ go mod edit -replace=github.com/alexedwards/argon2id=/home/alex/code/argon2id

这将像这样为你的 go.mod 文件添加一个_替代规则_。然后。未来的 go rungo build 等操作将会使用本地版本。

1
2
3
4
5
6
7
8
9
//File: go.mod
module alexedwards.net/example

go 1.12

require github.com/alexedwards/argon2id v0.0.0-20190109181859-24206601af6c

replace github.com/alexedwards/argon2id => /home/alex/Projects/playground/argon2id

一旦不需要本地版本了,就可以用下面这个命令来移除替换规则:

1
2
$ go mod edit -dropreplace=github.com/alexedwards/argon2id

可以使用同样的技巧来导入_只_存在于你自己文件系统上的包。如果你同时处理开发中的多个模块,其中一个模块依赖另一个模块,那么这将会很有用。

注意:如果不想使用 go mod edit 命令,可以手动编辑 go.mod 文件来进行修改。无论哪种方式都能行得通。

升级到新版本

go fix 文件最初于 2011 年发布(当时对于 Go 的 API 仍有定期修改),用以帮助用户自动更新旧代码至与 Go 的新版本兼容。从此,Go 的兼容性承诺意味着,如果你从一个 Go 1.x 版本升级至更新的 1.x 版本,那么将一切正常,故而通常没有必要使用 go fix

但是,它确实处理了一些非常具体的问题。可以运行 go tool fix -help 来查看这些问题的摘要。如果你决定升级之后想要或者需要运行 go fix,那么应该运行以下命令,然后在提交前检查更改的差异。

1
$ go fix ./...

报告错误

如果你确信找到了 Go 的标注库、工具或者文档中未报告的错误,那么可以使用 go bug 命令来创建一个新的 Github issue。

1
$ go bug

这个命令将会打开一个浏览器窗口,其中打开了 issue 页面,并且与填充了系统信息和报告模板。

备忘

2019-04-19 更新:[@FedirFR](https://twitter.com/FedirFR) 根据这篇文章制作了一份备忘。你可以在这里下载.

请言小午吃个甜筒~~