# go vet與go tool vet
命令```go vet```是一個用于檢查Go語言源碼中靜態錯誤的簡單工具。與大多數Go命令一樣,```go vet```命令可以接受```-n```標記和```-x```標記。```-n```標記用于只打印流程中執行的命令而不真正執行它們。```-n```標記也用于打印流程中執行的命令,但不會取消這些命令的執行。示例如下:
hc@ubt:~$ go vet -n pkgtool
/usr/local/go/pkg/tool/linux_386/vet golang/goc2p/src/pkgtool/envir.go golang/goc2p/src/pkgtool/envir_test.go golang/goc2p/src/pkgtool/fpath.go golang/goc2p/src/pkgtool/ipath.go golang/goc2p/src/pkgtool/pnode.go golang/goc2p/src/pkgtool/util.go golang/goc2p/src/pkgtool/util_test.go
```go vet```命令的參數既可以是代碼包的導入路徑,也可以是Go語言源碼文件的絕對路徑或相對路徑。但是,這兩種參數不能混用。也就是說,```go vet```命令的參數要么是一個或多個代碼包導入路徑,要么是一個或多個Go語言源碼文件的路徑。
```go vet```命令是```go tool vet```命令的簡單封裝。它會首先載入和分析指定的代碼包,并把指定代碼包中的所有Go語言源碼文件和以“.s”結尾的文件的相對路徑作為參數傳遞給```go tool vet```命令。其中,以“.s”結尾的文件是匯編語言的源碼文件。如果```go vet```命令的參數是Go語言源碼文件的路徑,則會直接將這些參數傳遞給```go tool vet```命令。
如果我們直接使用```go tool vet```命令,則其參數可以傳遞任意目錄的路徑,或者任何Go語言源碼文件和匯編語言源碼文件的路徑。路徑可以是絕對的也可以是相對的。
實際上,```vet```屬于Go語言自帶的特殊工具,也是比較底層的命令之一。Go語言自帶的特殊工具的存放路徑是$GOROOT/pkg/tool/$GOOS_$GOARCH/,我們暫且稱之為Go工具目錄。我們再來復習一下,環境變量GOROOT的值即Go語言的安裝目錄,環境變量GOOS的值代表程序構建環境的目標操作系統的標識,而環境變量$GOARCH的值則為程序構建環境的目標計算架構。另外,名為$GOOS_$GOARCH的目錄被叫做平臺相關目錄。Go語言允許我們通過執行```go tool```命令來運行這些特殊工具。在Linux 32bit的環境下,我們的Go語言安裝目錄是/usr/local/go/。因此,```go tool vet```命令指向的就是被存放在/usr/local/go/pkg/tool/linux_386目錄下的名為```vet```的工具。
```go tool vet```命令的作用是檢查Go語言源代碼并且報告可疑的代碼編寫問題。比如,在調用```Printf```函數時沒有傳入格式化字符串,以及某些不標準的方法簽名,等等。該命令使用試探性的手法檢查錯誤,因此并不能保證報告的問題確實需要解決。但是,它確實能夠找到一些編譯器沒有捕捉到的錯誤。
```go tool vet```命令程序在被執行后會首先解析標記并檢查標記值。```go tool vet```命令支持的所有標記如下表。
_表0-16 ```go tool vet```命令的標記說明_
<table class="table table-bordered table-striped table-condensed">
<tr>
<th width=120px>
標記名稱
</th>
<th>
標記描述
</th>
</tr>
<tr>
<td>
-all
</td>
<td>
進行全部檢查。如果有其他檢查標記被設置,則命令程序會將此值變為false。默認值為true。
</td>
</tr>
<tr>
<td>
-asmdecl
</td>
<td>
對匯編語言的源碼文件進行檢查。默認值為false。
</td>
</tr>
<tr>
<td>
-assign
</td>
<td>
檢查賦值語句。默認值為false。
</td>
</tr>
<tr>
<td>
-atomic
</td>
<td>
檢查代碼中對代碼包sync/atomic的使用是否正確。默認值為false。
</td>
</tr>
<tr>
<td>
-buildtags
</td>
<td>
檢查編譯標簽的有效性。默認值為false。
</td>
</tr>
<tr>
<td>
-composites
</td>
<td>
檢查復合結構實例的初始化代碼。默認值為false。
</td>
</tr>
<tr>
<td>
-compositeWhiteList
</td>
<td>
是否使用復合結構檢查的白名單。僅供測試使用。默認值為true。
</td>
</tr>
<tr>
<td>
-methods
</td>
<td>
檢查那些擁有標準命名的方法的簽名。默認值為false。
</td>
</tr>
<tr>
<td>
-printf
</td>
<td>
檢查代碼中對打印函數的使用是否正確。默認值為false。
</td>
</tr>
<tr>
<td>
-printfuncs
</td>
<td>
需要檢查的代碼中使用的打印函數的名稱的列表,多個函數名稱之間用英文半角逗號分隔。默認值為空字符串。
</td>
</tr>
<tr>
<td>
-rangeloops
</td>
<td>
檢查代碼中對在```range```語句塊中迭代賦值的變量的使用是否正確。默認值為false。
</td>
</tr>
<tr>
<td>
-structtags
</td>
<td>
檢查結構體類型的字段的標簽的格式是否標準。默認值為false。
</td>
</tr>
<tr>
<td>
-unreachable
</td>
<td>
查找并報告不可到達的代碼。默認值為false。
</td>
</tr>
</table>
在閱讀上面表格中的內容之后,讀者可能對這些標簽的具體作用及其對命令程序檢查步驟的具體影響還很模糊。不過沒關系,我們下面就會對它們進行逐一的說明。
**-all標記**
如果標記```-all```有效(標記值不為```false```),那么命令程序會對目標文件進行所有已知的檢查。實際上,標記```-all```的默認值就是```true```。也就是說,在執行```go tool vet```命令且不加任何標記的情況下,命令程序會對目標文件進行全面的檢查。但是,只要有一個另外的標記(```-compositeWhiteList```和```-printfuncs```這兩個標記除外)有效,命令程序就會把標記```-all```設置為false,并只會進行與有效的標記對應的檢查。
**-assign標記**
如果標記```-assign```有效(標記值不為```false```),則命令程序會對目標文件中的賦值語句進行自賦值操作檢查。什么叫做自賦值呢?簡單來說,就是將一個值或者實例賦值給它本身。像這樣:
var s1 string = "S1"
s1 = s1 // 自賦值
或者
s1, s2 := "S1", "S2"
s2, s1 = s2, s1 // 自賦值
檢查程序會同時遍歷等號兩邊的變量或者值。在抽象語法樹的語境中,它們都被叫做表達式節點。檢查程序會檢查等號兩邊對應的表達式是否相同。判斷的依據是這兩個表達式節點的字符串形式是否相同。在當前的場景下,這種相同意味著它們的變量名是相同的。如前面的示例。
有兩種情況是可以忽略自賦值檢查的。一種情況是短變量聲明語句。根據Go語言的語法規則,當我們在函數中要在聲明局部變量的同時對其賦值,就可以使用```:=```形式的變量賦值語句。這也就意味著```:=```左邊的變量名稱在當前的上下文環境中應該還未曾出現過(否則不能通過編譯)。因此,在這種賦值語句中不可能出現自賦值的情況,忽略對它的檢查也是合理的。另一種情況是等號左右兩邊的表達式個數不相等的變量賦值語句。如果在等號的右邊是對某個函數或方法的調用,就會造成這種情況。比如:
file, err := os.Open(wp)
很顯然,這個賦值語句肯定不是自賦值語句。因此,不需要對此種情況進行檢查。如果等號右邊并不是對函數或方法調用的表達式,并且等號兩邊的表達式數量也不相等,那么勢必會在編譯時引發錯誤,也不必檢查。
**-atomic標記**
如果標記```-atomic```有效(標記值不為```false```),則命令程序會對目標文件中的使用代碼包```sync/atomic```進行原子賦值的語句進行檢查。原子賦值語句像這樣:
var i32 int32
i32 = 0
newi32 := atomic.AddInt32(&i32, 3)
fmt.Printf("i32: %d, newi32: %d.\n", i32, newi32)
函數```AddInt32```會原子性的將變量```i32```的值加```3```,并返回這個新值。因此上面示例的打印結果是:
i32: 3, newi32: 3
在代碼包```sync/atomic```中,與```AddInt32```類似的函數還有```AddInt64```、```AddUint32```、```AddUint64```和```AddUintptr```。檢查程序會對上述這些函數的使用方式進行檢查。檢查的關注點在破壞原子性的使用方式上。比如:
i32 = 1
i32 = atomic.AddInt32(&i32, 3)
_, i32 = 5, atomic.AddInt32(&i32, 3)
i32, _ = atomic.AddInt32(&i32, 1), 5
上面示例中的后三行賦值語句都屬于原子賦值語句,但它們都破壞了原子賦值的原子性。以第二行的賦值語句為例,等號左邊的```atomic.AddInt32(&i32, 3)```的作用是原子性的將變量```i32```的值增加```3```。但該語句又將函數的結果值賦值給變量```i32```,這個二次賦值屬于對變量```i32```的重復賦值,也使原本擁有原子性的賦值操作被拆分為了兩個步驟的非原子操作。如果在對變量```i32```的第一次原子賦值和第二次非原子的重復賦值之間又有另一個程序對變量```i32```進行了原子賦值,那么當前程序中的這個第二次賦值就破壞了那兩次原子賦值本應有的順序性。因為,在另一個程序對變量```i32```進行原子賦值后,當前程序中的第二次賦值又將變量```i32```的值設置回了之前的值。這顯然是不對的。所以,上面示例中的第二行代碼應該改為:
atomic.AddInt32(&i32, 3)
并且,對第三行和第四行的代碼也應該有類似的修改。檢查程序如果在目標文件中查找到像上面示例的第二、三、四行那樣的語句,就會打印出相應的錯誤信息。
另外,上面所說的導致原子性被破壞的重復賦值語句還有一些類似的形式。比如:
i32p := &i32
*i32p = atomic.AddUint64(i32p, 1)
這與之前的示例中的代碼的含義幾乎是一樣。另外還有:
var counter struct{ N uint32 }
counter.N = atomic.AddUint64(&counter.N, 1)
和
ns := []uint32{10, 20}
ns[0] = atomic.AddUint32(&ns[0], 1)
nps := []*uint32{&ns[0], &ns[1]}
*nps[0] = atomic.AddUint32(nps[0], 1)
在最近的這兩個示例中,雖然破壞原子性的重復賦值操作因結構體類型或者數組類型的介入顯得并不那么直觀了,但依然會被檢查程序發現并及時打印錯誤信息。
順便提一句,對于原子賦值語句和普通賦值語句,檢查程序都會忽略掉對等號兩邊的表達式的個數不相等的賦值語句的檢查。
**-buildtags標記**
前文已提到,如果標記```-buildtags```有效(標記值不為```false```),那么命令程序會對目標文件中的編譯標簽(如果有的話)的格式進行檢查。什么叫做條件編譯?在實際場景中,有些源碼文件中包含了平臺相關的代碼。我們希望只在某些特定平臺下才編譯它們。這種有選擇的編譯方法就被叫做條件編譯。在Go語言中,條件編譯的配置就是通過編譯標簽來完成的。編譯器需要依據源碼文件中編譯標簽的內容來決定是否編譯當前文件。編譯標簽可必須出現在任何源碼文件(比如擴展名為“.go”,“.h”,“.c”,“.s”等的源碼文件) 的頭部的單行注釋中,并且在其后面需要有空行。
至于編譯標簽的具體寫法,我們就不在此贅述了。讀者可以參看Go語言官方的相關文檔。我們在這里只簡單羅列一下```-buildtags```有效時命令程序對編譯標簽的檢查內容:
1. 若編譯標簽前導符“+build”后沒有緊隨空格,則打印格式錯誤信息。
2. 若編譯標簽所在行與第一個多行注釋或代碼行之間沒有空行,則打印錯誤信息。
3. 若在某個單一參數的前面有兩個英文嘆號“!!”,則打印錯誤信息。
4. 若單個參數包含字母、數字、“_”和“.”以外的字符,則打印錯誤信息。
5. 若出現在文件頭部單行注釋中的編譯標簽前導符“+build”未緊隨在單行注釋前導符“//”之后,則打印錯誤信息。
如果一個在文件頭部的單行注釋中的編譯標簽通過了上述的這些檢查,則說明它的格式是正確無誤的。由于只有在文件頭部的單行注釋中編譯標簽才會被編譯器認可,所以檢查程序只會查找和檢查源碼文件中的第一個多行注釋或代碼行之前的內容。
**-composites標記和-compositeWhiteList標記**
如果標記```-composites```有效(標記值不為```false```),則命令程序會對目標文件中的復合字面量進行檢查。請看如下示例:
type counter struct {
name string
number int
}
...
c := counter{name: "c1", number: 0}
在上面的示例中,代碼```counter{name: "c1", number: 0}```是對結構體類型```counter```的初始化。如果復合字面量中涉及到的類型不在當前代碼包內部且未在所屬文件中被導入,那么檢查程序不但會打印錯誤信息還會將退出代碼設置為1,并且取消后續的檢查。退出代碼為1意味著檢查程序已經報告了一個或多個問題。這個問題比僅僅引起錯誤信息報告的問題更加嚴重。
在通過上述檢查的前提下,如果復合字面量中包含了對結構體類型的字段的賦值但卻沒有指明字段名,像這樣:
var v = flag.Flag{
"Name",
"Usage",
nil, // Value
"DefValue",
}
那么檢查程序也會打印錯誤信息,以提示在復合字面量中包含有未指明的字段賦值。
這有一個例外,那就是當標記```-compositeWhiteList```有效(標記值不為```false```)的時候。只要類型在白名單中,即使其初始化語句中含有未指明的字段賦值也不會被提示。這是出于什么考慮呢?先來看下面的示例:
type sliceType []string
...
st1 := sliceType{"1", "2", "3"}
上面示例中的```sliceType{"1", "2", "3"}```也屬于復合字面量。但是它初始化的類型實際上是一個切片值,只不過這個切片值被別名化并被包裝為了另一個類型而已。在這種情況下,復合字面量中的賦值不需要指明字段,事實上這樣的類型也不包含任何字段。白名單中所包含的類型都是這種情況。它們是在標準庫中的包裝了切片值的類型。它們不需要被檢查,因為這種情況是合理的。
在默認情況下,標記```-compositeWhiteList```是有效的。也就是說,檢查程序不會對它們的初始化代碼進行檢查,除非我們在執行```go tool vet```命令時顯示的將```-compositeWhiteList```標記的值設置為false。
**-methods標記**
如果標記```-methods```有效(標記值不為```false```),則命令程序會對目標文件中的方法定義進行規范性的進行檢查。這里所說的規范性是狹義的。
在檢查程序內部存有一個規范化方法字典。這個字典的鍵用來表示方法的名稱,而字典的元素則用來描述方法應有的參數和結果的類型。在該字典中列出的都是Go語言標準庫中使用最廣泛的接口類型的方法。這些方法的名字都非常通用。它們中的大多數都是它們所屬接口類型的唯一方法。我們在第4章中提到過,Go語言中的接口類型實現方式是非侵入式的。只要結構體類型實現了某一個接口類型中的所有方法,就可以說這個結構體類型是該接口類型的一個實現。這種判斷方式被稱為動態接口檢查。它只在運行時進行。如果我們想讓一個結構體類型成為某一個接口類型的實現,但又寫錯了要實現的接口類型中的方法的簽名,那么也不會引發編譯器報錯。這里所說的方法簽名包括方法的參數聲明列表和結果聲明列表。雖然動態接口檢查失敗時并不會報錯,但是它卻會間接的引發其它錯誤。而這些被間接引發的錯誤只會在運行時發生。示例如下:
type MySeeker struct {
// 忽略字段定義
}
func (self *MySeeker) Seek(whence int, offset int64) (ret int64, err error) {
// 想實現接口類型io.Seeker中的唯一方法,但是卻把參數的順序寫顛倒了。
// 忽略實現代碼
}
func NewMySeeker io.Seeker {
return &MySeeker{/* 忽略字段初始化 */} // 這里會引發一個運行時錯誤。
//由于MySeeker的Seek方法的簽名寫錯了,所以MySeeker不是io.Seeker的實現。
}
這種運行時錯誤看起來會比較詭異,并且錯誤排查也會相對困難,所以應該盡量避免。```-methods```標記所對應的檢查就是為了達到這個目的。檢查程序在發現目標文件中某個方法的名字被包含在規范化方法字典中但其簽名與對應的描述不對應的時候,就會打印錯誤信息并設置退出代碼為1。
我在這里附上在規范化方法字典中列出的方法的信息:
_表0-17 規范化方法字典中列出的方法_
<table class="table table-bordered table-striped table-condensed">
<tr>
<th width=100px>
方法名稱
</th>
<th width=90px>
參數類型
</th>
<th width=90px>
結果類型
</th>
<th width=80px>
所屬接口
</th>
<th width=60px>
唯一方法
</th>
</tr>
<tr>
<td>
Format
</td>
<td>
"fmt.State", "rune"
</td>
<td>
<無>
</td>
<td>
fmt.Formatter
</td>
<td>
是
</td>
</tr>
<tr>
<td>
GobDecode
</td>
<td>
"[]byte"
</td>
<td>
"error"
</td>
<td>
gob.GobDecoder
</td>
<td>
是
</td>
</tr>
<tr>
<td>
GobEncode
</td>
<td>
<無>
</td>
<td>
"[]byte", "error"
</td>
<td>
gob.GobEncoder
</td>
<td>
是
</td>
</tr>
<tr>
<td>
MarshalJSON
</td>
<td>
<無>
</td>
<td>
"[]byte", "error"
</td>
<td>
json.Marshaler
</td>
<td>
是
</td>
</tr>
<tr>
<td>
Peek
</td>
<td>
"int"
</td>
<td>
"[]byte", "error"
</td>
<td>
image.reader
</td>
<td>
否
</td>
</tr>
<tr>
<td>
ReadByte
</td>
<td>
"int"
</td>
<td>
"[]byte", "error"
</td>
<td>
io.ByteReader
</td>
<td>
是
</td>
</tr>
<tr>
<td>
ReadFrom
</td>
<td>
"io.Reader"
</td>
<td>
"int64", "error"
</td>
<td>
io.ReaderFrom
</td>
<td>
是
</td>
</tr>
<tr>
<td>
ReadRune
</td>
<td>
<無>
</td>
<td>
"rune", "int", "error"
</td>
<td>
io.RuneReader
</td>
<td>
是
</td>
</tr>
<tr>
<td>
Scan
</td>
<td>
"fmt.ScanState", "rune"
</td>
<td>
"error"
</td>
<td>
fmt.Scanner
</td>
<td>
是
</td>
</tr>
<tr>
<td>
Seek
</td>
<td>
"int64", "int"
</td>
<td>
"int64", "error"
</td>
<td>
io.Seeker
</td>
<td>
是
</td>
</tr>
<tr>
<td>
UnmarshalJSON
</td>
<td>
"[]byte"
</td>
<td>
"error"
</td>
<td>
json.Unmarshaler
</td>
<td>
是
</td>
</tr>
<tr>
<td>
UnreadByte
</td>
<td>
<無>
</td>
<td>
"error"
</td>
<td>
io.ByteScanner
</td>
<td>
否
</td>
</tr>
<tr>
<td>
UnreadRune
</td>
<td>
<無>
</td>
<td>
"error"
</td>
<td>
io.RuneScanner
</td>
<td>
否
</td>
</tr>
<tr>
<td>
WriteByte
</td>
<td>
"byte"
</td>
<td>
"error"
</td>
<td>
io.ByteWriter
</td>
<td>
是
</td>
</tr>
<tr>
<td>
WriteTo
</td>
<td>
"io.Writer"
</td>
<td>
"int64", "error"
</td>
<td>
io.WriterTo
</td>
<td>
是
</td>
</tr>
</table>
**-printf標記和-printfuncs標記**
標記```-printf```旨在目標文件中檢查各種打印函數使用的正確性。而標記```-printfuncs```及其值則用于明確指出需要檢查的打印函數。```-printfuncs```標記的默認值為空字符串。也就是說,若不明確指出檢查目標則檢查所有打印函數。可被檢查的打印函數如下表:
_表0-18 格式化字符串中動詞的格式要求_
<table class="table table-bordered table-striped table-condensed">
<tr>
<th width=120px>
函數全小寫名稱
</th>
<th width=120px>
支持格式化
</th>
<th width=120px>
可自定義輸出
</th>
<th width=120px>
自帶換行
</th>
</tr>
<tr>
<td>
error
</td>
<td>
否
</td>
<td>
否
</td>
<td>
是
</td>
</tr>
<tr>
<td>
fatal
</td>
<td>
否
</td>
<td>
否
</td>
<td>
是
</td>
</tr>
<tr>
<td>
fprint
</td>
<td>
否
</td>
<td>
是
</td>
<td>
否
</td>
</tr>
<tr>
<td>
fprintln
</td>
<td>
否
</td>
<td>
是
</td>
<td>
是
</td>
</tr>
<tr>
<td>
panic
</td>
<td>
否
</td>
<td>
否
</td>
<td>
否
</td>
</tr>
<tr>
<td>
panicln
</td>
<td>
否
</td>
<td>
否
</td>
<td>
是
</td>
</tr>
<tr>
<td>
print
</td>
<td>
否
</td>
<td>
否
</td>
<td>
否
</td>
</tr>
<tr>
<td>
println
</td>
<td>
否
</td>
<td>
否
</td>
<td>
是
</td>
</tr>
<tr>
<td>
sprint
</td>
<td>
否
</td>
<td>
否
</td>
<td>
否
</td>
</tr>
<tr>
<td>
sprintln
</td>
<td>
否
</td>
<td>
否
</td>
<td>
是
</td>
</tr>
<tr>
<td>
errorf
</td>
<td>
是
</td>
<td>
否
</td>
<td>
否
</td>
</tr>
<tr>
<td>
fatalf
</td>
<td>
是
</td>
<td>
否
</td>
<td>
否
</td>
</tr>
<tr>
<td>
fprintf
</td>
<td>
是
</td>
<td>
是
</td>
<td>
否
</td>
</tr>
<tr>
<td>
panicf
</td>
<td>
是
</td>
<td>
否
</td>
<td>
否
</td>
</tr>
<tr>
<td>
printf
</td>
<td>
是
</td>
<td>
否
</td>
<td>
否
</td>
</tr>
<tr>
<td>
sprintf
</td>
<td>
是
</td>
<td>
是
</td>
<td>
否
</td>
</tr>
</table>
以字符串格式化功能來區分,打印函數可以分為可打印格式化字符串的打印函數(以下簡稱格式化打印函數)和非格式化打印函數。對于格式化打印函數來說,其第一個參數必是格式化表達式,也可被稱為模板字符串。而其余參數應該為需要被填入模板字符串的變量。像這樣:
fmt.Printf("Hello, %s!\n", "Harry")
// 會輸出:Hello, Harry!
而非格式化打印函數的參數則是一個或多個要打印的內容。比如:
fmt.Println("Hello,", "Harry!")
// 會輸出:Hello, Harry!
以指定輸出目的地功能區分,打印函數可以被分為可自定義輸出目的地的的打印函數(以下簡稱自定義輸出打印函數)和標準輸出打印函數。對于自定義輸出打印函數來說,其第一個函數必是其打印的輸出目的地。比如:
fmt.Fprintf(os.Stdout, "Hello, %s!\n", "Harry")
// 會在標準輸出設備上輸出:Hello, Harry!
上面示例中的函數```fmt.Fprintf```既能夠讓我們自定義打印的輸出目的地,又能夠格式化字符串。此類打印函數的第一個參數的類型應為```io.Writer```接口類型。只要某個類型實現了該接口類型中的所有方法,就可以作為函數```Fprintf```的第一個參數。例如,我們還可以使用代碼包```bytes```中的結構體```Buffer```來接收打印函數打印的內容。像這樣:
var buff bytes.Buffer
fmt.Fprintf(&buff, "Hello, %s!\n", "Harry")
fmt.Print("Buffer content:", buff.String())
// 會在標準輸出設備上輸出:Buffer content: Hello, Harry!
而標準輸出打印函數則只能將打印內容到標準輸出設備上。就像函數```fmt.Printf```和```fmt.Println```所做的那樣。
檢查程序會首先關注打印函數的參數數量。如果參數數量不足,則可以認為在當前調用打印函數的語句中并不會出現用法錯誤。所以,檢查程序會忽略對它的檢查。檢查程序中對打印函數的最小參數是這樣定義的:對于可以自定義輸出的打印函數來說,最小參數數量為2,其它打印函數的最小參數數量為1。如果打印函數的實際參數數量小于對應的最小參數數量,就會被判定為參數數量不足。
對于格式化打印函數,檢查程序會進行如下檢查:
1. 如果格式化字符串無法被轉換為基本字面量(標識符以及用于表示int類型值、float類型值、char類型值、string類型值的字面量等),則檢查程序會忽略剩余的檢查。如果```-v```標記有效,則會在忽略檢查前打印錯誤信息。另外,格式化打印函數的格式化字符串必須是字符串類型的。因此,如果對應位置上的參數的類型不是字符串類型,那么檢查程序會立即打印錯誤信息,并設置退出代碼為1。實際上,這個問題已經可以引起一個編譯錯誤了。
2. 如果格式化字符串中不包含動詞(verbs),而格式化字符串后又有多余的參數,則檢查程序會立即打印錯誤信息,并設置退出代碼為1,且忽略后續檢查。我現在舉個例子。我們拿之前的一個示例作為基礎,即:
fmt.Printf("Hello, %s!\n", "Harry")
在這個示例中,格式化字符串中的“%s”就是我們所說的動詞,“%”就是動詞的前導符。它相當于一個需要被填的空。一般情況下,在格式化字符串中被填的空的數量應該與后續參數的數量相同。但是可以出現在格式化字符串中沒有動詞并且在格式化字符串之后沒有額外參數的情況。在這種情況下,該格式化打印函數就相當于一個非格式化打印函數。例如,下面這個語句會導致此步檢查不通過:
fmt.Printf("Hello!\n", "Harry")
3. 檢查程序還會檢查動詞的格式。這部分檢查會非常嚴格。檢查程序對于格式化字符串中動詞的格式要求如表0-19。表中對每個動詞只進行了簡要的說明。讀者可以查看標準庫代碼包```fmt```的文檔以了解關于它們的詳細信息。命令程序會按照表5-19中的要求對格式化及其后續參數進行檢查。如上表所示,這部分檢查分為兩步驟。第一個步驟是檢查格式化字符串中的動詞上是否附加了不合法的標記,第二個步驟是檢查格式化字符串中的動詞與后續對應的參數的類型是否匹配。只要檢查出問題,檢查程序就會打印出錯誤信息并且設置退出代碼為1。
4. 如果格式化字符串中的動詞不被支持,則檢查程序同樣會打印錯誤信息后,并設置退出代碼為1。
_表0-19 格式化字符串中動詞的格式要求_
<table class="table table-bordered table-striped table-condensed">
<tr>
<th width=35px>
動詞
</th>
<th width=120px>
合法的附加標記
</th>
<th width=120px>
允許的參數類型
</th>
<th width=60px>
簡要說明
</th>
</tr>
<tr>
<td>
b
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
int或float
</td>
<td>
用于二進制表示法。
</td>
</tr>
<tr>
<td>
c
</td>
<td>
“-”
</td>
<td>
rune或int
</td>
<td>
用于單個字符的Unicode表示法。
</td>
</tr>
<tr>
<td>
d
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
int
</td>
<td>
用于十進制表示法。
</td>
</tr>
<tr>
<td>
e
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
float
</td>
<td>
用于科學記數法。
</td>
</tr>
<tr>
<td>
E
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
float
</td>
<td>
用于科學記數法。
</td>
</tr>
<tr>
<td>
f
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
float
</td>
<td>
用于控制浮點數精度。
</td>
</tr>
<tr>
<td>
F
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
float
</td>
<td>
用于控制浮點數精度。
</td>
</tr>
<tr>
<td>
g
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
float
</td>
<td>
用于壓縮浮點數輸出。
</td>
</tr>
<tr>
<td>
G
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
float
</td>
<td>
用于動態選擇浮點數輸出格式。
</td>
</tr>
<tr>
<td>
o
</td>
<td>
“ ”,“-”,“+”,“.”,“0”和“#”
</td>
<td>
int
</td>
<td>
用于八進制表示法。
</td>
</tr>
<tr>
<td>
p
</td>
<td>
“-”和“#”
</td>
<td>
pointer
</td>
<td>
用于表示指針地址。
</td>
</tr>
<tr>
<td>
q
</td>
<td>
“ ”,“-”,“+”,“.”,“0”和“#”
</td>
<td>
rune,int或string
</td>
<td>
用于生成帶雙引號的字符串形式的內容。
</td>
</tr>
<tr>
<td>
s
</td>
<td>
“ ”,“-”,“+”,“.”和“0”
</td>
<td>
rune,int或string
</td>
<td>
用于生成字符串形式的內容。
</td>
</tr>
<tr>
<td>
t
</td>
<td>
“-”
</td>
<td>
bool
</td>
<td>
用于生成與布爾類型對應的字符串值。(“true”或“false”)
</td>
</tr>
<tr>
<td>
T
</td>
<td>
“-”
</td>
<td>
任何類型
</td>
<td>
用于用Go語法表示任何值的類型。
</td>
</tr>
<tr>
<td>
U
</td>
<td>
“-”和“#”
</td>
<td>
rune或int
</td>
<td>
用于針對Unicode的表示法。
</td>
</tr>
<tr>
<td>
v
</td>
<td>
“”,“-”,“+”,“.”,“0”和“#”
</td>
<td>
任何類型
</td>
<td>
以默認格式格式化任何值。
</td>
</tr>
<tr>
<td>
x
</td>
<td>
“”,“-”,“+”,“.”,“0”和“#”
</td>
<td>
rune,int或string
</td>
<td>
以十六進制、全小寫的形式格式化每個字節。
</td>
</tr>
<tr>
<td>
X
</td>
<td>
“”,“-”,“+”,“.”,“0”和“#”
</td>
<td>
rune,int或string
</td>
<td>
以十六進制、全大寫的形式格式化每個字節。
</td>
</tr>
</table>
對于非格式化打印函數,檢查程序會進行如下檢查:
1. 如果打印函數不是可以自定義輸出的打印函數,那么其第一個參數就不能是標準輸出```os.Stdout```或者標準錯誤輸出```os.Stderr```。否則,檢查程序將打印錯誤信息并設置退出代碼為1。這主要是為了防止程序編寫人員的筆誤。比如,他們可能會把函數```fmt.Println```當作函數```fmt.Printf```來用。
2. 如果打印函數是不自帶換行的,比如```fmt.Printf```和```fmt.Print```,則它必須只少有一個參數。否則,檢查程序將打印錯誤信息并設置退出代碼為1。像這樣的調用打印函數的語句是沒有任何意義的。并且,如果這個打印函數還是一個格式化打印函數,那么這還會引起一個編譯錯誤。需要注意的是,函數名稱為```Error```的方法不會在被檢查之列。比如,標準庫代碼包```testing```中的結構體類型```T```和```B```的方法```Error```。這是因為它們可能實現了接口類型```Error```。這個接口類型中唯一的方法```Error```無需任何參數。
3. 如果第一個參數的值為字符串類型的字面量且帶有格式化字符串中才應該有的動詞的前導符“%”,則檢查程序會打印錯誤信息并設置退出代碼為1。因為非格式化打印函數中不應該出現格式化字符串。
4. 如果打印函數是自帶換行的,那么在打印內容的末尾就不應該有換行符“\n”。否則,檢查程序會打印錯誤信息并設置退出代碼為1。換句話說,檢查程序認為程序中如果出現這樣的代碼:
fmt.Println("Hello!\n")
常常是由于程序編寫人員的筆誤。實際上,事實確實如此。如果我們確實想連續輸入多個換行,應該這樣寫:
fmt.Println("Hello!")
fmt.Println()
至此,我們詳細介紹了```go tool vet```命令中的檢查程序對打印函數的所有步驟和內容。打印函數的功能非常簡單,但是```go tool vet```命令對它的檢查卻很細致。從中我們可以領會到一些關于打印函數的最佳實踐。
**-rangeloops標記**
如果標記```-rangeloop```有效(標記值不為```false```),那么命令程序會對使用```range```進行迭代的```for```代碼塊進行檢查。我們之前提到過,使用```for```語句需要注意兩點:
1. 不要在```go```代碼塊中處理在迭代過程中被賦予值的迭代變量。比如:
mySlice := []string{"A", "B", "C"}
for index, value := range mySlice {
go func() {
fmt.Printf("Index: %d, Value: %s\n", index, value)
}()
}
在Go語言的并發編程模型中,并沒有線程的概念,但卻有一個特有的概念——Goroutine。Goroutine也可被稱為Go例程或簡稱為Go程。關于Goroutine的詳細介紹在第6章和第7章。我們現在只需要知道它是一個可以被并發執行的代碼塊。
2. 不要在```defer```語句的延遲函數中處理在迭代過程中被賦予值的迭代變量。比如:
myDict := make(map[string]int)
myDict["A"] = 1
myDict["B"] = 2
myDict["C"] = 3
for key, value := range myDict {
defer func() {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}()
}
其實,上述兩點所關注的問題是相同的,那就是不要在可能被延遲處理的代碼塊中直接使用迭代變量。```go```代碼塊和```defer```代碼塊都有這樣的特質。這是因為等到go函數(跟在```go```關鍵字之后的那個函數)或延遲函數真正被執行的時候,這些迭代變量的值可能已經不是我們想要的值了。
另一方面,當檢查程序發現在帶有```range```子句的```for```代碼塊中迭代出的數據并沒有賦值給標識符所代表的變量時,則會忽略對這一代碼塊的檢查。比如像這樣的代碼:
func nonIdentRange(slc []string) {
l := len(slc)
temp := make([]string, l)
l--
for _, temp[l] = range slc {
// 忽略了使用切片值temp的代碼。
if l > 0 {
l--
}
}
}
就不會受到檢查程序的關注。另外,當被迭代的對象的大小為```0```時,```for```代碼塊也不會被檢查。
據此,我們知道如果在可能被延遲處理的代碼塊中直接使用迭代中的臨時變量,那么就可能會造成與編程人員意圖不相符的結果。如果由此問題使程序的最終結果出現偏差甚至使程序報錯的話,那么看起來就會非常詭異。這種隱晦的錯誤在排查時也是非常困難的。這種不正確的代碼編寫方式應該徹底被避免。這也是檢查程序對迭代代碼塊進行檢查的最終目的。如果檢查程序發現了上述的不正確的代碼編寫方式,就會打印出錯誤信息以提醒編程人員。
**-structtags標記**
如果標記``-structtags```有效(標記值不為```false```),那么命令程序會對結構體類型的字段的標簽進行檢查。我們先來看下面的代碼:
type Person struct {
XMLName xml.Name `xml:"person"`
Id int `xml:"id,attr"`
FirstName string `xml:"name>first"`
LastName string `xml:"name>last"`
Age int `xml:"age"`
Height float32 `xml:"height,omitempty"`
Married bool
Address
Comment string `xml:",comment"`
}
在上面的例子中,在結構體類型的字段聲明后面的那些字符串形式的內容就是結構體類型的字段的標簽。對于Go語言本身來說,結構體類型的字段標簽就是注釋,它們是可選的,且會被Go語言的運行時系統忽略。但是,這些標簽可以通過標準庫代碼包```reflect```中的程序訪問到。因此,不同的代碼包中的程序可能會賦予這些結構體類型的字段標簽以不同的含義。比如上面例子中的結構體類型的字段標簽就對代碼包```encoding/xml```中的程序非常有用處。
嚴格來講,結構體類型的字段的標簽應該滿足如下要求:
1. 標簽應該包含鍵和值,且它們之間要用英文冒號分隔。
2. 標簽的鍵應該不包含空格、引號或冒號。
3. 標簽的值應該被英文雙引號包含。
4. 如果標簽內容符合了第3條,那么標簽的全部內容應該被反引號“`”包含。否則它需要被雙引號包含。
5. 標簽可以包含多個鍵值對,其它們之間要用空格“ ”分隔。例如:```key:"value" _gofix:"_magic"```
檢查程序首先會對結構體類型的字段標簽的內容做去引號處理,也就是把最外面的雙引號或者反引號去除。如果去除失敗,則檢查程序會打印錯誤信息并設置退出代碼為1,同時忽略后續檢查。如果去引號處理成功,檢查程序則會根據前面的規則對標簽的內容進行檢查。如果檢查出問題,檢查程序同樣會打印出錯誤信息并設置退出代碼為1。
**-unreachable標記**
如果標記``-unreachable```有效(標記值不為```false```),那么命令程序會在函數或方法定義中查找死代碼。死代碼就是永遠不會被訪問到的代碼。例如:
func deadCode1() int {
print(1)
return 2
println() // 這里存在死代碼
}
在上面示例中,函數```deadCode1```中的最后一行調用打印函數的語句就是死代碼。檢查程序如果在函數或方法中找到死代碼,則會打印錯誤信息以提醒編碼人員。我們把這段代碼放到命令源碼文件deadcode_demo.go中,并在main函數中調用它。現在,如果我們編譯這個命令源碼文件會馬上看到一個編譯錯誤:“missing return at end of function”。顯然,這個錯誤側面的提醒了我們,在這個函數中存在死代碼。實際上,我們在修正這個問題之前它根本就不可能被運行,所以也就不存在任何隱患。但是,如果在這個函數不需要結果的情況下又會如何呢?我們稍微改造一下上面這個函數:
func deadCode1() {
print(1)
return
println() // 這里存在死代碼
}
好了,我們現在把函數```deadcode1```的聲明中的結果聲明和函數中```return```語句后的數字都去掉了。不幸的是,當我們再次編譯文件時沒有看到任何報錯。但是,這里確實存在死代碼。在這種情況下,編譯器并不能幫助我們找到問題,而```go tool vet```命令卻可以。
hc@ubt:~$ go tool vet deadcode_demo.go
deadcode_demo.go:10: unreachable code
```go tool vet```命令中的檢查程序對于死代碼的判定有幾個依據,如下:
1. 在這里,我們把```return```語句、```goto```語句、```break```語句、```continue```語句和```panic```函數調用語句都叫做流程中斷語句。如果在當前函數、方法或流程控制代碼塊的分支中的流程中斷語句的后面還存在其他語句或代碼塊,比如:
func deadCode2() {
print(1)
panic(2)
println() // 這里存在死代碼
}
或
func deadCode3() {
L:
{
print(1)
goto L
}
println() // 這里存在死代碼
}
或
func deadCode4() {
print(1)
return
{ // 這里存在死代碼
}
}
則后面的語句或代碼塊就會被判定為死代碼。但檢查程序僅會在錯誤提示信息中包含第一行死代碼的位置。
2. 如果帶有```else```的```if```代碼塊中的每一個分支的最后一條語句均為流程中斷語句,則在此流程控制代碼塊后的代碼都被判定為死代碼。比如:
func deadCode5(x int) {
print(1)
if x == 1 {
panic(2)
} else {
return
}
println() // 這里存在死代碼
}
注意,只要其中一個分支不包含流程中斷語句,就不能判定后面的代碼為死代碼。像這樣:
func deadCode5(x int) {
print(1)
if x == 1 {
panic(2)
} else if x == 2 {
return
}
println() // 這里并不是死代碼
}
3. 如果在一個沒有顯式中斷條件或中斷語句的```for```代碼塊后面還存在其它語句,則這些語句將會被判定為死代碼。比如:
func deadCode6() {
for {
for {
break
}
}
println() // 這里存在死代碼
}
或
func deadCode7() {
for {
for {
}
break // 這里存在死代碼
}
println()
}
而我們對這兩個函數稍加改造后,就會消除```go tool vet```命令發出的死代碼告警。如下:
func deadCode6() {
x := 1
for x == 1 {
for {
break
}
}
println() // 這里存在死代碼
}
以及
func deadCode7() {
x := 1
for {
for x == 1 {
}
break // 這里存在死代碼
}
println()
}
我們只是加了一個顯式的中斷條件就能夠使之通過死代碼檢查。但是,請注意!這兩個函數中在被改造后仍然都包含死循環代碼!這說明檢查程序并不對中斷條件的邏輯進行檢查。
4. 如果```select```代碼塊的所有```case```中的最后一條語句均為流程中斷語句(```break```語句除外),那么在```select```代碼塊后面的語句都會被判定為死代碼。比如:
func deadCode8(c chan int) {
print(1)
select {
case <-c:
print(2)
panic(3)
}
println() // 這里存在死代碼
}
或
func deadCode9(c chan int) {
L:
print(1)
select {
case <-c:
print(2)
panic(3)
case c <- 1:
print(4)
goto L
}
println() // 這里存在死代碼
}
另外,在空的```select```語句塊之后的代碼也會被認為是死代碼。比如:
func deadCode10() {
print(1)
select {}
println() // 這里存在死代碼
}
或
func deadCode11(c chan int) {
print(1)
select {
case <-c:
print(2)
panic(3)
default:
select {}
}
println() // 這里存在死代碼
}
上面這兩個示例中的語句```select {}```都會引發一個運行時錯誤:“fatal error: all goroutines are asleep - deadlock!”。這就是死鎖!關于這個錯誤的詳細說明在第7章。
5. 如果```switch```代碼塊的所有```case```和```default case```中的最后一條語句均為流程中斷語句(除了```break```語句),那么在```switch```代碼塊后面的語句都會被判定為死代碼。比如:
func deadCode14(x int) {
print(1)
switch x {
case 1:
print(2)
panic(3)
default:
return
}
println(4) // 這里存在死代碼
}
我們知道,關鍵字```fallthrough```可以使流程從```switch```代碼塊中的一個```case```轉移到下一個```case```或```default case```。死代碼也可能由此產生。例如:
func deadCode15(x int) {
print(1)
switch x {
case 1:
print(2)
fallthrough
default:
return
}
println(3) // 這里存在死代碼
}
在上面的示例中,第一個case總會把流程轉移到第二個case,而第二個case中的最后一條語句為return語句,所以流程永遠不會轉移到語句```println(3)```上。因此,```println(3)```語句會被判定為死代碼。如果我們把```fallthrough```語句去掉,那么就可以消除這個死代碼判定。實際上,只要某一個```case```或者```default case```中的最后一條語句是break語句,就不會有死代碼的存在。當然,這個```break```語句本身不能是死代碼。另外,與```select```代碼塊不同的是,空的```switch```代碼塊并不會使它后面的代碼成為死代碼。
綜上所述,死代碼的判定雖然看似比較復雜,但其實還是有原則可循的。我們應該在編碼過程中就避免編寫可能會造成死代碼的代碼。如果我們實在不確定死代碼是否存在,也可以使用```go tool vet```命令來檢查。不過,需要提醒讀者的是,不存在死代碼并不意味著不存在造成死循環的代碼。當然,造成死循環的代碼也并不一定就是錯誤的代碼。但我們仍然需要對此保持警覺。
**-asmdecl標記**
如果標記``-asmdecl```有效(標記值不為```false```),那么命令程序會對匯編語言的源碼文件進行檢查。對匯編語言源碼文件及相應編寫規則的解讀已經超出了本書的范圍,所以我們并不在這里對此項檢查進行描述。如果讀者有興趣的話,可以查看此項檢查的程序的源碼文件asmdecl.go。它在Go語言安裝目錄的子目錄src/cmd/vet下。
至此,我們對```go vet```命令和```go tool vet```命令進行了全面詳細的介紹。之所以花費如此大的篇幅來介紹這兩個命令,不僅僅是為了介紹此命令的使用方法,更是因為此命令程序的檢查工作涉及到了很多我們在編寫Go語言代碼時需要避免的“坑”。由此我們也可以知曉應該怎樣正確的編寫Go語言代碼。同時,我們也應該在開發Go語言程序的過程中經常使用```go tool vet```命來檢查代碼。