go tool vet是你的好朋友,不要忽視它。
vet是一個優雅的工具,每個Go開發者都要知道並會使用它。它會做代碼靜態檢查發現可能的bug或者可疑的構造。vet是Go tool套件的一部分,我們會在以后的文章中詳細描述tool套件。它和go編譯器一起發布,這意味着它不需要額外的依賴,可以很方便地通過以下的命令調用:
$ go tool vet <directory|files>
本文中所有的go代碼段可以正常編譯。這使得go vet有價值:它可以在編譯階段和運行階段發現bug。
同時也注意,本文中的大多數代碼都是故意寫的很難看,不要使用。
在go vet和go tool vet之間選擇
go vet
和go tool vet
實際上是兩個分開的命令。
go vet,只在一個單獨的包內可用,不能使用flag 選項(來激活某些指定的檢測)。
go tool vet更加完整,它可用用於文件和目錄。目錄被遞歸遍歷來找到包。go tool vet也可以按照檢測的分組激活選項。
你可以打開一個終端,並比較go vet --help 和go tool vet --help兩個命令的不同。
Print-format 錯誤
盡管go是強類型的,但是printf-format錯誤不會在編譯階段檢測。C開發者可能使用默認激活的gcc的-Wformat選項。如果參數不能匹配格式它可以給出一個很好的警告:
warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
不幸的是,在Go里面編譯器沒有任何輸出。這是vet發揮作用的時候了。考慮下面的例子:
package mainimport "fmt" func main() { str := "hello world!" fmt.Printf("%d\n", str) }
這是一個典型的錯誤,一個壞的printf 格式。因為str是一個字符串,所以format應該用%s
,而不是%d
。
這個代碼編譯后運行,打印出%!d(string=hello world!)
,不夠友好。你可以點擊源碼下面的“run”鏈接來自己檢查。現在,我們開始運行vet。
$ go tool vet ex1.go
ex1.go:7: arg str for printf verb %d of wrong type: string
當一個指針被使用時,vet也可以檢測:
package main import "fmt" func main() { str := "hello world!" fmt.Printf("%s\n", &str) }
$ go tool vet ex2.go
ex2.go:7: arg &str for printf verb %s of wrong type: *string
vet也可以找到所有的Printf()家族函數(Printf(), Sprintf(), Fprintf(), Errorf(), Fatalf(), Logf(), Panicf()等)格式錯誤。但是如果你要實現一個函數,接收和printf類似的參數,你可以使用-printfuncs
選項使得vet來檢測。
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) }
$ 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
你可以看到如果沒有-printfuncs
選項,vet沒有任何輸出。
Boolean 錯誤
vet可以檢查一直為true、false或者冗余的表達式。
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) }
$ 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
這種類型的警告常常是非常危險的,可以引起討厭的bug。大多數情況下是由於排版錯誤引起的。
Range 循環
當讀取變量的時候,在range塊內的go協程可能是有問題的。在這些場景下,vet可以檢測到它們:
package main import "fmt" func main() { words := []string{"foo", "bar", "baz"} for _, word := range words { go func() { fmt.Println(word) }() } }
注意,這個代碼包含競態,可能不輸出任何東西。事實上,main函數可能在所有的協程執行前已經結束,這導致進程退出。
$ go tool vet range.go
range.go:10: range variable word captured by func literal
Unreachable的代碼
下面的例子包含3個函數,帶有不能到達的代碼,每個函數使用了不同的方式。
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)) }
$ go vet unreachable.go
unreachable.go:8: unreachable code
unreachable.go:19: unreachable code
unreachable.go:33: unreachable code
混雜的錯誤
這里是一個代碼段,包含了其他的幾個vet可以檢測的混雜的錯誤:
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) } }
$ 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
誤報和漏報
有時,vet可能忽略了錯誤,並警告可疑代碼,這些代碼實際上是正確的。下面的例子:
package main import "fmt" func main() { rate := 42 // this condition can never be true if rate > 60 && rate < 40 { fmt.Println("rate %:", rate) } }
$ go tool vet false.go
false.go:10: possible formatting directive in Println call
這種情況很明顯永遠都不是true,但是並不會檢測出來。然而,vet警告了一種可能的錯誤(使用Println()而不是Printf()),這里的Println()非常好用。
總的來說,使用go tool vet
提示很少會有誤報與漏報的情況。
性能
vet的README描述了,只是可能的錯誤是值得檢測的。這種方式保證了vet不會變慢。
此時,Docker包含了23M的Go代碼(包含依賴)。在Core i5機器上,vet花費了21.6秒來分析它。這是1MB/s的數量級。
可能你期待有一天,可以看到這些“不可能的檢查”包含在vet里面。為了滿足所有人的需求,默認不激活它們可能是一個好辦法。如果檢查在技術上是可行的,並且在現實生活中可以找到實際的缺陷項目,那么把它作為一個選項是有價值的。
vet和build比較
雖然vet是不完美的,但是它仍然是一個非常有價值的工具,它應該在所有的Go項目中定期使用。它是那么有價值,以至於它甚至可以讓我們懷疑是不是有些檢測不應該被編譯器檢測到。為什么有人會編譯一個檢測到有prrintf格式錯誤的代碼呢?
vet的使用
go vet 和 go tool vet基本上功能類似,go tool vet可以遞歸的對package進行語法檢測,可以自行測試區別。
對一個.go源文件進行檢查
下面的vet.go代碼有一行語法錯誤,我們用go tool vet vet.go
檢查
....
一個包下所有源文件進行檢測
go tool vet source/directory/*.go
對一個package進行語法檢查
我們同樣可以利用vet 對一個package進行檢查,當然傳入的包名必須是 相對路徑 或者完整package。
例如我當前項目目錄在$GOPATH/src/Test ,那么傳入可以輸入 go tool vet Test/vet 對vet包進行語法檢查
如果我當前工作目錄就是Test 那么我也可以直接輸入 go tool vet ./vet 利用相對路徑進行語法檢查
不可以同時對package和源文件進行檢查,但可以同時對多個包或者多個源文件進行檢查
檢測多個package
go tool vet package1 package2
檢測多個源文件
go tool vet file1.go file2.go
錯誤的用法
go tool vet file.go package1
附加tags
我們還可以給vet 傳遞一些 tag 來指定檢測行為,默認是all,全部檢查,當傳入以下tag的時候all將被設置為false
使用方法
go tool vet -atomic=false test.go
更多tag含義如下表,我從網上尋找,大家也可以去golang 官方去找 vet command的文檔,里面更精確地描述。