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