



自从现代编程语言出现以来,针对每种编程语言的代码风格的争论就不曾停止过,直到Go语言的出现,人们才惊奇地发现Go社区似乎很少有针对Go语言代码风格的争论。
gofmt的代码风格不是某个人的最爱,而是所有人的最爱。
——Rob Pike
Go语言设计的目标之一是解决大型软件系统的大规模协作开发问题,也就是说Go语言不仅要让独立开发人员使用起来感觉良好,还要将这个良好的体验扩展到拥有一定规模的团队甚至是大型开发团队。Go核心团队将这类问题归结为一个词——规模化(scale),这也是近几年比较火热的Go2演进方案将主要解决的问题 [1] 。
gofmt是伴随着Go语言的诞生而在“规模化”这个目标上进行的第一批实践和尝试。它试图“消灭”软件开发过程中阻碍“规模化”的问题,即开发人员在编程语言代码风格上的无休止且始终无法达成一致的争论,以及不同代码风格给开发人员在阅读和维护他人代码时带来的低效。gofmt先入为主地将一种统一的代码风格内置到Go语言之中,并将其与Go语言一起以一种“标准”的形式推广给所有Go开发者。
在Go语言诞生和推广的初期,也许会有开发人员因gofmt所格式化出来的统一代码风格与自己喜好的风格不一致而抱怨,但随着Go影响力的扩大以及采用gofmt标准代码风格的代码的累积,Go开发者们渐渐注意到关于其他编程语言的那种针对代码风格的“争吵”变少了甚至消失了。在一致的代码风格下,Go开发人员阅读和维护他人代码时不再感到陌生,效率也变得更高了,gofmt的代码风格成为了所有人的最爱,以至于在Go的世界里代码风格已经没有了存在感。
gofmt代码风格已经成为Go开发者的共识,融入Go语言的开发文化当中,以至于多数Go开发者可能说不出gofmt代码风格是什么样的,因为代码会被gofmt自动变成那种风格,大家已经不再关心风格。gofmt是Go语言在解决规模化问题上的一个最佳实践,并成为Go语言吸引其他语言开发者的一大亮点。很多主流语言在效仿Go语言而推出自己的格式化工具,比如Java formatter、Clang formatter、Dartfmt等。作为Go开发人员, 请在提交代码前使用gofmt进行格式化。
[1] 见 https://blog.golang.org/toward-go2 。
截至Go 1.16稳定版,gofmt工具一直是放置在Go安装包中与Go编译器工具一并发布的,这足以说明gofmt工具的重要程度。
gofmt保持了Go语言“简单”的设计哲学,这点通过其帮助手册即可看出:
$ gofmt -help
usage: gofmt [flags] [path ...]
-cpuprofile string
write cpu profile to this file
-d display diffs instead of rewriting files
-e report all errors (not just the first 10 on different lines)
-l list files whose formatting differs from gofmt's
-r string
rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
-s simplify code
-w write result to (source) file instead of stdout
gofmt最大的特点是 没有提供任何关于代码风格设置的命令行选项和参数 ,这样Go开发人员就无法通过设置命令行特定选项来定制自己喜好的风格。不过gofmt却提供了足够在工程上对代码进行按格式查找、代码重构的命令行选项。我们来看一些gofmt的实用技巧。
虽然Go语言推崇一件事情仅有一种方式完成,但难免存在一些事情依然有多种表达方法,比如下面这个例子。
存在一个字符串切片v:
v := []string{...}
如果要迭代访问字符串切片v的各个元素,可以这么做:
for _ = range v {
...
}
在Go 1.4及后续版本中,还可以这么做:
for range v {
...
}
Go开发者更推崇后面那种简化后的写法。这样的例子在Go语言的演进过程中还存在一些。为了避免将代码转换为简化语法给开发人员带来额外工作量,Go官方在gofmt中提供了-s选项。通过gofmt -s可以将遗留代码中的非简化代码自动转换为简化写法,并且没有副作用,因此一般“-s”选项都会是gofmt执行的默认选项。
代码重构是软件工程过程中的日常操作,Go语言曾经为了支持大规模软件的全局重构加入了类型别名(type alias)语法。gofmt除了具有格式化代码的功能外,对代码重构也具有一定的支撑能力。我们可以通过-r命令行选项对代码进行表达式级别的替换,以达到重构的目的。
下面是-r选项的用法:
gofmt -r 'pattern -> replacement' [other flags] [path ...]
gofmt -r的原理就是在对源码进行重新格式化之前,搜索源码是否有可以匹配pattern的表达式,如果有,就将所有匹配到的结果替换为replacement表达式。gofmt要求pattern和replacement都是合法的Go表达式。比如:
$gofmt -r 'a[3:len(a)] -> a[3:]' -w chapter2/sources/gofmt_demo.go
上面gofmt -r命令执行的意图就是先将源码文件gofmt_demo.go中能与a[3:len(a)]匹配的代码替换为a[3:],然后重新格式化。因此上面的命令对下面的源码片段都可以成功匹配:
- fmt.Println(s[3:len(s)]) + fmt.Println(s[3:]) - n, err := s.r.Read(s.buf[3:len(s.buf)]) + n, err := s.r.Read(s.buf[3:]) - reverseLabels = append(reverseLabels, domain[3:len(domain)]) + reverseLabels = append(reverseLabels, domain[3:])
注意,上述命令中的a并不是一个具体的字符,而是代表的一个 通配符 。出现在'pattern -> replacement'中的小写字母都会被视为 通配符 。我们将pattern中的3改为字母b(通配符):
$gofmt -r 'a[b:len(a)] -> a[b:]' -w chapter2/sources/gofmt_demo.go
这样pattern匹配的范围就更大了:
- fmt.Println(s[3:len(s)]) + fmt.Println(s[3:]) - n, err := s.r.Read(s.buf[s.end:len(s.buf)]) + n, err := s.r.Read(s.buf[s.end:]) - reverseLabels = append(reverseLabels, domain[3:len(domain)]) + reverseLabels = append(reverseLabels, domain[3:]) - reverseLabels = append(reverseLabels, domain[i+1:len(domain)]) + reverseLabels = append(reverseLabels, domain[i+1:])
gofmt提供了-l选项,可以按格式要求输出满足条件的文件列表。比如,输出$GOROOT/src下所有不满足gofmt格式要求的文件列表(以Go 1.12.6版本为例):
$ gofmt -l $GOROOT/src $GOROOT/src/cmd/cgo/zdefaultcc.go $GOROOT/src/cmd/go/internal/cfg/zdefaultcc.go $GOROOT/src/cmd/go/internal/cfg/zosarch.go ... $GOROOT/src/go/build/zcgo.go
我们看到,即便是Go项目自身源码也有“漏网之鱼”,不过这可能是gofmt的格式化标准有过微调,而很多源文件没有及时调整导致的。
我们也可以将-r和-l结合起来使用,输出匹配到pattern的文件列表。比如查找$GOROOT/src下能匹配到'a[b:len(a)]' pattern的文件列表:
$ gofmt -r 'a[b:len(a)] -> a[b:]' -l $GOROOT/src $GOROOT/src/bufio/scan.go $GOROOT/src/crypto/x509/verify.go
不过要注意的是, 如果某路径下有很多不符合gofmt格式的文件,这些文件也会被一并输出。
Go编译器在编译源码时会对源码文件导入的包进行检查,对于源文件中没有使用但却导入了的包或使用了但没有导入的包,Go编译器都会报错。遗憾的是,gofmt工具无法自动增加或删除文件头部的包导入列表。为此,Go核心团队的Brad Fitzpatrick实现了goimports [1] ,该工具后来被移到官方仓库golang.org/x/tools/cmd/goimports下维护了。
goimports在gofmt功能的基础上增加了对包导入列表的维护功能,可根据源码的最新变动自动从导入包列表中增删包。
安装goimports的方法很简单:
$go get golang.org/x/tools/cmd/goimports
如果Go编译器发现$GOPATH/bin路径存在,就会将goimports可执行文件放到该路径下,这时只要保证该路径在$PATH中即可。可以认为goimports是在gofmt之上又封装了一层,而且goimports提供的命令行选项和参数与gofmt也十分类似:
$ ./goimports -help
usage: goimports [flags] [path ...]
-cpuprofile string
CPU profile output
-d display diffs instead of rewriting files
-e report all errors (not just the first 10 on different lines)
-format-only
if true, don't fix imports and only format. In this mode, goimports is effectively gofmt, with the addition that imports are grouped into sections.
-l list files whose formatting differs from goimport's
...
因此,这里就不再赘述goimports的用法了。
[1] https://github.com/bradfitz/goimports
日常开发工作中,Go开发人员多使用各种主流编辑器进行代码的编写、测试和重构工作,他们一般会将gofmt/goimports与编辑器集成,由编辑器在保存源文件时自动调用gofmt/goimports完成代码的格式化,而几乎不会手动敲入gofmt命令进行代码格式化。下面是对gofmt/goimport与主流Go源码编辑器集成方法的简要说明。
(1)Visual Studio Code
Visual Studio Code(VS Code)是微软开源的IDE工具,它集成了Git,支持智能提示,提供各种方便的快捷键等,最为强大的是其插件扩展。通过插件扩展,VS Code迅速抢占了各大编程语言的IDE榜单头部位置,Go语言也不例外。
微软为Go提供了官方插件支持——vscode-go,该插件项目已经正式成为Go官方子项目并被放入Go项目仓库中托管。vscode-go借助第三方工具实现了代码智能感知、代码导航、编辑、诊断、调试和单元测试等功能。其“在文件保存时格式化”功能就是通过调用gofmt或goimports实现的。VS Code与gofmt/goimports的集成很简单,只需在安装vscode-go插件时按照提示安装vscode-go所依赖的第三方工具或者保证gofmt在环境变量PATH的路径中即可(将$GOROOT/bin加入PATH环境变量)。如果要使用goimports,可通过前面goimports的安装命令手动安装,并保证goimports所在目录在PATH环境变量的路径中。
(2)Vim
Vim是在*NIX世界普遍存在的一款历史悠久的著名文本编辑器,也是很多做后端开发工作的开发者最喜欢的编辑工具。Vim的强大之处与VS Code类似,它也有一个强大的插件扩展机制,基于Vim插件我们便可以实现想要的各种功能。
Go和Vim通过vim-go插件连接在一起。vim-go是由前DigitalOcean工程师Fatih Arslan开发的Vim插件(需要Vim 7.4.2009及以上版本),你可以通过Pathogen、vim-plug或Vundle中的任一款Vim插件管理器安装vim-go插件。以使用vim-plug为例:
先安装vim-plug和vim-go两个Vim插件:
$ curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim $ git clone https://github.com/fatih/vim-go.git ~/.vim/plugged/vim-go
编辑~/.vimrc文件,添加下面内容:
call plug#begin() Plug 'fatih/vim-go' call plug#end()
保存退出后再启动Vim,在命令模式下(在普通模式下输入“:”进入命令模式),执行GoInstallBinaries,vim-go会自动下载并安装其所依赖的第三方工具,其中就包含goimports。这些第三方工具都会被默认放置在$GOPATH/bin下。如果没有显式设置GOPATH,$HOME/go将被作为默认GOPATH。因此你要确保$GOPATH/bin在PATH环境变量中。
vim-go默认使用gofmt,在保存文件时对Go源文件进行重新格式化,不过你可以设置使用goimports,只需在.vimrc中添加下面这行配置:
let g:go_fmt_command = "goimports"
这样只要goimports可执行文件在PATH路径下,vim-go就可以使用它来格式化代码并管理文件头部的包列表了。
(3)GoLand
GoLand是知名IDE厂商JetBrains开发的Go语言IDE产品。JetBrains在IDE领域浸淫多年,积累了丰富的IDE产品经验,这让GoLand一经推出就大受Gopher欢迎。开源编辑器提供的功能在GoLand中均能找到,并且体验更佳。经过快速发展,目前GoLand已经成为市场占有率最高的商业Go语言IDE产品。
GoLand同样也是通过第三方工具(如gofmt/goimports)来实现对代码的格式化。在GoLand中,我们可以手动对文件或工程执行格式化,也可以创建File Watcher来实现在保存文件时对文件进行自动格式化。
手工格式化的调用方法(以GoLand 2019.1.3版本为例,后续版本设置方法可能有所不同)是,在GoLand主菜单中依次选择Tools→Go Tools→Go fmt file/Go fmt project/Goimports file,如图6-1所示。
图6-1 GoLand手工执行gofmt/goimports对源文件进行格式化
在保存文件时自动执行gofmt/goimports对源文件进行格式化的设置方法如下:在“Pereferences...”对话框中,依次选择Tools→File Watchers,然后添加一个File Watcher,选择go fmt模板或goimports模板即可(见图6-2)。
图6-2 配置GoLand以在保存文件时自动执行gofmt/goimports
gofmt以及其背后的设计哲学是Go语言的创新,也是对编程世界的一项重要贡献。作为Go开发人员,我们要牢记在提交源码前先用gofmt对源码进行格式化,并学会将gofmt/goimports工具集成到IDE/编辑器工具中,让这一过程自动化,使代码格式化这件事在开发过程中变得透明,不会成为我们的心智负担。