Go工具之vet静态诊断器

Go工具之vet——静态诊断器

go的vet工具是go代码静态诊断器,可以用以检查go项目中可通过编译但仍可能存在错误的代码,例如无法访问的代码、错误的锁使用、不必要的赋值、布尔运算错误等。

使用示例

vet是go工具套件的其中之一,它是和go编译器一起发布的,因此当我们安装好go之后,就已经带有vet工具。

vet调用方式非常简单,参数1的位置可以指定目录、源码文件或包(packages)。

1
go vet <directory|files|packages>

例如当前目录定义了main.go文件,在fmt.Printf()中使用了错误的格式符%d,而编译器并不会检查到该错误(这里笔者觉得有点奇怪,go既然是强类型语言,编译阶段为何要允许通过?),这会导致程序运行时的输出和预期不符。

1
2
3
4
5
6
7
8
1package main
2
3import "fmt"
4
5func main() {
6 s := "this is a string"
7 fmt.Printf("inappropriate formate %d\n", s)
8}

使用vet命令进行静态检查,会报告此错误。

1
2
3
1$ go vet main.go 
2# command-line-arguments
3./main.go:7:2: Printf format %d has arg s of wrong type string

诊断器与flag

vet的代码分析是由多个子诊断器组成的,目前包含22个(基于go 1.14.1,在最新版1.15 Release Notes中发现vet增加了一些内容,新增对string(int)转换的诊断和对interface-interface类型断言的诊断),这些诊断器单元代表着vet的检测范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 1asmdecl      report mismatches between assembly files and Go declarations
2assign check for useless assignments
3atomic check for common mistakes using the sync/atomic package
4bools check for common mistakes involving boolean operators
5buildtag check that +build tags are well-formed and correctly located
6cgocall detect some violations of the cgo pointer passing rules
7composites check for unkeyed composite literals
8copylocks check for locks erroneously passed by value
9errorsas report passing non-pointer or non-error values to errors.As
10httpresponse check for mistakes using HTTP responses
11loopclosure check references to loop variables from within nested functions
12lostcancel check cancel func returned by context.WithCancel is called
13nilfunc check for useless comparisons between functions and nil
14printf check consistency of Printf format strings and arguments
15shift check for shifts that equal or exceed the width of the integer
16stdmethods check signature of methods of well-known interfaces
17structtag check that struct field tags conform to reflect.StructTag.Get
18tests check for common mistaken usages of tests and examples
19unmarshal report passing non-pointer or non-interface values to unmarshal
20unreachable check for unreachable code
21unsafeptr check for invalid conversions of uintptr to unsafe.Pointer
22unusedresult check for unused results of calls to some functions

例如上文示例中的格式化符的错误,就是检查子诊断器printf中报出的错误。对于特定的诊断器,如果想了解更多细节,可通过命令go tool vet help 查看,例如

1
go tool vet help printf

使用vet时,其默认是打开了所有的诊断器。如果想禁用某个诊断器analyzer,则可以加上-=false,代表不检查analyzer包含的内容。

1
1go vet -printf=false main.go

相应的,如果只想使用某个特定的analyzer,那么可加上-=true,代表只检查analyzer所包含的内容。

1
1go vet -printf=true main.go

vet命令除了可以设置诊断器之外,还提供了很多flag,这里就不详细列出。可通过go tool vet help命令查看完整内容。

这里介绍两个相对有用的flag

  • -c=N

设置此flag可以输出错误代码行的上下相邻N行源代码。

1
2
3
4
5
6
7
8
$ go vet -c=2 main.go
# command-line-arguments
./main.go:7:2: Printf format %d has arg s of wrong type string
5 func main() {
6 s := "this is a string"
7 fmt.Printf("inappropriate formate %d\n", s)
8 }
9
  • -json

设置此flag可以将错误报告以json形式输出。

1
2
3
4
5
6
7
8
9
10
11
12
 $ go vet -json main.go
