高效的 Go 编程 Effective Go - 空白标识符
免费 - Sunrise - - 浏览量: 0 - 文章来源
我们在 for-range
循环和映射中提过几次空白标识符。 空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。它有点像Unix中的 /dev/null
文件:它表示只写的值,在需要变量但不需要实际值的地方用作占位符。 我们在前面已经见过它的用法了。
多个参数赋值中的空白标识符
for range
循环中对空表标识符的用法是一种具体情况,更一般的情况即为多个参数赋值。
若某次赋值需要匹配多个左值,但其中某个变量不会被程序使用, 那么用空白标识符来代替该变量可避免创建无用的变量,并能清楚地表明该值将被丢弃。 例如,当调用某个函数时,它会返回一个值和一个错误,但只有错误很重要, 那么可使用空白标识符来丢弃无关的值。
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
你偶尔会看见为忽略错误而丢弃错误值的代码,这是种糟糕的实践。请务必检查错误返回, 它们会提供错误的理由。
// 很糟糕的代码!若路径不存在,它就会崩溃。
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 // 用于调试,结束时删除。
var _ io.Reader // 用于调试,结束时删除。
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页面访问数据。只为了其辅助作用来导入该包, 只需将包重命名为空白标识符:
import _ "net/http/pprof"
这种导入格式能明确表示该包是为其辅助作用而导入的,因为没有其它使用该包的可能: 在此文件中,它没有名字。(若它有名字而我们没有使用,编译器就会拒绝该程序。)
接口检查
就像我们在前面接口中讨论的那样, 一个类型无需显式地声明它实现了某个接口。取而代之,该类型只要实现了某个接口的方法, 其实就实现了该接口。在实践中,大部分接口转换都是静态的,因此会在编译时检测。 例如,将一个 *os.File
传入一个预期的 io.Reader
函数将不会被编译, 除非 *os.File
实现了 io.Reader
接口。
尽管有些接口检查会在运行时进行。encoding/json 包中就有个实例它定义了一个 Marshaler 接口。当JSON编码器接收到一个实现了该接口的值,那么该编码器就会调用该值的编组方法, 将其转换为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
接口被更改,此包将无法通过编译, 而我们则会注意到它需要更新。
在这种结构中出现空白标识符,即表示该声明的存在只是为了类型检查。 不过请不要为满足接口就将它用于任何类型。作为约定, 只有当代码中不存在静态类型转换时才能使用这种声明,毕竟这是种非常罕见的情况。