Skip to content

Latest commit

 

History

History
1215 lines (883 loc) · 64.7 KB

44 利其器!Go常用工具大检阅.md

File metadata and controls

1215 lines (883 loc) · 64.7 KB

44 利其器!Go常用工具大检阅

49 利其器!Go常用工具大检阅

Go 的一个设计哲学就是 “自带电池 (battery-included)”。前面说过,所谓 “电池”,除了包括标准库之外,还包括其他主流同类语言所不能媲美的 Go 工具链。对于每个 Gopher 而言,熟练掌握 Go 工具链中的这些工具,将对日常 Go 开发效率与代码质量带来事半功倍的效果。在本节中,我们将全面了解 Go 包开发生命周期使用的常用工具,涵盖主要原生工具以及应用较广的第三方工具。

1. 获取与安装

在 Go 包开发生命周期伊始,我们通常会获取和安装一些独立的 Go 包 (工具) 或项目的依赖包。Go 语言从诞生那一刻起就原生提供了获取和安装 Go 包的工具:go getgo install,它们也是如今每个 Gopher 每天几乎必会用到的工具,尤其是 go get

1) go get

go get 用于获取 Go 包及其依赖包。就像我们在前面章节所讲到的那样,对于刚刚进入 Go 世界的开发者而言,go get 给人一种的感觉是惊艳: 一键搞定包 (及依赖) 下载和安装。最初的 go get(gopath mode 下) 的行为与 GOPATH 绑定紧密,在 Go 1.11 版本引入 go module 后,在开启 module-aware mode 的情况下 (GO111MODULE=on),go get 就不再依赖 GOPATH 的设置了。因此,考虑到还有大量 Gopher 在非 go module 模式下工作,这里在深入 go get 用法时会考虑 go get 在两种模式下的行为差异。

  • go get -d:仅获取包源码

要获取一个托管在诸如 github.com 上的项目的源码,传统的方法是打开该项目主页,获取到该项目的 git 地址,然后在本地建立特定目录,并在这个目录下执行 git clone repo_url;如果该项目是 Go 项目,我们有 “一键式” 获取源码的方法,那就是 go get -d

go get 的标准行为是下载 Go 包及依赖包到本地,并编译和安装目标 Go 包。但如果给 go get 传入 -d 命令行标志选项,那么 go get 将仅会下载源码到本地,不会对目标包进行编译和安装。下面我们分别在 module-aware mode 和 gopath mode 下去获取我在 github.com 上托管的一个 Go 包:github.com/bigwhite/govanityurls

module-aware mode //go 1.14

$go get -d github.com/bigwhite/govanityurls
go: downloading github.com/bigwhite/govanityurls v0.0.0-20200921074623-184bfe1ae1b7
go: github.com/bigwhite/govanityurls upgrade => v0.0.0-20200921074623-184bfe1ae1b7
go: downloading gopkg.in/yaml.v2 v2.3.0

$tree -L 3 $GOPATH/pkg/mod
/root/go/pkg/mod
├── cache
│   └── download
│       ├── github.com
│       ├── gopkg.in
│       └── sumdb
├── github.com
│   └── bigwhite
│       └── [email protected] // 源码
└── gopkg.in
    └── [email protected]

gopath mode // go1.9.7

$go get -d github.com/bigwhite/govanityurls
$ls $GOPATH/src/github.com/bigwhite
govanityurls/
$ls $GOPATH/src/gopkg.in/
yaml.v2/ 

我们看到:在 module-aware mode 模式下 (GO111MODULE=on),go get -d 不仅将 govanityurls 包源码下载到 GOPATH[0]/pkg/mod 下面,而且还下载了 govanityurls 的 go.mod 中依赖的 gopkg.in/yaml.v2 源码;在 gopath mode 下,go get -d 同样将 govanityurls 和其依赖的 yaml.v2 下载到了 $GOPATH/src 下面。

不同的是 module-aware mode 模式下,go get 会分析目标 module 的依赖以决定下载依赖的版本 (go module 模式下依赖包的版本选择可以详见前面的 “使用 module 管理包依赖” 一节),这里下载的 yaml.v2 就是 govanityurls 的 go.mod 中显式要求的 v2.3.0 版本,而在 gopath mode 下,go get 下载的 yaml.v2 则是其 master 分支上的最新版 (latest)。

但这两种模式下,go get -d 都没有对下载后的代码进行编译和安装,我们在 $GOPATH/bin 下面没有看到可执行二进制文件 govanityurls。

除此之外,module-aware mode 下的 go get -d 支持获取指定版本的项目 (go get -d A@version) 以及其特定版本依赖包的源码,这点是传统 gopath mode 下 go get -d 所做不到的。

  • go get (无命令行标志选项,标准 go get)

go get -d 相比,标准 go get (无命令行标志选项) 不仅要下载项目和其依赖的源码,还要执行对下载的源码进行编译和安装。

在 gopath mode 下,如果目标源码最终被编译为一个可执行二进制文件,则该文件将被安装到 $GOBIN$GOPATH/bin 下;如果目标源码仅是库 (不包含 main 包),则编译后的库目标文件将以.a 文件的形式被安装到 $GOPATH/pkg/$GOOS_$GOARCH 下面;在 module-aware mode 下,编译出的可执行二进制文件也会被安装到 $GOBIN$GOPATH/bin下,如果目标源码是库,则只编译并将编译结果缓存下来 (linux 环境下缓存默认在~/.cache/go-build 下),但不安装。

  • go get -u:更新依赖版本

默认情况下,go get 仅会检查目标包以及它的依赖包在本地是否存在,如果不存在才会从远程获取。如果存在,那么即便远程仓库中的目标包以及依赖包版本发生了更新,go get 默认也会 “置之不理”。如果我们要 go get 更新目标包以及其依赖包的版本,我们需要给它传入 -u 命令行标志选项。

在经典的 gopath mode 下,如果本地既没有目标包,也没有目标包依赖的包存在,那么首次 go get -ugo get 执行的效果是一样的,都是将目标包以及它的依赖包的当前最新版本获取到本地。以我在 bitbucket.org 上托管的三个项目 p、q 和 r 为例 (p 直接依赖 q,q 直接依赖 r),我们首次在本地执行如下命令:

$go get -u bitbucket.org/bigwhite/p 

go get -u 会将 p、其直接依赖的 q 以及间接依赖的 r 的当前最新版本代码获取到本地并编译安装:

~/go/src/bitbucket.org/bigwhite/p$ go run main.go
this is project P
Q v0.1.0
R v0.1.0

// p, q, r被编译和安装
$ls ~/go/pkg/linux_amd64/bitbucket.org/bigwhite/
q.a r.a

$ls ~/go/bin
p 

这时,如果我们再执行一次标准 go get,即便 p, q, r 远程仓库的代码发生了更新 (比如:从 v0.1.0 升级到 v0.2.0),由于本地存在 p, q, r,go get 也不会做任何操作。而如果执行 go get -u bitbucket.org/bigwhite/p,那么 go 命令会检查 p 以及其直接依赖 q、间接依赖 r 是否有最新更新,如果有,则获取到本地。这样当我们再运行 p,我们将得到更新后的最新结果:

~/go/src/bitbucket.org/bigwhite/p$ go get -u bitbucket.org/bigwhite/p
~/go/src/bitbucket.org/bigwhite/p$ go run main.go
this is project P(upd)
Q v0.2.0
R v0.2.0 

不过在 gopath mode 模式下,执行 go get -u 是有风险的:由于没有版本的概念,go get -u 只是单纯地下载包的最新版本,一但最新版本无法编译或存在接口兼容性问题,就会导致你的包在本地无法通过编译。实际开发过程中,我们也经常遇到这个问题。

而在 go module-aware 模式下,go get 与 GOPATH 解绑,go get -u 根据目标 module 的 go.mod 中的依赖 module 版本获取满足要求的依赖 module 的 minor 版本或 patch 版本更新。下面是我托管在 bitbucket.org 上的三个 module:s、t 和 u。s 依赖 module t 中的包 t、module t 中的包 t 依赖 module u 中的包 u。初始状态 module t 和 u 都发布了 v1.0.0 版本,我们在开启 go module 的情况下用 go get -u 获取 module s:

$ go get -u bitbucket.org/bigwhite/s
go: downloading bitbucket.org/bigwhite/s v0.0.0-20201018032600-b810912d7dd5
go: bitbucket.org/bigwhite/s upgrade => v0.0.0-20201018032600-b810912d7dd5
go: finding module for package bitbucket.org/bigwhite/t
go: downloading bitbucket.org/bigwhite/t v1.0.0
go: found bitbucket.org/bigwhite/t in bitbucket.org/bigwhite/t v1.0.0
go: downloading bitbucket.org/bigwhite/u v1.0.0

