截至目前,我们已经两次提及“空白标识符”这个概念了,一次是在讲for
range
loops形式的循环时,另一次是在讲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"
这一种非常干净的导入包的方式,由于在当前文件中,被导入的包是匿名的,因此你无法访问包内的任何符号。(如果导入的包不是匿名的,而在程序中又没有使用到其内部的符号,那么编译器将报错。)
正如我们在前面接口那章所讨论的,一个类型不需要明确的声明它实现了某个接口。一个类型要实现某个接口,只需要实现该接口对应的方法就可以了。在实际中,多数接口的类型转换和检查都是在编译阶段静态完成的。例如,将一个*os.File
类型传入一个接受io.Reader
类型参数的函数时,只有在*os.File
实现了io.Reader
接口时,才能编译通过。
但是,也有一些接口检查是发生在运行时的。其中一个例子来自encoding/json
包内定义的Marshaler
接口。当JSON编码器接收到一个实现了Marshaler接口的参数时,就调用该参数的marshaling方法来代替标准方法处理JSON编码。编码器利用类型断言机制在运行时进行类型检查:
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
接口被修改后,上面的代码将无法正确编译,因而很容易发现错误并及时修改代码。
在这个结构中出现的空白标识符,表示了该声明语句仅仅是为了触发编译器进行类型检查,而非创建任何新的变量。但是,也不需要对所有满足某接口的类型都进行这样的处理。按照约定,这类声明仅当代码中没有其他静态转换时才需要使用,这类情况通常很少出现。