Skip to content

Blog Post Back to Blog

2017
Mar 16

If you code in Go, don't forget to vet

  • Thu 16th Mar 2017

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.