$ ~/go/bin/s
this is project S
T v1.0.0
U v1.0.0 

下面我们更新 module u 中的包 u,将其升级为 v1.1.0,这种间接依赖的兼容更新也会被 go get -u 捕捉到,即便 module s 直接依赖的 module t 并没有任何改变:

$ go get -u bitbucket.org/bigwhite/s
go: bitbucket.org/bigwhite/s upgrade => v0.0.0-20201018032600-b810912d7dd5
go: finding module for package bitbucket.org/bigwhite/t
go: found bitbucket.org/bigwhite/t in bitbucket.org/bigwhite/t v1.0.0
go: bitbucket.org/bigwhite/u upgrade => v1.1.0
go: downloading bitbucket.org/bigwhite/u v1.1.0

$ ~/go/bin/s
this is project S
T v1.0.0
U v1.1.0 

存在一种特殊情况,如下图: 44 利其器!Go常用工具大检阅

图:module s,t,u,w 之间的依赖关系

module s 的直接依赖 module t 中的包 t1 (因添加 t1 包,module t 的版本变为 v1.1.0) 并未直接参与到 module s 的构建,这时如果采用 go get -u 更新 module s 的依赖版本,go get -u 仅会更新 t 到最新的兼容版本 (v1.1.0),而 module t 中未直接参与构建包 s 的 t1 包所依赖的 module w 并不会被下载更新到本地:

$go get -u bitbucket.org/bigwhite/s
go: bitbucket.org/bigwhite/s upgrade => v0.0.0-20201018032600-b810912d7dd5
go: finding module for package bitbucket.org/bigwhite/t
go: downloading bitbucket.org/bigwhite/t v1.1.0
go: found bitbucket.org/bigwhite/t in bitbucket.org/bigwhite/t v1.1.0
go: bitbucket.org/bigwhite/u upgrade => v1.1.0 
  • go get -t:获取测试代码依赖的包

-t 命令行标志选项是一个辅助选项,它通常与 - d 或 - u 组合使用,用来指示 go get 在仅下载源码或构建安装时要考虑测试代码的依赖,将测试代码的依赖包一并获取。该标志选项比较简单,这里不再赘述了。

  • GOPATH mode 和 module-aware mode 下的 go get 行为对比 (见下表)

表:go get 命令在 GOPATH mode 和 module-aware mode 下的对比

命令标志选项 / 特性 go get(GOPATH mode) go get(module-ware mode)
支持 GOPROXY 不支持 支持
支持获取特定版本 不支持 (go get A) 支持 (go get A@version)
依赖 GOPATH 依赖 不依赖
go get 获取包最新源码 (包括依赖包最新源码),放置到 $GOPATH/src 下面,编译源码并安装 获取 module 特定版本源码 (包括依赖 module 特定版本源码),放置到 $GOPATH/pkg/mod 下面,编译源码并安装
go get -d 获取包最新源码 (包括依赖包最新源码),放置到 $GOPATH/src 下面,但不编译安装 获取 module 特定版本源码 (包括依赖 module 特定版本源码),放置到 $GOPATH/pkg/mod 下面,但不编译安装
go get -u 获取包的最新版本以及其依赖包的最新版本,但常会因依赖包无法编译或版本不兼容问题而导致编译失败 根据目标 module 的 go.mod 中的依赖 module 版本获取满足要求的依赖 module 的 minor 版本或 patch 版本更新
go get -t 获取包中测试代码的依赖包的最新版本 获取 module 中测试代码的满足 go.mod 要求的依赖 module 版本

2) go install

由于 go get 知名度太高且涵盖了对目标包 /module 及依赖的安装功能,这使得 go install 命令长期活在了 go get 的阴影之下,以至于在日常开发中,我们已经很少直接使用 go install 了。但 go install 仍然是重要的工具命令,尤其是在仅进行本地安装时,它可以将本地构建出的可执行文件安装到 $ \GOBIN(默认值为 $GOPATH/bin) 下,将包目标文件 (.a) 安装到 GOPATH/pkg/∗∗*GOPATH*/*pkg*/∗∗GOOS_$GOARCH** 下。

和 go get 一样,go install 在 gopath mode 和 module-aware mode 下的行为略有差异,我们分别来看一下。

  • 引入 go module 之前的 gopath mode

在 Go 1.11 之前的版本中,go 工具链依然以 GOPATH 为中心。在这个版本范围内,我们先针对已经下载到本地的 bitbucket.org/bigwhite/p 进行一次安装操作 (环境为 ubuntu 18.04, Go 1.19.7):

$go install -x -v bitbucket.org/bigwhite/p

WORK=/tmp/go-build188075949
... ...

cd /root/go/src/bitbucket.org/bigwhite/r
$GOROOT/pkg/tool/linux_amd64/compile -o $WORK/bitbucket.org/bigwhite/r.a -trimpath $WORK -goversion go1.9.7 -p bitbucket.org/bigwhite/r -complete -buildid a0ebded166066b1d119b9824683b6e6f48e09a7f -D _/root/go/src/bitbucket.org/bigwhite/r -I $WORK -pack ./r.go
mkdir -p /root/go/pkg/linux_amd64/bitbucket.org/bigwhite/
mv $WORK/bitbucket.org/bigwhite/r.a /root/go/pkg/linux_amd64/bitbucket.org/bigwhite/r.a
... ...
cd /root/go/src/bitbucket.org/bigwhite/q
... ...
mv $WORK/bitbucket.org/bigwhite/q.a /root/go/pkg/linux_amd64/bitbucket.org/bigwhite/q.a
... ...
cd /root/go/src/bitbucket.org/bigwhite/p
$GOROOT/pkg/tool/linux_amd64/compile -o $WORK/bitbucket.org/bigwhite/p.a -trimpath $WORK -goversion go1.9.7 -p main -complete -buildid be75b0d03dabdcd80b97e23b4a05bb1fad07b11e -D _/root/go/src/bitbucket.org/bigwhite/p -I $WORK -I /root/go/pkg/linux_amd64 -pack ./main.go
cd .
/root/.bin/go1.9.7/pkg/tool/linux_amd64/link -o $WORK/bitbucket.org/bigwhite/p/_obj/exe/a.out -L $WORK -L /root/go/pkg/linux_amd64 -extld=gcc -buildmode=exe -buildid=be75b0d03dabdcd80b97e23b4a05bb1fad07b11e $WORK/bitbucket.org/bigwhite/p.a
mkdir -p /root/go/bin/
mv $WORK/bitbucket.org/bigwhite/p/_obj/exe/a.out /root/go/bin/p 

