Or why go tool vet is your friend and should not be overlooked.
Vet is an awesome tool that every Go developper has to know and use. It does static code analysis to find possible bugs or suspicious constructs. vet is a part of the Go tool suite, which we will talk more about in future posts. It is shipped with go, which means it requires no external dependency and can simply be invoked with the following command:
$ go tool vet <directory|files>
All the go snippets presented in this article build perfectly. That is what makes go vet so valuable: it can find bugs in code that builds and runs.
Also note that most of the code you can read in this article is purposely buggy, do not use it.
Choosing between go vet and go tool vet
go vet and go tool vet are actually two separate
commands.
The first one, go vet only works with a single package and
cannot take flag options (to enable only specific checks).
go tool vet is much more complete, it works with files and
directories. Directories are recursively explored to find packages. go
tool vet also handles options to enable each check by category.
You can open a terminal and compare go vet --help and go
tool vet --help to see the difference by yourself.
Printf-format errors
Even though go is strongly typed, printf-format errors are not checked at compilation time. C developers may be used to gcc's -Wformat option, enabled by default, which gives a nice warning if the arguments do not match the format:
warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
Unfortunatly, in Go, the compiler stays silent. That's where vet kicks in. Consider this example:
package main
import "fmt"
func main() {
str := "hello world!"
fmt.Printf("%d\n", str)
}
hello-world1.go - run
This is a classic mistake, a bad printf format. Since str is a string, the format
should have been %s instead of %d.
This code builds and prints %!d(string=hello world!), not very pretty. You can check by yourself with the "run" link under the source code.
Now, let's run vet.
$ go tool vet ex1.go ex1.go:7: arg str for printf verb %d of wrong type: string
Vet also detects when a pointer is used:
package main
import "fmt"
func main() {
str := "hello world!"
fmt.Printf("%s\n", &str)
}
hello-world2.go - run
$ go tool vet ex2.go ex2.go:7: arg &str for printf verb %s of wrong type: *string
Vet is able to find format errors for all the Printf()
family functions (Printf(), Sprintf(),
Fprintf(), Errorf(), Fatalf(),
Logf(),
Panicf(), etc).
But if you were to implement a function that takes printf-like arguments, you can use the -printfuncs option to make vet check it:
package main
import "fmt"
func customLogf(str string, args ...interface{}) {
fmt.Printf(str, args...)
}
func main() {
i := 42
customLogf("the answer is %s\n", i)
}
custom-printf-func.go - run
$ go tool vet custom-printf-func.go $ go tool vet -printfuncs customLogf custom-printf-func.go custom-printf-func.go:11: arg i for printf verb %s of wrong type: int
You can see that without the -printfuncs option, vet stays silent.
Boolean errors
Vet can detect expressions that are always true, always false and redundant.
package main
import "fmt"
func main() {
var i int
// always true
fmt.Println(i != 0 || i != 1)
// always false
fmt.Println(i == 0 && i == 1)
// redundant check
fmt.Println(i == 0 && i == 0)
}
bool-expr.go - run
$ go vet bool-expr.go bool-expr.go:9: suspect or: i != 0 || i != 1 bool-expr.go:12: suspect and: i == 0 && i == 1 bool-expr.go:15: redundant and: i == 0 && i == 0
This kind of warnings are usually pretty serious and can be the cause of nasty bugs, mostly caused by typo errors.
Range loops
Goroutines inside range blocks can be problematic when reading variables. In some cases, vet is able to detect them:
package main
import "fmt"
func main() {
words := []string{"foo", "bar", "baz"}
for _, word := range words {
go func() {
fmt.Println(word)
}()
}
}
range.go - run
Note: this (terrible) piece of code contains a race condition and may
very well not output anything. Indeed, the main() function could
end before any of the goroutines are executed, ending the process.
$ go tool vet range.go range.go:10: range variable word captured by func literal
Unreachable code
The following example contains 3 functions with code that cannot be reached, each in a different way.
package main
import "fmt"
func add(a int, b int) int {
return a + b
fmt.Println("unreachable")
return 0
}
func div(a int, b int) int {
if b == 0 {
panic("division by 0")
} else {
return a / b
}
fmt.Println("unreachable")
return 0
}
func fibonnaci(n int) int {
switch n {
case 0:
return 1
case 1:
return 1
default:
return fibonnaci(n-1) + fibonnaci(n-2)
}
fmt.Println("unreachable")
return 0
}
func main() {
fmt.Println(add(1, 2))
fmt.Println(div(10, 2))
fmt.Println(fibonnaci(5))
}
unreachable.go - run
$ go vet unreachable.go unreachable.go:8: unreachable code unreachable.go:19: unreachable code unreachable.go:33: unreachable code
Miscellaneous errors
Here is a code snippet that contains other miscellaneous examples of errors that Vet can detect:
package main
import (
"fmt"
"log"
"net/http"
)
func f() {}
func main() {
// Self assignment
i := 42
i = i
// a declared function cannot be nil
fmt.Println(f == nil)
// shift too long
fmt.Println(i >> 32)
// res used before checking err
res, err := http.Get("https://www.spreadsheetdb.io/")
defer res.Body.Close()
if err != nil {
log.Fatal(err)
}
}
misc.go - run
$ go tool vet misc.go misc.go:14: self-assignment of i to i misc.go:17: comparison of function f == nil is always false misc.go:20: i might be too small for shift of 32 misc.go:24: using res before checking for errors
False positives and false negatives
Occasionally, vet may overlook mistakes and warn about suspicious code that is actually correct. Here is an example that does both:
package main
import "fmt"
func main() {
rate := 42
// this condition can never be true
if rate > 60 && rate < 40 {
fmt.Println("rate %:", rate)
}
}
false.go - run
$ go tool vet false.go false.go:10: possible formatting directive in Println call
The condition will obviously never be true but it is not detected.
However, vet warns about a possible mistake (use of
Println() instead of
Printf()) when Println() is perfectly fine here.
Overall, false positives and false negatives are rare enough to keep
go tool vet output widly relevant.
Performances
Vet's README states that only problems likely are worth checking. This approach ensures that vet won't get slower over time.
At the moment, Docker contains around 23M of Go code (including dependencies). On a Core i5, vet takes around 21.6 seconds to analyze it. That's an order of magnitude of 1MB/s.
Maybe we can hope to see, one day, those "unlikely checks" included in vet. Not enabling them by default would be a good way to satisfy everyone. If a check is technically doable and can find actual bugs in real life projects, it would be worth having it as an option.
Vet vs build
Even though vet is not perfect, it is still an extremely valuable ally and it should be used regularly with all Go projects. So valuable in fact, that it can even make us wonder if some of the checks should not be performed by the compiler instead. Why would anyone want to compile a code containing a detectable printf format error?
You can find all the examples from the post in this dedicated Github repository.
Follow us on Twitter to be updated about future Go posts.
More about vet