# command-line-arguments
{
"command-line-arguments": {
"printf": [
{
"posn": "/Users/slp/go/src/example/vet/main.go:7:2",
"message": "Printf format %d has arg s of wrong type string"
}
]
}
}

通过查看vet的源码(位于$GOROOT/src/cmd/vet/main.go),可以发现其诊断器全是通过引入库golang.org/x/tools/go/analysis中的内容,这是Go官方所维护的库。详细代码可以在github地址:https://github.com/golang/tools/tree/master/go/analysis中获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"cmd/internal/objabi"

"golang.org/x/tools/go/analysis/unitchecker"

"golang.org/x/tools/go/analysis/passes/asmdecl"
"golang.org/x/tools/go/analysis/passes/assign"
"golang.org/x/tools/go/analysis/passes/atomic"
...
)

func main() {
objabi.AddVersionFlag()

unitchecker.Main(
asmdecl.Analyzer,
assign.Analyzer,
atomic.Analyzer,
...
)
}

常见错误示例与vet诊断

  • printf错误
1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
s := "this is a string"
fmt.Printf("inappropriate formate %s\n", &s)
}

$ go vet main.go
# command-line-arguments
./main.go:7:2: Printf format %s has arg &s of wrong type *string
  • 布尔错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
i := 1

fmt.Println(i != 0 || i != 1)
fmt.Println(i == 1 && i == 0)
fmt.Println(i == 1 && i == 1)
}

$ go vet main.go
# command-line-arguments
./main.go:8:14: suspect or: i != 0 || i != 1
./main.go:9:14: suspect and: i == 1 && i == 0
./main.go:10:14: redundant and: i == 1 && i == 1
  • range与go协程引起的错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"time"
)

func main() {
arr := []int{1,2,3}
for _, i := range arr{
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}

$ go run main.go
3
3
3

$ go vet main.go
# command-line-arguments
./main.go:12:16: loop variable i captured by func literal

这个错误小菜刀在《不要忽略goroutine的启动时间》介绍过,感兴趣的可以看下。顺便纠正下那篇文章的结论:goroutine的启动延迟是诱因,本质原因还是由于迭代获取的临时变量是同一个地址变量。

  • 错误使用锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"sync"
)

func valueMutex(msg string, mutex sync.Mutex) {
mutex.Lock()
defer mutex.Unlock()
fmt.Println(msg)
}

func main() {
mu := sync.Mutex{}
msg := "this is a message"
valueMutex(msg, mu)
}

$ go run main.go
this is a message

$ go vet main.go
# command-line-arguments
./main.go:8:35: valueMutex passes lock by value: sync.Mutex
./main.go:17:18: call of valueMutex copies lock value: sync.Mutex

这种用法是非常危险的,函数参数中不能值传递锁(锁不能被复制,在《no copy机制》文中有介绍),应该使用指针(将参数类型sync.Mutex改为*sync.Mutex),否则极容易导致死锁。

  • 不能到达的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func unreachable(str string) string {
return str
// something work
return "result"
}

func main() {
s := unreachable("init string")
fmt.Println(s)
}

$ go run main.go
init string

$ go vet main.go
# command-line-arguments
./main.go:8:2: unreachable code
  • 定义context,忽略了cancel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"context"
"time"
)

func child(ctx context.Context) {
// do something
}

func main() {
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
child(ctx)
}

$ go run main.go
no problem

$ go vet main.go
# command-line-arguments
./main.go:15:7: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak

图片

总结

go vet是检测go项目中静态错误的有效工具,清除go vet扫描出的错误,有利于提高代码质量和养成良好的编程习惯。但是,go vet也并不是万能的,它仅仅是帮助程序员排除可能的错误,代码的质量高低更取决于程序员的水平、代码规范和编码态度。

最后,vet作为一个有效的代码质量诊断工具,笔者希望每位gopher都能够充分利用。

以上内容转载自机器铃砍菜刀

-------------本文结束 感谢阅读-------------