我们看到: go [install 编译了本地的 bitbucket.org/bigwhite/p 仓库下的 main 包,并将编译出的可执行二进制文件 p 安装到了 $GOBIN 下 (这里是~/go/bin 下),并将 main 包依赖的 q、r 包的目标文件 (.a) 安装到了 GOPATH/pkg/linux_amd64 下了。](http://xn--installbitbucket-4g1zy45imq4dve6ckvvbss8c.org/bigwhite/p%E4%BB%93%E5%BA%93%E4%B8%8B%E7%9A%84main%E5%8C%85%EF%BC%8C%E5%B9%B6%E5%B0%86%E7%BC%96%E8%AF%91%E5%87%BA%E7%9A%84%E5%8F%AF%E6%89%A7%E8%A1%8C%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%96%87%E4%BB%B6p%E5%AE%89%E8%A3%85%E5%88%B0%E4%BA%86*G**O**P**A**T**H*/*p**k**g*/*l**i**n**u**x*_*a**m**d*64下了。](*h**t**t**p*://*x**n*−−*i**n**s**t**a**l**l**b**i**t**b**u**c**k**e**t*−4*g*1*z**y*45*i**m**q*4*d**v**e*6*c**k**v**v**b**s**s*8*c*.*o**r**g*/*b**i**g**w**h**i**t**e*/*p*GOBIN%E4%B8%8B(%E8%BF%99%E9%87%8C%E6%98%AF~/go/bin%E4%B8%8B)%EF%BC%8C%E5%B9%B6%E5%B0%86main%E5%8C%85%E4%BE%9D%E8%B5%96%E7%9A%84q%E3%80%81r%E5%8C%85%E7%9A%84%E7%9B%AE%E6%A0%87%E6%96%87%E4%BB%B6(.a)%E5%AE%89%E8%A3%85%E5%88%B0%E4%BA%86$GOPATH/pkg/linux_amd64%E4%B8%8B%E4%BA%86%E3%80%82)

我们通过 go clean 可以清理掉当前目录下编译构建得到的可执行文件 p,但 go clean 既不能将 GOBIN 下的 p 清理掉,也无法清理掉 GOBIN 下的p清理掉,也无法清理掉‘GOPATH/pkg/linux_amd64下的目标文件 (\*.a)。go clean -i 会清理掉 $GOBIN 下的 p,但不会清理掉 $GOPATH/pkg/linux\_amd64 下的目标文件 (\*.a)。如要清理掉 $GOPATH/pkg/linux\_amd64 下的目标文件 (\*.a),比如 q,需要显式调用go clean -i bitbucket.org/bigwhite/q`。

将依赖的包的目标文件 (*.a) 安装到 $GOPATH/pkg/linux_amd64 下有一个好处,那就是后续使用 go build 再次构建 p 时,go 编译器不会像上面那样再去编译包 q、r 的代码,而是直接链接 $GOPATH/pkg/linux_amd64 下的 q.a 和 r.a,大大加快 build 速度,缩短 build 时间,提升开发者体验,这在 Go 1.11 引入 go module 之前的 Go 版本中十分实用

// 不会再重新编译r和q包:
$go build -x -v bitbucket.org/bigwhite/p
WORK=/tmp/go-build258243017
bitbucket.org/bigwhite/p
mkdir -p $WORK/bitbucket.org/bigwhite/p/_obj/
mkdir -p $WORK/bitbucket.org/bigwhite/p/_obj/exe/
cd /root/go/src/bitbucket.org/bigwhite/p
$GOROOT/pkg/tool/linux_amd64/compile -o $WORK/bitbucket.org/bigwhite/p.a -trimpath $WORK -goversion go1.9.7 -p main -complete -buildid be75b0d03dabdcd80b97e23b4a05bb1fad07b11e -D _/root/go/src/bitbucket.org/bigwhite/p -I $WORK -I /root/go/pkg/linux_amd64 -pack ./main.go
cd .
$GOROOT/pkg/tool/linux_amd64/link -o $WORK/bitbucket.org/bigwhite/p/_obj/exe/a.out -L $WORK -L /root/go/pkg/linux_amd64 -extld=gcc -buildmode=exe -buildid=be75b0d03dabdcd80b97e23b4a05bb1fad07b11e $WORK/bitbucket.org/bigwhite/p.a
mv $WORK/bitbucket.org/bigwhite/p/_obj/exe/a.out p 
  • 引入 go module 后的 legacy gopath mode

引入 go module 后,go 工具链的工作模式也分为两种:legacy gopath mode 和 module-aware mode,环境变量 GO111MODULE 的值决定了当前 go 工具链工作在哪种工模模式下。在 legacy gopath mode (go 1.14,GO111MODULE=off) 下,go install 仅会安装项目中 main 包编译后的二进制文件,而项目的其他依赖包被编译后则不会被安装到 GOPATH/pkg/*GOPATH*/pkg/GOOS_$GOARCH** 下,而仅会拷贝一份放到 $GOCACHE 下面。

此时如果要单独将项目的依赖包安装到 GOPATH/pkg/*GOPATH*/pkg/GOOS_$GOARCH** 下,我们需要使用 - i 命令行标志选项,以 bitbucket.org/bigwhite/p 的依赖包 r, q 为例:

# go install -i -x -v bitbucket.org/bigwhite/p
WORK=/tmp/go-build124912233
mkdir -p $WORK/b003/
mkdir -p /root/go/pkg/linux_amd64/bitbucket.org/bigwhite/
cp /root/.cache/go-build/57/57217139aa428b2e7cb736a5fbf6e6387a59333876336792673822efeea8e209-d /root/go/pkg/linux_amd64/bitbucket.org/bigwhite/r.a
rm -r $WORK/b003/
mkdir -p $WORK/b002/
cp /root/.cache/go-build/09/0927fbeb98b6c6ce73427ffc08821cb4ce74649e50ca1c6101fe44ba3962a2f1-d /root/go/pkg/linux_amd64/bitbucket.org/bigwhite/q.a
rm -r $WORK/b002/ 

我们看到:使用 - i 标志选项后,go install 不会重新安装项目 p 编译出的可执行文件,而仅仅是安装项目 p 的依赖包。

  • module-aware mode

在 module-aware mode 下 (GO111MODULE=on),go install 仅会将编译为可执行二进制文件的目标 module 安装到 $GOBIN 下,不会安装其依赖的 module 到 GOPATH/pkg/∗∗*GOPATH*/pkg/GOOS_$GOARCH** 下。即便加上 - i 命令行标志选型,依赖包或不能编译成可执行二进制文件的目标 module 都不会被安装,而仅会被缓存到的 $GOCACHE 下。

下面是三种模式下的行为特征对比汇总:

表:go install 命令在 GOPATH mode 和 module-aware mode 下的对比

命令标志选项 / 特性 gopath mode (go 版本 < 1.11) legacy gopath mode(GO111MODULE=off) module-aware mode
go install 安装可执行文件到 $GOBIN, 安装依赖包到 GOPATH/pkg/GOPAT**H/pkg/GOOS_$GOARCH 下 仅安装可执行文件到 $GOBIN,依赖包编译后放入 $GOCACHE 仅安装可执行文件到 $GOBIN,依赖包编译后放入 $GOCACHE
go install -i 不支持 安装依赖包到 GOPATH/pkg/GOPAT**H/pkg/GOOS_$GOARCH 下 仅将依赖包编译后放入 $GOCACHE 下

2. 包或 module 检视

go 原生提供了一个工具 go list 用于列出关于包 /module 的各类信息,这里把输出这类信息的行为称之为检视。go list 的检视功能甚为灵活和强大,它也因此被 Go 社区称为 “Go 工具链中的瑞士军刀”。它规整的输出信息常常被作为一些功能脚本的输入以实现某些更为高级的、自动化的检视和处理功能。

1) go list 基础

默认 go list 列出当前路径下的包的导入路径。如果是在 module-aware 模式下,go list 会在当前路径下寻找 go.mod:

// gopath mode
~/go/src/bitbucket.org/bigwhite/p $go list
bitbucket.org/bigwhite/p

// module-aware mode
~/go/src/bitbucket.org/bigwhite/s# go list
bitbucket.org/bigwhite/s 

在 gopath mode 下,如果当前路径下没有包,go list 命令会报如下错误:

~/go/src/bitbucket.org/bigwhite $go list
can't load package: package bitbucket.org/bigwhite: no Go files in /root/go/src/bitbucket.org/bigwhite 

在 gopath mode 下,go list 后面可以直接接包的导入路径。go list 会在 $GOPATH/src 下寻找该包,如果存在,则输出包的导入路径:

// gopath mode
$GO111MODULE=off go list bitbucket.org/bigwhite/p
bitbucket.org/bigwhite/p 

在 module-aware 模式下,如果当前路径下没有 go.mod 文件,go list 则会报出如下错误:

~/go/src/bitbucket.org/bigwhite $go list
go: cannot find main module; see 'go help modules' 

如果要列出当前路径以及子路径 (递归) 下面的所有包,可以用 go list {当前路径}/…

// gopath mode
~/go/src/bitbucket.org/bigwhite $GO111MODULE=off go list ./...
bitbucket.org/bigwhite/p
bitbucket.org/bigwhite/q
bitbucket.org/bigwhite/r
bitbucket.org/bigwhite/s

// module-aware mode

~/go/src/github.com/bigwhite/gocmpp $go list ./...
github.com/bigwhite/gocmpp
github.com/bigwhite/gocmpp/examples/cmpp2-client
github.com/bigwhite/gocmpp/examples/cmpp3-client
github.com/bigwhite/gocmpp/examples/cmpp3-server
github.com/bigwhite/gocmpp/fuzztest/fwd/gen
github.com/bigwhite/gocmpp/fuzztest/submit/gen
github.com/bigwhite/gocmpp/utils 

也可以使用包导入路径 +... 的方式,表示列出该路径下所有子路径下的包导入路径:

// gopath mode
$GO111MODULE=off go list bitbucket.org/bigwhite/...
bitbucket.org/bigwhite/p
bitbucket.org/bigwhite/q
bitbucket.org/bigwhite/r
bitbucket.org/bigwhite/s 

但 module-aware 模式下,go list 会尝试查找 go.mod 文件,如果当前目录不是 module 根目录,但使用 module 根路径 +... 的方式列举包会导致 go list 无法匹配到包:

// module-aware mode
~/go/src/bitbucket.org/bigwhite $go list bitbucket.org/bigwhite/...
go: warning: "bitbucket.org/bigwhite/..." matched no packages 

Go 原生保留了几个代表特定包或包集合的路径关键字:main、all、cmd 和 std。这些保留的路径关键字不要用于 go 包的构建中。

  • main:表示独立可执行程序的顶层包;
  • all:在 gopath mode 下,它可以展开为标准库和 GOPATH 路径下的所有包;在 module-aware mode 下,all 展开为主 module (当前路径下的 module) 下的所有包以及它们的所有依赖包,也包括测试代码的依赖包;
// gopath mode
~/go/src$ GO111MODULE=off go list all
archive/tar
archive/zip
bufio
... ...
vendor/golang.org/x/text/unicode/bidi
vendor/golang.org/x/text/unicode/norm
bitbucket.org/bigwhite/p
bitbucket.org/bigwhite/q
bitbucket.org/bigwhite/r
bitbucket.org/bigwhite/s
gopkg.in/yaml.v2

// module-aware mode
~/go/src/bitbucket.org/bigwhite/s$ go list all
bitbucket.org/bigwhite/s
bitbucket.org/bigwhite/t
bitbucket.org/bigwhite/u
bufio
bytes
... ...
unicode
unicode/utf16
unicode/utf8
unsafe
vendor/golang.org/x/crypto/chacha20
vendor/golang.org/x/crypto/chacha20poly1305
... ...
vendor/golang.org/x/text/unicode/bidi
vendor/golang.org/x/text/unicode/norm 
  • std:代表标准库所有包的集合
  • cmd:代码 Go 本身仓库下的 src/cmd 下的所有包以及 internal 包
~/go/src/bitbucket.org/bigwhite$ go list cmd
cmd/addr2line
cmd/api
... ...
cmd/link/internal/sym
cmd/link/internal/wasm
cmd/link/internal/x86
... ...
cmd/vendor/golang.org/x/tools/go/types/typeutil
cmd/vendor/golang.org/x/xerrors
cmd/vendor/golang.org/x/xerrors/internal
cmd/vet 

默认情况下,go list 输出的都是包的导入路径信息,当我们要列出 module 信息时,我们需要为 list 命令传入 - m 命令行标志选项:

~/go/src/bitbucket.org/bigwhite/s$go list -m 
bitbucket.org/bigwhite/s

// 列出当前主module以及它依赖的所有module
~/go/src/bitbucket.org/bigwhite/s$go list -m all
bitbucket.org/bigwhite/s
bitbucket.org/bigwhite/t v1.1.0
bitbucket.org/bigwhite/u v1.0.0
bitbucket.org/bigwhite/w v1.0.0 

2) 有关 module 的可用升级版本信息

通过 - m 标志位选项,我们可以让 go list 列出 module 信息,-m 就像是一个从包到 module 的转换开关。基于该开关,我们还可以通过传入其他标志位选项来获得更多有关 module 的信息,比如:通过 - u 我们可以获取到可用的 module 升级版本:

// 在$GOPATH/src/github.com/bigwhite/gocmpp目录下执行
$ go list -m -u all
github.com/bigwhite/gocmpp
github.com/dvyukov/go-fuzz v0.0.0-20190516070045-5cc3605ccbb6 [v0.0.0-20201003075337-90825f39c90b]
golang.org/x/text v0.3.0 [v0.3.3] 

我们看到:-u 选项分析了 gocmpp module 自身以及其依赖的 module 是否有新的版本可以升级,在结果中我们看到 gocmpp 依赖的 go-fuzz 和 text 两个 module 都有可升级的版本 (列在了方括号中)。

3. 构建

Go 原生的 go build 命令用于 Go 源码构建,多数情况下我们只需标准 go build (不加任何参数) 即可满足我们的构建需求。go build 还提供了很多命令行标志位选项可用于对构建过程出现的问题进行辅助诊断、定制构建以及向编译器 / 链接器传递参数。这些命令行标志位选项对高效掌握 go build 命令大有裨益。

1) -x -v 让构建过程一目了然

go build 过程会执行很多命令,但默认情况下,go build 并不会输出执行了哪些命令以及这些命令的执行细节,我们能看到的要么是构建成功,要么是 go build 输出一些错误信息。多数时候,go build 输出的错误信息可以指导我们很快修复错误并顺利通过下一次构建。但也有一些时候,单纯地依靠这些错误信息还不能定位构建问题究竟出在哪里,这个时候我们可以通过传入 -v 和 -x 命令行标志选项让 go build 输出构建过程都构建了哪些包以及究竟都执行了哪些命令。其中,-v 输出当前正在编译的包,而 -x 则输出 go build 执行的每一个命令

下面我们以一个较为简单的 module:bitbucket.org/bigwhite/s 为例:

~/go/src/bitbucket.org/bigwhite/s$ go build -x -v
WORK=/tmp/go-build372336922
... ...
go: downloading bitbucket.org/bigwhite/t v1.1.0
go: downloading bitbucket.org/bigwhite/u v1.0.0
... ...
bitbucket.org/bigwhite/u <--- 开始构建u
... ...
mkdir -p $WORK/b003/
... ...
cd /root/go/pkg/mod/bitbucket.org/bigwhite/[email protected]
/root/.bin/go1.14/pkg/tool/linux_amd64/compile -o $WORK/b003/_pkg_.a -trimpath "$WORK/b003=>" -p bitbucket.org/bigwhite/u -lang=go1.14 -complete -buildid AIwco_at8GJ_LW6GEnoC/AIwco_at8GJ_LW6GEnoC -goversion go1.14 -D "" -importcfg $WORK/b003/importcfg -pack ./u.go
/root/.bin/go1.14/pkg/tool/linux_amd64/buildid -w $WORK/b003/_pkg_.a # internal
cp $WORK/b003/_pkg_.a /root/.cache/go-build/fa/fa212f60686c96136b4211f281681858ed44fe1dc4809fe8996bb159fafc80ca-d # internal

bitbucket.org/bigwhite/t <--- 开始构建t
mkdir -p $WORK/b002/
cat >$WORK/b002/importcfg << 'EOF' # internal
# import config
packagefile bitbucket.org/bigwhite/u=$WORK/b003/_pkg_.a
packagefile fmt=/root/.bin/go1.14/pkg/linux_amd64/fmt.a
EOF
cd /root/go/pkg/mod/bitbucket.org/bigwhite/[email protected]
/root/.bin/go1.14/pkg/tool/linux_amd64/compile -o $WORK/b002/_pkg_.a -trimpath "$WORK/b002=>" -p bitbucket.org/bigwhite/t -lang=go1.14 -complete -buildid sgLS84mgIfSizBt_FtDY/sgLS84mgIfSizBt_FtDY -goversion go1.14 -D "" -importcfg $WORK/b002/importcfg -pack ./t.go
/root/.bin/go1.14/pkg/tool/linux_amd64/buildid -w $WORK/b002/_pkg_.a # internal
cp $WORK/b002/_pkg_.a /root/.cache/go-build/41/4181659098cd01fc534d7dcc06be18bc35dce111579e1baa5dbca94ff7a93146-d # internal

bitbucket.org/bigwhite/s <--- 开始构建s
mkdir -p $WORK/b001/
cat >$WORK/b001/_gomod_.go << 'EOF' # internal
package main
import _ "unsafe"
... ...
cat >$WORK/b001/importcfg << 'EOF' # internal
# import config
packagefile bitbucket.org/bigwhite/t=$WORK/b002/_pkg_.a
packagefile fmt=/root/.bin/go1.14/pkg/linux_amd64/fmt.a
packagefile runtime=/root/.bin/go1.14/pkg/linux_amd64/runtime.a
EOF
cd /root/go/src/bitbucket.org/bigwhite/s
/root/.bin/go1.14/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -lang=go1.14 -complete -buildid m8q9eZlHs_rkuXyx1ccf/m8q9eZlHs_rkuXyx1ccf -goversion go1.14 -D "" -importcfg $WORK/b001/importcfg -pack ./main.go $WORK/b001/_gomod_.go
/root/.bin/go1.14/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /root/.cache/go-build/c4/c4eabb90c98e5a3c676301cc0bf4a9da9e6727b004512e2bbf5ac61422be207b-d # internal

cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile bitbucket.org/bigwhite/s=$WORK/b001/_pkg_.a
packagefile bitbucket.org/bigwhite/t=$WORK/b002/_pkg_.a
packagefile fmt=/root/.bin/go1.14/pkg/linux_amd64/fmt.a
packagefile runtime=/root/.bin/go1.14/pkg/linux_amd64/runtime.a
packagefile bitbucket.org/bigwhite/u=$WORK/b003/_pkg_.a
packagefile errors=/root/.bin/go1.14/pkg/linux_amd64/errors.a
... ...
packagefile time=/root/.bin/go1.14/pkg/linux_amd64/time.a
packagefile unicode=/root/.bin/go1.14/pkg/linux_amd64/unicode.a
packagefile internal/race=/root/.bin/go1.14/pkg/linux_amd64/internal/race.a
EOF

mkdir -p $WORK/b001/exe/  <--- 进入链接过程
cd .
/root/.bin/go1.14/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=VcunyaIBK08ZT_O18OWQ/m8q9eZlHs_rkuXyx1ccf/5ablpczFReGfuN2bFZ7a/VcunyaIBK08ZT_O18OWQ -extld=gcc $WORK/b001/_pkg_.a
/root/.bin/go1.14/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out s
rm -r $WORK/b001/ 

我们看到 go build 执行命令的顺序大致是:

  • 创建用于构建的临时目录;
  • 下载构建 module s 依赖的 module t 和 u;
  • 分别编译 module t 和 u,编译后的结果存储到临时目录以及 GOCACHE 目录;
  • 编译 module s;
  • 定位和汇总 module s 的各个依赖包构建后的目标文件 (.a 文件) 的位置,形成 importcfg.link 文件,供后续链接器使用;
  • 链接成可执行文件;
  • 清理临时构建环境。

从上面 build -x -v 的输出,我们还看到 go build 过程主要调用了 go tool compile (GOROOT/pkg/tool/linux_amd64/compile) 和 go tool link (GOROO**T/pkg/too**l/linux_amd64/compile)和gotoollin**k(GOROOT/pkg/tool/linux_amd64/link) 分别进行包编译和最终的链接操作。编译以及链接命令中的每一个标志选项都会对最终结果产生影响,比如:-goversion 的值就会影响 go 编译器的行为,而这个 goversion 选项的值可能来自 go.mod 中的 go 版本指示标记,笔者就遇到过一次因 goversion 值版本过低而导致的问题。而 - v -x 选项对这类问题的解决是会起到关键作用的。

2) -a:强制重新构建所有包

go build 提供给 gopher 们完全干净地重新构建他们的 module / 包的能力,这就是 -a 标志位选项。当我们传入 -a 选项后,go build 就会毫无顾忌地忽略掉所有缓存机制、忽略掉已经安装到 $GOPATH/pkg 下面的依赖包库文件 (.a),并且 “六亲不认” 地从目标包 /module 依赖的标准库包的每个 Go 源文件开始重新构建,并将构建出的结果放入临时构建环境中用作后续链接器的输入,当然目标包 /module 依赖自身以及其依赖的第三方包也无法逃脱被重新构建的 “命运”。这样的构建的一个直观后果就是构建过程十分缓慢 (相比于标准构建)。

go build -a 的工作原理也使得它在传统的 gopath mode 下会更有意义,因为它可以绕过缓存和已经安装到 $GOPATH/pkg 下面的依赖包库文件 (.a),直接将各个依赖包在本地的最新变化反映到重新构建出的成果中。

而在 module-aware mode 下,依赖 module / 包的源码都是由 go 自动放置在特定路径下的,gopher 几乎不会去改动这些 go 工具下载的源码包。并且由于 go module 是支持可重复构建的,致使依赖库代码是区分了版本的,要修改依赖包代码也要先去定位版本,增加了 gopher 修改这些依赖包代码的难度。于是,在 module-aware mode 下 go build -a 和 go build 构建出的结果一般都是一致的,这也导致了在该模式下 go build -a 很少再被 gopher 们使用了。

3) -race: 让并发 bug 无处遁形

-race 命令行选项会在构建的结果中加入竞态检测的代码,这些代码在程序运行过程中如果发现对数据的并发竞态访问会给出警告,这些警告信息可以用来辅助后续查找和解决竞态问题。不过由于插入竞态检测的代码这个动作,带有 - race 的构建过程会比标准构建略慢一些。

以下面这个对原生 map 进行并发写的代码为例:

// sources/go-tools/build_with_race_option.go
... ...
func main() {
	var wg sync.WaitGroup
	var m = make(map[int]int, 100)

	for i := 0; i < 100; i++ {
		m[i] = i
	}

	wg.Add(10)
	for i := 0; i < 10; i++ {
		// 并发写
		go func(i int) {
			for n := 0; n < 100; n++ {
				n := rand.Intn(100)
				m[n] = n
			}
			wg.Done()
		}(i)
	}
	wg.Wait()
} 

我们使用带有 - race 的 go build 构建该源文件并执行构建后的结果:

$ go build -race build_with_race_option.go
$ ./build_with_race_option

==================
WARNING: DATA RACE
Write at 0x00c000118030 by goroutine 7:
  runtime.mapassign_fast64()
      /Users/tonybai/.bin/go1.14/src/runtime/map_fast64.go:92 +0x0
  main.main.func1()
      ./sources/go-tools/build_with_race_option.go:22 +0x85

Previous write at 0x00c000118030 by goroutine 6:
  runtime.mapassign_fast64()
      /Users/tonybai/.bin/go1.14/src/runtime/map_fast64.go:92 +0x0
  main.main.func1()
      ./sources/go-tools/build_with_race_option.go:22 +0x85

Goroutine 7 (running) created at:
  main.main()
      ./sources/go-tools/build_with_race_option.go:19 +0x13f

Goroutine 6 (finished) created at:
  main.main()
      ./sources/go-tools/build_with_race_option.go:19 +0x13f
==================
==================
WARNING: DATA RACE
Write at 0x00c00011c960 by goroutine 7:
  main.main.func1()
      ./sources/go-tools/build_with_race_option.go:22 +0x9a
... ...
==================
fatal error: concurrent map writes
... ... 

我们看到这个程序最终因 panic 而崩溃,但在 panic 之前,-race 插入的竞态检测代码给出了两个警告,并且在警告信息中,我们看到警告位置是在对原生 map 的写动作上。当然最终的 panic 信息也印证了并发写原生 map 是 “罪魁祸首”。如果我们采用标准 go build 构建该源码,程序运行依然会 panic,但不会有上面的警告信息。

Go 社区的一个最佳实践就是在正式发布到生产环境之前的调试、测试环节使用带有 - race 构建选项构建出的程序,这样便于在正式发布到生产环境之前尽可能多地发现程序中潜在的并发竞态问题并快速解决掉。

4) -gcflags:传给编译器 (compiler) 的标志位选项集合

我们知道 go build 实质上是通过调用 go 自带的 compile 工具 (以 linux 为例,该工具对应的是 $GOROOT/pkg/tool/linux_amd64/compile) 对 Go 代码进行编译的。go build 可以经由 -gcflags 向 compile 工具传递编译所需的命令行标志位选项集合。

go build 采用下面的模式将标志位选项列表传递给 Go 编译器:

go build -gcflags[=标志位应用的包范围]='空格分隔的标志位选项列表' 

其中 “标志位应用的包范围” 是一个可选项,如果不显式填写,那么 Go 编译器仅将通过 gcflags 传递的编译选项应用在当前包;如果显式指定了包范围,则通过 gcflags 传递的编译选项不仅会应用在当前包的编译上,还会应用于包范围指定的包上:

go build -gcflags='-N -l'  // 仅将传递的编译选项应用于当前包
go build -gcflags=all='-N -l'  // 将传递的编译选项应用于当前包及其所有依赖包
go build -gcflags=std='-N -l'  // 仅将传递的编译选项应用于标准库包 

这些命令行标志选项是传递给 Go 编译器的,所以我们可以通过下面命令查看可以传递的所有选项集合:

$go tool compile -help 

下面介绍一些常用的编译器命令行表示选项:

  • -l:关闭内联
  • -N:关闭代码优化
  • -m:输出代码优化决策,主要是逃逸分析 (决定哪些对象在栈上分配,哪些对象在堆上分配) 的过程
  • -S:输出汇编代码

在运行调试器 (debugger) 对程序进行调试之前,我们通常使用 “-N -l” 两个选项关闭对代码的内联和优化,这样能得到更多调试信息。

一些选项还具有级别属性,即支持设定选项的作用级别或输出信息内容的详尽级别。以 -m 为例,我们可以通过下面命令输出更为详尽的逃逸分析过程信息:

go build -gcflags='-m' 
go build -gcflags='-m -m' // 输出比上一条命令更为详尽的逃逸分析过程信息
go build -gcflags='-m=2' // 与上一条命令等价
go build -gcflags='-m -m -m' // 输出最为详尽的逃逸分析过程信息
go build -gcflags='-m=3' // 与上一条命令等价 

5) -ldflags:传给链接器 (linker) 的标志位选项集合

go build 在支持为编译器传递标志位选项集合的同时,也支持通过 -ldflags 为链接器 (以 linux 为例,该工具对应的是 $GOROOT/pkg/tool/linux_amd64/link) 传递链接选项集合。我们同样可以通过下面命令查看链接器支持的所有链接选项:

$go tool link -help 

链接器支持的选项有很多,这里不能一一详细说明,下面则是三个笔者日常开发中常用的链接器选项,这里简要说明一下。

  • -X:设定包中 string 类型变量的值 (注意:仅支持 string 类型变量)

通过 - X 选项,我们可以在编译链接期间 “动态” 地为程序中的字符串变量进行赋值,这个选项的一个典型应用就是在构建脚本中设定程序的版本值。我们通常会为应用程序添加 version 命令行标志选项,用来输出当前程序的版本信息,就像 go 自身那样:

$go version
go version go1.14 darwin/amd64 

如果将版本信息写死到程序代码中,显然不够灵活,耦合太紧。而将版本信息在程序构建时 “注入” 进去则是一个不错的方案。-X 选项就可以用来实现这个方案:

// sources/go-tools/linker_x_flag.go
package main

import (
	"fmt"
	"os"
)

var (
	version string
)

func main() {
	if os.Args[1] == "version" {
		fmt.Println("version:", version)
		return
	}
} 

注意:在这个源文件中,我们并未显式初始化 version 这个变量。接下来,我们构建这个程序,在构建时我们 “动态” 为 version 这个 string 类型变量注入新值:

$go build -ldflags "-X main.version=v0.7.0" linker_x_flag.go
$./linker_x_flag version                                    
version: v0.7.0 

我们看到:在 - X 后面的式子中我们使用包导入路径.变量名=新值的形式为 main 包中的 version 变量赋予了新的值。

  • -s:不生成符号表 (symbol table)
  • -w:不生成 DWARF (Debugging With Attributed RecordFormats) 调试信息

默认情况下,go build 构建出的可执行二进制文件中都是包含符号表和 DWARF 格式的调试信息的,这虽然让最终二进制文件的体积增加了,但是符号表和调试信息对于生产环境下程序异常时的现场保存和在线调试都有着重要意义。但如果你不在意这些信息或者对应用的大小十分敏感,那么可以通过 - s 和 - w 选项将符号表和调试信息从最终的二进制文件中剔除。我们还以上面的 linker_x_flag 为例,我们通过下面命令构建再来构建一次:

$go build -ldflags "-X main.version=v0.7.0 -s -w" -o linker_x_flag_without_symboltable_and_dwarf linker_x_flag.go 

然后我们对比一下两次生成的二进制文件的大小 (macos, go1.14):

-rwxr-xr-x  1 tonybai  staff  2169976 10 25 08:17 linker_x_flag*
-rwxr-xr-x  1 tonybai  staff  1674360 10 25 08:34 linker_x_flag_without_symboltable_and_dwarf* 

我们看到去除了符号表和调试信息的可执行文件在大小上要比默认构建的小 20% 左右。

4. 格式化与静态代码检查 (lint)

1) 人人都爱的 gofmt

在第 6 条中,我们提出 “提交前请使用 gofmt 格式化源码”。 gofmt 是至今 (到 Go 1.14 版本) 唯一一个仍然与 Go 发行版本绑定在一起发布的 Go 官方工具,可见其地位与重要性。也正是因为 gofmt 的牢固地位以及 Go 社区对唯一的 Go 标准代码风格的认同,导致第三方的 Go 代码格式化工具凤毛麟角。

goimports 是另外一个被广泛使用的 Go 格式化工具,之所以被广泛使用,是因为它是前 Go 核心团队的 Brad Fitzpatrick 实现的格式化工具,和 gofmt 相比,它可以自动更新源文件中的 import 区域:添加代码中引用的包的包导入路径或删除代码中没有使用的包的导入路径。goimports 的其余格式化功能与 gofmt 并无二致,甚至连命令行选项都完全一样。目前 goimports 也被 “招安” 到 go 官方扩展工具仓库的下面了:golang.org/x/tools/cmd/goimports

2) 提交代码前请使用 go vet 对代码进行静态检查

如果编译器能够发现代码中所有潜在的问题,那么我们就不会使用静态代码检查工具了。事实上,编译器和静态代码检查工具的关注点不同。就拿下面这段代码来说:

// sources/go-tools/vet_printf.go
package main

import "fmt"

func main() {
	fmt.Printf("%s, %d", "hello")
} 

Go 编译器关注点在代码的语法正确性,以有问题的 fmt.Printf 这一行代码来说,编译器关注的是是否能在导入路径中找到 fmt 包、fmt 包中是否有 Printf 函数、Printf 函数传入的实参与其函数原型中的参数是否匹配等。一旦满足了编译器的要求,编译器就不会有任何 “抱怨”。而静态代码检查工具 (如 go vet) 则按设定好的规则对代码进行扫描,在 “语义” 层面尝试发现潜在问题:

$go build vet_printf.go
$./vet_printf 
hello, %!d(MISSING)

$go vet -c 1 vet_printf.go
# command-line-arguments
./vet_printf.go:6:2: Printf format %d reads arg #2, but call has 1 arg
5	func main() {
6		fmt.Printf("%s, %d", "hello")
7	} 

我们看到:在语法层面上述代码通过了编译器的检查。但在语义层面,静态代码检测器 go vet 发现传给 Printf 函数的参数有个数不匹配的问题。

go vet 是官方 Go 工具链提供的静态代码检查工具,它内置了多条静态代码检查规则,这里挑几个规则做简要介绍:

  • assign 规则:检查代码中是否有无用的赋值操作
// sources/go-tools/vet_assign.go
func main() {
	x := 1
	x = x // vet报错:self-assignment of x to x(x自赋值给x)
	println(x)
} 
  • atomic 规则:检查代码中是否有对 sync.atomic 包中函数的误用情况
// sources/go-tools/vet_atomic.go
func main() {
	var x uint64 = 17
	x = atomic.AddUint64(&x, 1) // vet报错:direct assignment to atomic value(直接赋值给一个原子值)
	//atomic.AddUint64(&x, 1) // ok
	println(x)
} 
  • bools 规则:检查代码中是否存在对布尔操作符的误用情况
// sources/go-tools/vet_bools.go

func main() {
	var x, y int
	if x == y || x == y { // vet报错:redundant or: x == y || x == y(冗余的或操作)
		println("ok")
	}
} 
  • buildtag 规则:检查源文件中 +build tag 是否正确定义
// sources/go-tools/vet_buildtag.go

// Copyright 2020 tonybai.com

// +building   // vet报错:possible malformed +build comment(+build可能存在格式错误)
// +build !ignore 

package foo

// +build pro // vet报错:+build comment must appear before package clause and be followed by a blank line(+build必须出现在package语句上方并用空行间隔)

var i = 5 
  • composites 规则:检查源文件中是否有未使用 “field:value” 格式的复合字面值形式对 struct 类型变量进行值构造的问题
// sources/go-tools/vet_composites.go
type myFoo struct {
	name string
	age  int
}

func main() {
	f := &myFoo{"tony", 20} // ok
	err := &net.AddrError{"not found", "localhost"} // vet报错:net.AddrError composite literal uses unkeyed fields(net.AddrError未使用"field:value"格式的复合字面值)
	fmt.Printf("%#v,%#v\n", *f, err)
} 

我们看到 go vet 仅针对导入包的结构体类型的实例构造 / 赋值进行检查,对于源文件中自定义的结构体类型不作检查。

  • copylocks 规则:检查源文件中是否存在 lock 类型变量的按值传递问题

sync 包中大多数类型在首次使用后就不能被复制或按值传递,该规则就是用来检查代码是否满足该约束的。

// sources/go-tools/vet_copylocks.go

func foo(mu sync.Mutex) { // vet报错:foo passes lock by value: sync.Mutex(foo函数通过值传递传入sync.Mutex类型)
	mu.Lock()
	mu.Unlock()
}

func main() {
	var mu sync.Mutex
	foo(mu) // vet报错:call of foo copies lock value: sync.Mutex(foo函数拷贝了sync.Mutex类型变量的值)
	mu1 := mu // vet报错:assignment copies lock value to mu1: sync.Mutex(将sync.Mutex类型变量值拷贝给mu1了)
	mu1.Lock()
	mu1.Unlock()

	pMu := &mu
	pMu1 := &mu1
	*pMu1 = *pMu // vet报错:assignment copies lock value to *pMu1: sync.Mutex(将sync.Mutex类型变量值拷贝给*pMu1了)
} 
  • loopclosure 规则:检查源文件中是否存在循环内的匿名函数引用循环变量的问题
// sources/go-tools/vet_loopclosure.go

func main() {
	var s = []int{11, 12, 13, 14}
	for i, v := range s {
		go func() {
			println(i) // vet报错:loop variable i captured by func literal(匿名函数引用了循环变量i)
			println(v) // vet报错:loop variable v captured by func literal(匿名函数引用了循环变量v)
		}()
	}
	time.Sleep(5 * time.Second)
} 
  • unmarshal 规则: 检查源码中是否有非指针或非接口类型值传给 unmarshal 的问题
// sources/go-tools/vet_unmarshal.go

type Foo struct {
	Name string
	Age  int
}

func main() {
	var v Foo
	json.Unmarshal([]byte{}, v) // vet报错:call of Unmarshal passes non-pointer as second argument(Unmarshal的第二个参数传入的不是指针值)
} 
  • unsafeptr 规则:检查源码中是否有非法将 uintptr 转换为 unsafe.Pointer 的问题
// sources/go-tools/vet_unsafeptr.go

func main() {
	var x unsafe.Pointer
	var y uintptr
	x = unsafe.Pointer(y) // vet报错:possible misuse of unsafe.Pointer(可能误用unsafe.Pointer)
	_ = x
} 

更多检查规则,可以通过 go tool vet help 查看。默认情况下,go vet 内置的所有检查规则均开启。我们可以手工关闭其中一个或几个规则:

$go vet -printf=false -buildtag=false vet_printf.go 

我们看到关闭 printf 规则检测后,go vet 针对 vet_printf.go 并未报出任何错误。如果显式开启某些检查,则其他检查规则将不生效,比如:

$go vet -buildtag=true vet_printf.go 

上面这行命令表示仅对 vet_printf.go 做 buildtag 规则检查,go vet 自然也不会报错。

go vet 的检查规则不是固定不变的,随着 Go 版本的演化,Go vet 的静态检查规则会逐渐丰富和强大。但也正是由于 go vet 是原生工具链一部分,添加何种检查规则需要提高到 Go 核心团队层面去讨论,灵活性和敏捷性较差;另外 go vet 的静态代码检查规则的更新的确较慢,因此它要与 Go 版本同步更新,而 Go 版本的发布周期为半年一次。

3) 第三方 linters 聚合:golangci-lint

第三方的 lint 工具是官方 go vet 工具的一个很好的补充。第三方 lint 工具中有通用类型的 lint 工具 (比如:staticcheck),但更多的是聚焦于某特定主题的 lint 工具 (比如:用于检查未用代码的 deadcode、用于检查未处理错误的 errcheck 等),但没有一种第三方 lint 工具可以囊括所有的静态检查规则,于是 linter 聚合工具出现了!通过这类聚合工具可以对代码进行尽可能详尽地检查。

早期 Go 社区有一款比较活跃的第三方 linter 聚合工具:gometalinter,但目前 gometalinter 已经停止演进,取而代之的是目前比较活跃的 golangci-lint

golangci-lint 聚合了几十种 go lint 工具,但默认情况仅开启如下几种:

  • deadcode:查找代码中的未用代码;
  • errcheck:检查代码中是否存在未处理的错误;
  • gosimple:专注于发现可以进一步简化的代码的 lint 工具;
  • govet:go 官方工具链中的 vet 工具;
  • ineffassign:检查源码中是否存在无效赋值的情况 (赋值了,但没有使用);
  • staticcheck:通用型 lint 工具,增强的 “go vet”,对代码进行很多 go vet 尚未进行的静态检查;
  • structcheck:查找未使用的结构体字段;
  • typecheck:像 go 编译器前端那样去解析 Go 代码并做类型检查;
  • unused:检查源码中是否存在未使用的常量、变量、函数和类型;
  • varcheck:检查源码中是否存在未使用的全局变量和常量;

除了上述这些默认开启的 lint 工具之外,我们可以通过 golangci-lint linters 命令查看所有内置 lint 工具列表,包括默认不开启的。

我们也可以像 go vet 指定静态检查规则那样开启和关闭某些 linter 工具,通过 golangci-lint linters 可以查看显式指定 lint 工具或关闭 lint 工具后的 lint 工具状态列表:

$golangci-lint linters --disable-all -E staticcheck // 仅开启staticcheck这一个lint工具
$golangci-lint linters -E bodyclose,dupl // 在默认开启的lint工具集合的基础上,再额外开启bodyclose和dupl这两个lint工具
$golangci-lint linters -D unused,varcheck // 关闭unused和varcheck这两个lint工具的检查 

明确了究竟开启哪些 lint 工具之后,我们就可以对 Go 源码进行静态检查了:

// 仅对当前目录下的vet_assign.go源文件进行staticcheck检查
$golangci-lint run --disable-all -E staticcheck vet_assign.go 
vet_assign.go:5:2: SA4018: self-assignment of x to x (staticcheck)
	x = x
	^

// 对当前路径下的Go包进行默认lint工具集合的检查
$golangci-lint run 

// 对当前路径以及子路径(递归)下的所有Go包进行默认开启的lint工具集合检查以及额外的dupl工具检查
$golangci-lint run -E dupl ./... 

5. 查看文档

查看文档是开发人员日常必不可少的开发活动之一。Go 语言从诞生那天起就十分重视项目文档的建设,除了在官方网站可以查看到最新稳定发布版的文档之外,在 tip.golang.org 上还可以查看到项目主线分支上最新开发版本的文档。同时 Go 还将整个 Go 项目文档加入到 Go 发行版中,这样开发人员在本地安装 Go 的同时也拥有了一份完整的 Go 项目文档。

除了在发行版中集成所有文档,从 1.5 版本开始,Go 还将文档查看工具集成到其工具链当中 (go doc),使之成为 Go 工具链不可分割的一部分,这也再次体现了文档在 Go 语言中的重要性。

1) go doc

自 go doc 在 1.5 版本加入 Go 工具链之后,它就和 go getgo build 一样成为了 Gopher 们每日必用的 go 命令,也成为了 Go 包文档的 “百科全书”

在查看包文档时,go doc 在命令行上接受的参数使用了 Go 语法的格式,这使得 go doc 的上手使用几乎是 “零门槛”:

go doc <pkg>
go doc <sym>[.<methodOrField>]
go doc [<pkg>.]<sym>[.<methodOrField>]
go doc [<pkg>.][<sym>.]<methodOrField> 

下面我们就来简要介绍一下如何使用 go doc 查看各类包文档。

  • 查看标准库文档

我们可以在任意路径下执行 go doc 命令查看标准库文档,下面是一些查看标准库不同元素文档的命令示例。

查看标准库 net/http 包文档:

$go doc net/http
或
$go doc http 

查看 http 包的 Get 函数的文档:

$ go doc net/http.Get
或
$ go doc http.Get 

查看 http 包中结构体类型 Requset 中字段 Form 的文档:

$go doc net/http.Request.Form
或
$go doc http.Request.Form 
  • 查看当前项目文档

除了查看标准库文档,我们在从事项目开发时很可能会查看当前项目中其他包的文档以决定如何使用这些包。go doc 也可以很方便地查看当前路径下项目的文档,我们还以已经下载到本地 (比如:~/temp/gocmpp) 的 github.com/bigwhite/gocmpp 项目为例。

查看当前路径下的包的文档:

$go doc 

package cmpp // import "github.com/bigwhite/gocmpp"

const CmppActiveTestReqPktLen uint32 = 12 ...
const CmppConnReqPktLen uint32 = 4 + 4 + 4 + 6 + 16 + 1 + 4 ...
const Cmpp2DeliverReqPktMaxLen uint32 = 12 + 233 ...
... ... 

查看当前路径下包的导出元素的文档:

$go doc CmppActiveTestReqPktLen
package cmpp // import "."

const (
	CmppActiveTestReqPktLen uint32 = 12     //12d, 0xc
	CmppActiveTestRspPktLen uint32 = 12 + 1 //13d, 0xd
)
    Packet length const for cmpp active test request and response packets. 

我们看到包导出元素的头字母是大写的,go doc 不会将其解析为包名,而会认为它是当前包中的某个元素。

通过 - u 选项,我们也可以查看当前路径下包的非导出元素的文档:

$go doc -u newPacketWriter
package cmpp // import "github.com/bigwhite/gocmpp"

func newPacketWriter(initSize uint32) *packetWriter 

查看当前路径的子路径下的包的文档:

$go doc ./utils
或
$go doc utils

package cmpputils // import "github.com/bigwhite/gocmpp/utils"

var ErrInvalidUtf8Rune = errors.New("Not Invalid Utf8 runes")
func GB18030ToUtf8(in string) (string, error)
... ... 
  • 查看第三方项目文档

在 go module 开启的情况下,go doc 会首先确定包所在 module,定位该 module 根路径。由于 module 可能存在于任意路径下,因此我们在 module-aware 模式下要查看第三方项目文档只能先切换到该第三方项目的 module 根路径下再使用查看当前路径下包的方法查看该项目的相关包文档。

在 gopath 模式下,go doc 则会自动到 $GOPATH 下面查找对应的包路径,如果该包存在,就可以输出该包的相关文档。因此我们可以在任意路径下通过 go doc 查看第三方项目包的文档:

$export GO111MODULE=off
$go doc github.com/bigwhite/gocmpp.CmppActiveTestReqPktLen
package cmpp // import "github.com/bigwhite/gocmpp"

const (
	CmppActiveTestReqPktLen uint32 = 12     //12d, 0xc
	CmppActiveTestRspPktLen uint32 = 12 + 1 //13d, 0xd
)
    Packet length const for cmpp active test request and response packets. 
  • 查看源码

如果要查看包的源码,我们没有必要将目录切换到该包所在路径并通过编辑器打开源文件查看,通过 go doc 我们一样可以查看包的完整源码或包的某元素的源码。

查看标准库包源码:

$go doc -src fmt.Printf
package fmt // import "fmt"

// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
} 

查看当前路径包中导出元素的源码:

$go doc -src NewClient
package cmpp // import "."

// New establishes a new cmpp client.
func NewClient(typ Type) *Client {
	return &Client{
		typ: typ,
	}
} 

查看当前路径包中未导出元素的源码:

$go doc -u -src newPacketWriter
package cmpp // import "github.com/bigwhite/gocmpp"

func newPacketWriter(initSize uint32) *packetWriter {
	buf := make([]byte, 0, initSize)
	return &packetWriter{
		wb: bytes.NewBuffer(buf),
	}
} 

2) godoc:Web 化的文档中心

很多接触 Go 语言较早的 gopher 都知道,在 go doc 之前,还有一个像 gofmt 一样随着 Go 安装包一起发布的文档查看工具,它就是 godoc,也就是说 godoc 在 Go 世界的存在历史比 go doc 还要悠久。在 Go 1.5 版本增加 go doc 工具后,godoc 与 go doc 就一直并存在 Go 中,这也或多或少会给一些 Go 初学者带去困惑:go doc 与 godoc 究竟该用哪一个,二者有啥差别。这种情况一直持续到 Go 1.13 版。在 Go 1.13 版本中,godoc 就不再和 go、gofmt 一起内置在 Go 安装包中发布了。godoc 被挪到 Go 扩展工具链中,我们可以通过下面命令单独安装 godoc:

$go get golang.org/x/tools/cmd/godoc 
  • 建立 web 形式的文档中心

和命令行 go doc 工具不同的是,godoc 实质上是一个 web 服务,它会在本地建立起一个 web 形式的 Go 文档中心,当我们执行下面命令时这个文档中心服务就启动了:

$godoc -http=localhost:6060 

在浏览器地址栏输入 http://localhost:6060,打开 Go 文档中心首页:

44 利其器!Go常用工具大检阅

图 10-4-2:Go 文档中心首页

我们看到 godoc 将 $GOROOT 下面的内容以 web 页面的形似呈现给开发者,同时我们看到首页顶部的菜单与 Go 官方主页的菜单也基本如出一辙。点击 “Packages” 可以打开 Go 包参考文档页面: 44 利其器!Go常用工具大检阅

图 10-4-3:Go 包参考文档页面

Go 包参考文档页面将包分为几类:标准库包 (Standard library)、第三方包 (Third party) 和其它包 (Other packages),其中的第三方包就是本地 $GOPATH 下面的各个包。

  • 查看 Go 旧版本的文档

godoc 建立的 Go 文档中心对应的文档版本默认是当前 Go 的版本,即如果当前本地安装的 Go 版本为 go 1.14,那么 godoc 所呈现的就是 go 1.14 稳定版对应的文档。而 Go 官方网站上的文档版本对应的是最新 Go 稳定版的版本,而 tip.golang.org 上的文档版本则是当前 Go 项目主线分支 (master) 上的文档版本。

那么如果我想查看一下旧版本的文档,比如 Go 1.9 版本的文档,我该如何做呢?首先我们需要下载 go 1.9 版本的安装包,将其解压到本地目录下,比如是 /Users/tonybai/.bin/go1.9,接下来我们执行如下命令:

$godoc -goroot /Users/tonybai/.bin/go1.9 -http=localhost:6060 

我们用 -goroot 命令行选项显式告诉 godoc 从哪个路径加载 Go 文档数据,这样 godoc 建立的文档中心中的文档版本就是 Go 1.9 的了。

  • 查看 blog

Go 官方博客是学习和理解 go 语言的重要资料,Go 团队为了方便 gopher 查找阅读博客资料,单独在 Go 扩展工具链项目中为 blog 创建了一个内容服务程序:blog,我们可以通过下面命令安装该工具:

$go get golang.org/x/blog

官博的内容数据并没有被包含在 Go 安装包中,而是单独存放在 github.com/golang/blog 仓库下,我们需要将该仓库下载到本地后 (比如:/Users/tonybai/.bin/goblog/blog),再切换到该路径下启动 blog 服务程序:

$cd /Users/tonybai/.bin/goblog
$git clone https://github.com/golang/blog.git
$cd blog
$blog
2020/10/31 05:14:47 Listening on addr localhost:8080 

我们用浏览器打开 localhost:8080 页面,我们能看到与官方 blog 首页相同的页面:

44 利其器!Go常用工具大检阅

图 10-4-4:本地 Go 博客页面

之后点击 “Blog index” 就能看到 Go 历史上每个时期发表的博客了。

6. 代码导航与洞察

还有一类工具对 gopher 们的日常开发效率有着较大影响,这就是代码导航和洞察工具。这类工具通常对开发人员是 “透明的”,因为它总是与 IDE / 编辑器插件绑定使用。当我们敲入代码、光标移动、鼠标悬停或某些快捷键操作时,这类工具将被驱动运作起来,以帮助 IDE / 编辑器实现自动代码补全、悬停提示、跳转定义、调用查找、代码语法问题诊断等功能特性。以往 Go 开发所常用的 IDE / 编辑器,诸如 vim、vscode、emacs 等都会聚合一堆这类工具来实现上述功能特性,常见的工具包括:gocode、gorename、godefgopkgsgo-symbolsgo-outlineguru 以及 staticcheck 等。

2019 年中旬 Go 官方启动了 Go 语言服务器实现项目: gopls,旨在替代上面那些由不同个体开发人员维护的工具,为 Go 编辑器 / IDE 提供高质量、高性能的 语言服务器标准协议的 Go 实现。

语言服务器协议(Language Server Protocol,LSP) 由微软创建,包括 Codenvy、Red Hat 和 Sourcegraph 在内的多家公司已经联合起来共同支持该协议的演进和推广发展,并且该协议已经得到了来自各种编程语言社区的支持,主流编程语言几乎都有自己的 LSP 实现

语言服务器协议旨在使语言服务器和开发工具之间的通信协议标准化。这样,单个语言服务器就可以在多个开发工具中重复使用,从而避免以往必须为每个开发工具都单独进行一次诸如自动代码补全、定义跳转、悬停提示等功能特性的开发,大幅节省了 IDE / 编辑器 (及插件) 作者的精力。

gopls 目前还在活跃开发阶段,但目前它已经覆盖实现了语言服务器协议定义的所有功能。虽然 gopls 尚未发布 1.0 版本,但目前主流的编辑器 (vscode、vim 等) 都已经支持了 gopls,因此这里也强烈建议各位 gopher 积极转向并使用 gopls 进行日常 Go 开发工作。

7. 小结

本节介绍了 Gopher 在各个开发阶段需要掌握的 Go 常用工具,它们既有 Go 团队维护的 Go 原生工具链中的工具,也有成熟稳定的第三方工具。在 Go 开发过程中熟练运用这些工具,不仅体现出作为一个 gopher 的专业性,还将为你插上 “效率” 的翅膀。