Go语言精进之路:从新手到高手的编程思想、方法和技巧(1)
上QQ阅读APP看书,第一时间看更新

6.2 使用gofmt

截至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的实用技巧。

1. 使用gofmt -s选项简化代码

虽然Go语言推崇一件事情仅有一种方式完成,但难免存在一些事情依然有多种表达方法,比如下面这个例子。

存在一个字符串切片v:

v := []string{...}

如果要迭代访问字符串切片v的各个元素,可以这么做:

for _ = range v {
    ...
}

在Go 1.4及后续版本中,还可以这么做:

for range v {
    ...
}

Go开发者更推崇后面那种简化后的写法。这样的例子在Go语言的演进过程中还存在一些。为了避免将代码转换为简化语法给开发人员带来额外工作量,Go官方在gofmt中提供了-s选项。通过gofmt -s可以将遗留代码中的非简化代码自动转换为简化写法,并且没有副作用,因此一般“-s”选项都会是gofmt执行的默认选项。

2. 使用gofmt -r执行代码“微重构”

代码重构是软件工程过程中的日常操作,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:])

3. 使用gofmt -l按格式要求输出满足条件的文件列表

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格式的文件,这些文件也会被一并输出。