[TOC]
截至目前,我們已經兩次提及“空白標識符”這個概念了,一次是在講[`for`?`range`?loops](http://www.hellogcc.org/effective_go.html#for)形式的循環時,另一次是在講[maps](http://www.hellogcc.org/effective_go.html#maps)結構時。空白標識符可以賦值給任意變量或者聲明為任意類型,只要忽略這些值不會帶來問題就可以。這有點像在Unix系統中向`/dev/null`文件寫入數據:它為那些需要出現但值其實可以忽略的變量提供了一個“只寫”的占位符。但正如我們之前看到的那樣,它實際的用途其實不止于此。
## 空白標識符在多賦值語句中的使用
空白標識符在`for`?`range`循環中使用的其實是其應用在多語句賦值情況下的一個特例。
一個多賦值語句需要多個左值,但假如其中某個左值在程序中并沒有被使用到,那么就可以用空白標識符來占位,以避免引入一個新的無用變量。例如,當調用的函 數同時返回一個值和一個error,但我們只關心error時,那么就可以用空白標識符來對另一個返回值進行占位,從而將其忽略。
~~~
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
~~~
有時,你也會發現一些代碼用空白標識符對error占位,以忽略錯誤信息;這不是一種好的做法。好的實現應該總是檢查返回的error值,因為它會告訴我們錯誤發生的原因。
~~~
// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}
~~~
## 未使用的導入和變量
如果你在程序中導入了一個包或聲明了一個變量卻沒有使用的話,會引起編譯錯誤。因為,導入未使用的包不僅會使程序變得臃腫,同時也降低了編譯效率;初始化 一個變量卻不使用,輕則造成對計算的浪費,重則可能會引起更加嚴重BUG。當一個程序處于開發階段時,會存在一些暫時沒有被使用的導入包和變量,如果為了 使程序編譯通過而將它們刪除,那么后續開發需要使用時,又得重新添加,這非常麻煩。空白標識符為上述場景提供了解決方案。
以下一段代碼包含了兩個未使用的導入包(`fmt`和`io`) 以及一個未使用的變量(`fd`),因此無法編譯通過。我們可能希望這個程序現在就可以正確編譯。
~~~
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
}
~~~
為了禁止編譯器對未使用導入包的錯誤報告,我們可以用空白標識符來引用一個被導入包中的符號。同樣的,將未使用的變量`fd`賦值給一個空白標識符也可以禁止編譯錯誤。這個版本的程序就可以編譯通過了。
~~~
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader // For debugging; delete when done.
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}
~~~
按照約定,用來臨時禁止未使用導入錯誤的全局聲明語句必須緊隨導入語句塊之后,并且需要提供相應的注釋信息 —— 這些規定使得將來很容易找并刪除這些語句。
## 副作用式導入
像上面例子中的導入的包,`fmt`或`io`,最終要么被使用,要么被刪除:使用空白標識符只是一種臨時性的舉措。但有時,導入一個包僅僅是為了引入一些副作用,而不是為了真正使用它們。例如,`net/http/pprof`包會在其導入階段調用`init`函數,該函數注冊HTTP處理程序以提供調試信息。這個包中確實也包含一些導出的API,但大多數客戶端只會通過注冊處理函數的方式訪問web頁面的數據,而不需要使用這些API。為了實現僅為副作用而導入包的操作,可以在導入語句中,將包用空白標識符進行重命名:
~~~
import _ "net/http/pprof"
~~~
這一種非常干凈的導入包的方式,由于在當前文件中,被導入的包是匿名的,因此你無法訪問包內的任何符號。(如果導入的包不是匿名的,而在程序中又沒有使用到其內部的符號,那么編譯器將報錯。)
## 接口檢查
正如我們在前面[接口](http://www.hellogcc.org/effective_go.html#interfaces_and_types)那章所討論的,一個類型不需要明確的聲明它實現了某個接口。一個類型要實現某個接口,只需要實現該接口對應的方法就可以了。在實際中,多數接口的類型轉換和檢查都是在編譯階段靜態完成的。例如,將一個`*os.File`類型傳入一個接受`io.Reader`類型參數的函數時,只有在`*os.File`實現了`io.Reader`接口時,才能編譯通過。
但是,也有一些接口檢查是發生在運行時的。其中一個例子來自`encoding/json`包內定義的`Marshaler`接口。當JSON編碼器接收到一個實現了Marshaler接口的參數時,就調用該參數的marshaling方法來代替標準方法處理JSON編碼。編碼器利用[類型斷言](http://www.hellogcc.org/effective_go.html#interface_conversions)機制在運行時進行類型檢查:
~~~
m, ok := val.(json.Marshaler)
~~~
假設我們只是想知道某個類型是否實現了某個接口,而實際上并不需要使用這個接口本身 —— 例如在一段錯誤檢查代碼中 —— 那么可以使用空白標識符來忽略類型斷言的返回值:
~~~
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
~~~
在某些情況下,我們必須在包的內部確保某個類型確實滿足某個接口的定義。例如類型`json.RawMessage`,如果它要提供一種定制的JSON格式,就必須實現`json.Marshaler`接口,但是編譯器不會自動對其進行靜態類型驗證。如果該類型在實現上沒有充分滿足接口定義,JSON編碼器仍然會工作,只不過不是用定制的方式。為了確保接口實現的正確性,可以在包內部,利用空白標識符進行一個全局聲明:
~~~
var _ json.Marshaler = (*RawMessage)(nil)
~~~
在該聲明中,賦值語句導致了從`*RawMessage`到`Marshaler`的類型轉換,這要求`*RawMessage`必須正確實現了`Marshaler`接口,該屬性將在編譯期間被檢查。當`json.Marshaler`接口被修改后,上面的代碼將無法正確編譯,因而很容易發現錯誤并及時修改代碼。
在這個結構中出現的空白標識符,表示了該聲明語句僅僅是為了觸發編譯器進行類型檢查,而非創建任何新的變量。但是,也不需要對所有滿足某接口的類型都進行這樣的處理。按照約定,這類聲明僅當代碼中沒有其他靜態轉換時才需要使用,這類情況通常很少出現。