gofmt 的代码风格不是某个人的最爱,而是所有人的最爱。
Go 语言设计的目标之一就是解决大型软件系统的大规模开发的问题,也就是说 Go 语言不仅要让某个独立开发人员使用起来感觉良好,还要能将这个良好的体验扩展到某个拥有一定人员规模的团队甚至是大型的开发团队。Go 核心团队将这类问题归结为一个词:规模化(scale),这同样也是目前比较火热的 Go2 演进方案将主要解决的问题。
gofmt 是伴随着 Go 语言诞生的第一批在“规模化”这个目标上的实践和尝试。它试图“消灭”软件开发过程中的阻碍“规模化”的问题,即开发人员在编程语言代码风格上的无休止且始终无法达成一致的争论以及不同代码风格给开发人员在阅读和维护他人代码时候带来的低效。gofmt 先入为主地将一种统一的代码风格内化到 Go 语言之中,并和 Go 语言一起以一种“标准”的形式推广给所有 Go 开发者。
在 Go 语言诞生和推广的初期,也许出现过因 gofmt 所格式化出来的统一代码风格与某个开发人员自己喜好的风格不一致而导致抱怨的情况,但随着 Go 影响力的扩大以及大量采用 gofmt 标准代码风格代码的累积,Go 开发者们渐渐意识到以往在其他编程语言中的那种针对代码风格的“争吵”渐渐少了甚至是消失了。在一致的代码风格下,Go 开发人员阅读和维护他人代码时感觉不再陌生,也变得更有效率了,gofmt 的代码风格成为了所有人能接受的共同的最爱,甚至于在 Go 的世界里代码风格已经变得没有了“存在感”。
gofmt 代码风格已经成为 Go 开发者的一种共识,融入到 Go 语言的开发文化当中了,以至于你让某个 Go 开发者说出 gofmt 后的代码风格是什么样的, 多数 Go 开发者可能说不出,因为代码会被 gofmt 自动变成那种风格,大家已经不再关心风格。gofmt 是 Go 语言在解决规模化问题上的一个最佳实践,并成为了 Go 语言吸引其他语言开发者的一大卖点。很多其他主流语言也在效仿 Go 语言推出自己的 format 工具,比如:Java formatter、Clang formatter、Dartfmt 等。因此,作为 Go 开发人员,请在提交你的代码前使用 gofmt 格式化你的 Go 源码。
截至 Go 1.14 稳定版,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 却提供了满足在工程上对代码进行按格式查找、代码重构的足够的命令行选项,我们来看一些实用的用法:
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 main.go
上面 gofmt -r 命令执行的意图就是将源码文件 main.go 中能与 a[3:len(a)] 匹配的代码替换为 a[3:],然后再做重新格式化。注意:上述命令中的 a 并不是一个具体的字符,而是代表的一个通配符。出现在‘pattern -> replacement’的小写字母都会被视为通配符。因此上面的命令对下面的源码片段都可以成功匹配:
- 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:])
我们也可以将 pattern 中的 3 改为一个字母 b(通配符):
gofmt -r 'a[b:len(a)] -> a[b:]' -w xxx.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[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
/home/tonybai/.bin/go1.12.6/src/cmd/cgo/zdefaultcc.go
/home/tonybai/.bin/go1.12.6/src/cmd/go/internal/cfg/zdefaultcc.go
/home/tonybai/.bin/go1.12.6/src/cmd/go/internal/cfg/zosarch.go
/home/tonybai/.bin/go1.12.6/src/cmd/go/testdata/src/badpkg/x.go:1:1: expected 'package', found pkg
/home/tonybai/.bin/go1.12.6/src/cmd/go/testdata/src/notest/hello.go:6:1: expected declaration, found Hello
/home/tonybai/.bin/go1.12.6/src/cmd/go/testdata/src/syntaxerror/x_test.go:3:11: expected identifier
/home/tonybai/.bin/go1.12.6/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
/home/out1/.bin/go1.12.6/src/bufio/scan.go
/home/out1/.bin/go1.12.6/src/crypto/x509/verify.go
不过要注意的是:如果某路径下有很多不符合 gofmt 格式的文件,这些文件也会被一并输出。
Go 编译器在编译源码时会对源码文件 import 的 package 进行检查,对于源文件中没有使用但却导入了的 package 或使用了但没有导入的包,Go 编译器会报错。很遗憾的是 gofmt 工具无法自动增加或删除掉文件首部的 package 导入列表。为此,Go 核心团队的 Brad Fitzpatrick 实现了goimports 工具,后该工具移到官方 golang.org/x/tools/cmd/goimports 下维护。
goimports 在 gofmt 的功能的基础上,增加了对 package 列表的维护功能,可根据源码的最新变动自动从导入包列表中增删包。
安装 goimports 的方法很简单:
$go get golang.org/x/tools/cmd/goimports
如果 go 编译器发现GOPATH/bin 路径存在,就会将 goimports 可执行文件放入该路径下,这时只要保证该路径在GOPAT**H/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
-local string
put imports beginning with this string after 3rd-party packages; comma-separated list
-memprofile string
memory profile output
-memrate int
if > 0, sets runtime.MemProfileRate
-srcdir dir
choose imports as if source code is from dir. When operating on a single file, dir may instead be the complete file name.
-trace string
trace profile output
-v verbose logging
-w write result to (source) file instead of stdout
因此,这里就不再单独对 goimports 进行用法上的赘述了。
日常开发工作中,Go 开发人员多使用各种主流编辑器进行代码的编写、测试和重构工作,对代码的格式化一般是通过将 gofmt/goimports 与编辑器集成后在源文件保存时由编辑器自动调用 gofmt/goimports 完成的,开发人员几乎没有手工敲入 gofmt 命令对源码进行格式化的。下面是将 gofmt/goimport 与主流 Go 源码编辑器集成方法的简要说明。
- Visual Studio Code(vscode) Visual Studio Code 是微软开源的开源 IDE 工具,它集成了 git、支持智能提示、提供各种方便的快捷键等,vscode 最为强大的是其插件扩展。通过插件扩展,vscode 迅速抢占了各大编程语言的 IDE 榜单头部位置,Go 语言也不例外。
微软为 Go 提供了官方插件支持:vscode-go。vscode-go 自身也是借助第三方工具试下新了代码智能感知、代码导航、编辑、诊断、调试和单元测试等功能。其中“在文件保存时格式化”就是通过调用 gofmt 或 goimports 实现的。vscode 与 gofmt/goimports 的集成很简单,只需在安装 vscode-go 插件时按照提示安装 vscode-go 所依赖的第三方工具或者自己手工保证 gofmt 在环境变量 PATH 的路径中即可(将$GOROOT/bin 加入到 PATH 环境变量);如果要使用 goimports,可通过本篇前面 goimports 的安装命令手工安装,并保证 goimports 所在目录在 PATH 环境变量的路径中即可。
- Vim Vim 是在*NIX 世界普遍存在的一款历史悠久的著名文本编辑器,也是很多做后端开发工作的开发者最喜欢的编辑工具。vim 的强大之处与 vscode 类似,它也有一个强大的插件扩展机制,基于 vim 插件我们便可以实现想要的各种功能。
Go 和 VIM 两个世界通过vim-go插件连接在一起。vim-go 是由 Digital Ocean 工程师 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,GOPAT**H/bin下。如果没有显式设置GOPAT**H,HOME/go 将被作为默认 GOPATH。因此你要确保 $GOPATH/bin 在 PATH 环境变量中。
vim-go 默认使用 gofmt 在文件保存时对 Go 源文件进行重新格式化,不过你可以设置使用 goimports:
在.vimrc 中,添加下面一行:
let g:go_fmt_command = "goimports"
这样只要 goimports 可执行文件在 PATH 路径下,vim-go 就可以使用它来格式化你的代码并管理文件头部的包列表了。
- 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 版本为例,后续版本设置方法可能有所不同)
【Tools】-> 【Go Tools】-> Go fmt file/Go fmt project/Goimports file
在文件保存时自动执行 gofmt/goimports 对源文件进行格式化的设置方法如下:
在【Pereferences...】对话框中,选择【Tools】-> 【File Watchers】,然后添加一个File Watcher,选择go fmt模板或goimports模板即可。
gofmt 以及其背后的设计哲学是 Go 语言的一个创新,同样也是对编程世界的一个重要贡献。作为 Go 开发人员,要牢记在提交源码前先用 gofmt 对源码进行格式化,并学会将 gofmt/goimports 工具集成到你使用的 IDE/编辑器工具中去,让这一过程自动化,使得代码格式化这件事在开发过程中变得透明,不会成为开发人员的心智负担。