偶尔有人问我:“你为什么喜欢Go?”而我经常提到的一件事是:作为go命令的一部分,与语言一起存在的实用工具。有一些我每天使用的工具,比如go fmt和go build,还有一些工具,比如go tool pprof,我只使用它们来解决特定的问题。但总的来说,我很欣赏这样一个事实,即它们使管理和维护我的项目变得更容易。
在这篇文章中,我希望提供一些我认为最有用的工具,更重要的是,解释它们如何适合典型项目的工作流程。如果你是新手,我希望它能给你一个好的开始。或者,如果你已经使用Go一段时间了,有些东西并不适用于你,希望你仍然会发现一个命令是你以前不知道的。
1、安装工具
在这篇文章中,我将主要关注go命令的一部分工具。要在使用Go module的同时安装模块,首先需要确保您在启用模块的目录之外(我通常只需要更改到/tmp)。然后可以使用GO111MODULE=on go get命令安装工具。例如:
$ cd /tmp
$ GO111MODULE=on go get golang.org/x/tools/cmd/stress
这将下载相关的包和依赖项,构建可执行文件并将其添加到GOBIN目录中。如果您没有明确设置GOBIN目录,那么可执行文件将被添加到您的GOPATH/bin文件夹中。无论采用哪种方式,您都应该确保有适当的目录在系统路径上。
2、查看环境信息
您可以使用go env工具来显示关于当前go运行环境的信息。如果您使用的是不熟悉的机器,那么这一点特别有用。
$ 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。例如:
$ go env GOPATH GOOS GOARCH
/home/Alex/go
linux
amd64
要显示所有go env变量和值的文档可以运行:
go help environment
开发工具
运行代码
在开发过程中,go run工具是测试代码的一种方便的方法。它本质上是一个编译代码的快捷方式,在/tmp目录中创建一个可执行的二进制文件,然后运行这个二进制文件。
$ go run . # 在当前目录下运行包
$ go run ./cmd/foo # 在“./cmd/foo”目录下运行
获取依赖
假设您已经启用了go module,当您使用go run(或go test或go build)时,任何外部依赖项都将自动(并递归地)下载以完成代码中的导入语句。默认情况下,将下载该依赖项的最新标记版本,如果没有可用的标记版本,则下载最新提交的依赖项。
如果您提前知道需要某个依赖项的特定版本(而不是Go默认获取的版本),您可以使用带有相关版本号的Go get或commit哈希值。例如:
$ 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工具查看它们,如下所示:
$ go list -m all
有时你可能想知道为什么需要这个依赖?可以用go mod why命令来查看答案,它会显示从主模块中的包到给定依赖项的最短路径。例如:
$ 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命令将返回大多数(而不是全部)依赖。
如果您对应用程序依赖关系的分析或可视化感兴趣,那么您可能想要查看go mod graph工具。这里有一个生成可视化的教程和示例代码。
最后,下载的依赖项存储在位于GOPATH/pkg/mod的模块缓存中。如果您需要清除模块缓存,可以使用go clean工具。但是要注意:这将删除您机器上所有项目下载的依赖项。
$ go clean -modcache
代码重构
您可能很熟悉使用gofmt工具来自动格式化代码。但它也支持重写规则,您可以使用这些规则来帮助重构代码。我将演示。假设你有以下代码,你想把foo变量改为foo,这样它就可导出了。
var foo int
func bar() {
foo = 1
fmt.Println("foo")
}
要做到这一点,你可以使用带有-r标志的gofmt来实现重写规则,使用-d标志来显示更改的差异,使用-w标志来原地进行更改,如下所示:
$ 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标志的重写规则,先检查差异,以确保对代码的更改是您所期望的。让我们看一个稍微复杂一点的例子。假设您想要更新代码,使用新的strings.ReplaceAll()函数而不是strings.Replace()。要进行此更改,可以运行以下命令:
$ gofmt -w -r 'strings.Replace(a, b, c, -1) -> strings.ReplaceAll(a, b, c)' .
查看文档
您可以使用go doc工具通过终端查看标准库包的文档。我经常在开发过程中使用它来快速检查某些东西——比如特定函数的名称或签名。我发现它比浏览网络文档更快,而且它总是离线可用。
$ go doc strings # 查看字符串包的简化文档
$ go doc -all strings # 查看strings包的完整文档
$ go doc strings.Replace # 查看字符串文档中Replace函数
$ go doc sql.DB # 查看database/sql.DB 类型文档
$ go doc sql.DB.Query # 查看database/sql.DB.Query方法的文档
还可以包含-src标志以显示相关的Go源代码。例如:
$ go doc -src strings.Replace # 查看strings.Replace函数源码
测试Testing
执行测试
可以使用go test工具在项目中运行测试,如下所示:
$ go test . # 运行当前目录中的所有测试
$ go test ./... # 运行当前目录和子目录中的所有测试
$ go test ./foo/bar # 运行./foo/bar目录中的所有测试
通常情况下,我在运行测试时启用了Go的竞争检测,它可以帮助收集在实际使用中可能发生的一些数据竞争。像这样:
$ go test -race ./...
值得注意的是,启用竞争检测将增加测试的总体运行时间。因此,如果您在TDD工作流中非常频繁地运行测试,您可能更喜欢将其保存下来,只用于预提交测试运行。
从1.10开始,Go在包级别缓存测试结果。如果一个包在测试运行时没有更新——并且您正在使用相同的、可缓存的go测试标志——那么将显示缓存的结果并在旁边标注(cached)。这对于加快大型代码库的测试运行时非常有帮助。如果您希望强制完整运行测试(并避免缓存),您可以使用-count=1标志,或使用go clean工具清除所有缓存的测试结果。
$ go test -count=1 ./... # 运行测试时绕过测试缓存
$ go clean -testcache # 删除所有缓存的测试结果
注意:缓存的测试结果与缓存的构建结果一起存储在GOCACHE目录中。如果不确定它在机器上的位置,请检查go env GOCACHE。
可以使用-run标志将go test限制为运行特定的测试(和子测试)。它接受一个正则表达式,并且只运行名称与正则表达式匹配的测试。我喜欢将其与-v标志结合使用以启用详细模式,这样运行的测试和子测试的名称就会显示出来。这是一个有用的方法,可以确保我没有用错正则表达式,运行的是正确的测试用例。
$ go test -v -run=^TestFooBar$ . # 使用确切的名称TestFooBar运行测试
$ go test -v -run=^TestFoo . # 运行名称以TestFoo开头的测试
$ go test -v -run=^TestFooBar$/^Baz$ . #只运行TestFooBar测试的Baz子测试
需注意的两个标志是-short(可以用来跳过长时间运行的测试)和-failfast(在第一次失败后将停止运行剩下的测试)。注意-failfast将阻止缓存测试结果。
$ go test -short ./... #跳过长时间运行的测试
$ go test -failfast ./... # 在失败后不要运行后面的测试。
分析测试覆盖率
您可以在运行测试时使用-cover标志启用覆盖率分析。这将在每个包的输出中显示测试覆盖的代码的百分比,类似如下所示:
$ go test -cover ./...
ok github.com/alexedwards/argon2id 0.467s coverage: 78.6% of statements
你也可以使用-coverprofile标志生成覆盖配置文件,并通过go tool cover -html命令在你的浏览器中查看它,如下所示:
$ go test -coverprofile=/tmp/profile.out ./...
$ go tool cover -html=/tmp/profile.out
这将为您提供所有测试文件的导航列表,其中测试覆盖的代码显示为绿色,未覆盖的代码显示为红色。
如果愿意,可以进一步设置-covermode=count标志,使覆盖率配置文件记录测试期间每个语句执行的确切次数。
$ go test -covermode=count -coverprofile=/tmp/profile.out ./...
$ go tool cover -html=/tmp/profile.out
当在浏览器中查看时,执行频率更高的语句会显示为更饱和的绿色阴影,类似如下:
最后,如果你没有可用的web浏览器来查看覆盖率配置文件,你可以在终端中通过命令查看按功能/方法划分的测试覆盖率:
$ 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命令连续多次运行测试,如果希望检查零星或间歇故障,这可能很有用。例如:
$ go test -run=^TestFooBar$ -count=500 .
在本例中,TestFooBar测试将重复500次。但是需要注意的是,测试将以串行方式重复执行——即使它包含t.Parallel()指令。因此,如果测试的速度相对较慢,比如访问数据库、硬盘或互联网,那么运行大量测试可能会花费相当长的时间。
在这种情况下,您可能希望使用压力工具并行地多次重复相同的测试。你可以像安装stress:
$ cd /tmp
$ GO111MODULE=on go get golang.org/x/tools/cmd/stress
要使用压测工具,首先需要为要测试的特定包编译一个测试二进制文件。你可以使用go test -c命令。例如,要为当前目录中的包创建一个测试二进制文件:
$ go test -c -o=/tmp/foo.test .
在本例中,测试二进制文件将输出到/tmp/foo.test。然后,你可以使用压力工具在测试二进制文件中执行特定的测试,如下所示:
$ 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命令:
$ go test all
这将运行模块中的所有包和所有依赖项的测试——包括测试依赖项和必要的标准库包——它可以帮助验证所使用的依赖项的确切版本彼此兼容。这可能需要相当长的时间来运行,但使用测试结果缓存,后续测试都应该会很快。如果愿意,还可以使用go test -short all来跳过任何长时间运行的测试。
代码提交前的检查
格式化代码
Go提供了两个工具:gofmt和Go fmt来根据Go约定自动格式化代码。使用这些工具有助于保持您的代码在文件和项目之间的一致性,并且——如果您在提交代码之前使用它们——有助于在检查文件版本之间的差异时减少干扰。
我喜欢使用gofmt工具带有以下标志:
$ gofmt -w -s -d foo.go # 格式化foo.go文件
$ gofmt -w -s -d . # 递归格式化当前目录和子目录中的所有文件
在这些命令中,-w标志指示工具在适当的地方重写文件,-s指示工具在可能的情况下对代码进行简化,而-d标志指示工具输出更改的差异(因为我很想知道更改了什么)。如果您想只显示更改后的文件的名称,而不是显示差异,您可以将此替换为-l标志。
注意:gofmt命令是递归工作的。如果你传递给它一个目录如.或者./cmd/foo它会格式化目录下所有的.go文件。
另一个格式化工具gofmt - tool是一个包装器,它在指定的文件或目录上调用gofmt -l -w。你可以这样使用它:
$ go fmt ./...
静态分析工具
go vet工具对你的代码进行静态分析,并提醒你代码中可能存在的错误,但编译器不会发现这些错误。诸如执行不到的代码、不必要的分配和格式错误的构建标记等问题。用法如下:
$ go vet foo.go # 检查单个文件
$ go vet . # 检查当前目录中所有文件
$ go vet ./... # 检查当前目录和子目录中的所有文件
$ go vet ./foo/bar # 检查./foo/bar目录所有文件
go vet运行了很多不同的分析器点击查看,你可以根据具体情况禁用特定的分析器。例如,要禁用composite分析器:
$ go vet -composites=false ./...
在golang.org/x/tools中有几个您可能想要尝试的实验分析器:nilness(检查冗余或不可能的nil比较)和shadow(检查可能无意中隐藏的变量)。如果您想要使用这些分析器,您需要分别安装并运行。例如,要安装nilness,你可以运行:
$ cd /tmp
$ GO111MODULE=on go get golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness
然后你可以这样使用:
$ go vet -vettool=$(which nilness) ./...
注意:当使用-vettool参数时,它将只运行指定的分析器,所有其他go vet分析器将不会运行。
顺便提一下,从Go1.10开始,go test工具会在运行任何测试之前自动执行部分可靠的Go vet检查。您可以在运行测试时关闭该功能,如下所示:
$ go test -vet=off ./...
整理和验证代码依赖
在您提交任何代码修改之前,我建议运行以下两个命令来整理和验证代码依赖关系:
$ go mod tidy
$ go mod verify
go mod tidy命令将从go.mod和go.sum文件中删除任何未使用的依赖项,并更新包含所有可能的构建标签/OS/架构组合的依赖(注意:go run, go test, go build等都是“惰性命令”,只会获取当前构建环境所需的包)。每次提交代码之前运行此命令,在查看版本控制历史记录时可以更容易地确定,哪些代码导致添加或删除了哪些依赖项。
我还建议使用go mod verify命令来检查您的计算机上的依赖项是否在下载后意外(或有意)被更改,并且它们与您的go.sum文件中的加密哈希值是否匹配。运行此命令有助于确保所使用的依赖项与您所期望的完全一致。
构建和部署
要编译一个main包并创建一个可执行的二进制文件,你可以使用go build工具。通常,我将它与-o参数结合使用,让你显式地设置输出目录和二进制文件的名称:
$ go build -o=/tmp/foo . # 在当前目录中编译包
$ go build -o=/tmp/foo ./cmd/foo # 在./cmd/foo目录下编译
值得注意的是,从Go 1.10开始,Go build工具将缓存构建结果。这个缓存将在以后的构建中使用,这可以加快构建时间。如果你不确定构建缓存在哪里,你可以通过运行go env GOCACHE命令来查看:
$ go env GOCACHE
/home/alex/.cache/go-build
使用构建缓存有一个重要的注意事项——它不检测用cgo导入的C库的更改。因此,如果您的代码通过cgo导入了一个C库,并且自上次构建以来对它进行了更改,那么您将需要使用-a标志来强制重新构建所有包。或者,你可以使用go clean来清除缓存:
$ go build -a -o=/tmp/foo . # 强制重新生成所有包
$ go clean -cache # 从构建缓存中删除所有内容
注意:运行go clean -cache也会删除缓存的测试结果。
如果你对go build具体做什么感兴趣,你可以使用以下命令:
$ go list -deps . | sort -u # 列出用于构建可执行文件的所有包
$ go build -a -x -o=/tmp/foo . # 重新生成所有内容并显示正在运行的命令
最后,如果您在一个非main包里运行go build,它将在一个临时位置编译,结果将存储在构建缓存中。不生成可执行文件。
跨平台编译
默认情况下,go build将输出适合于当前操作系统和体系结构使用的二进制文件。但是它也支持交叉编译,因此您可以生成适合在不同机器上使用的二进制文件。如果您在一个操作系统上开发,在另一个操作系统上部署,那么这一点特别有用。
通过分别设置GOOS和GOARCH环境变量,可以指定要为其创建二进制文件的操作系统和体系结构。例如:
$ 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:
$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
...
关于交叉编译的更深入的信息,我推荐阅读这篇文章。
编译器和连接器参数
在构建可执行文件时,您可以使用-gcflags标志来改变编译器的行为,并查看关于执行的更多信息。你可以通过下面的命令查看可用编译器参数的完整列表:
$ go tool compile -help
您可能会感兴趣的一个参数是-m,它触发打印关于在编译期间做出的优化信息。使用如下:
$ go build -gcflags="-m -m" -o=/tmp/foo . # 打印关于优化决策的信息
在上面的示例中,我使用了两次-m标志来表示想打印两层深度的决策信息。只使用一个就可以得到更简单的输出。
另外,从Go 1.10开始,编译器标志只适用于go build的特定包——在上面的例子中是当前目录中的包(用.表示)。如果你想打印所有包的优化决策,包括依赖项,可以使用下面的命令:
$ go build -gcflags="all=-m" -o=/tmp/foo .
从Go 1.11开始,您会发现调试优化的二进制文件比以前更容易了。但是,如果需要,您仍然可以使用标志-N来禁用优化,使用标志-l来禁用内联。例如:
$ go build -gcflags="all=-N -l" -o=/tmp/foo . # 禁用优化和内联
您可能还对使用-s和-w参数,从二进制文件中剥离调试信息感兴趣。通常会使可执行文件大小减少25%。例如:
$ go build -ldflags="-s -w" -o=/tmp/foo . # 从二进制文件中剥离调试信息
注意:如果二进制文件大小是你需要优化的目标,你可能会使用upx来压缩它。更多信息请看这篇文章。