空白符(blank identifier)
在Go中,空白符可以被赋予/声明为任何类型的任何值,其值可以被无害丢弃。就像python中的空白符一样,它表示一个只写值,在那些需要变量,但是变量的真实值无关紧要的地方被用作占位符。
多重赋值中的空白符
如果某赋值的左边要求有多个值,但是其中一个值不会被程序所使用到,那么,在该赋值的左边放一个空白符,可以避免创建一个哑变量,并且可以更清晰地表明该值将被丢弃。例如: 1
2
3
4// 下面调用中,只有err是重要的,因此使用空白符来丢弃无需用到的值
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
未使用的导入和变量
导入一个包或者声明一个变量,但是并不使用它们,在Go中,这是一种错误。但是,在代码频繁修改测试的情况下,常常会有未使用的导入和变量,而仅仅为了编译就把它们删掉是一件烦得不要不要的事情(而且可能稍后你还会用到它们)。
这个时候,空白符就闪亮登场了。
打个比方: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package main
import (
"fmt"
"io"
"log"
"os"
)
func main(){
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
//TODO: use fd
}fmt和io,一个未使用的变量fd。要让编译器对这些未使用的东西保持沉默,我们可以使用一个空白符指向导入包的一个符号。类似地,将未使用变量赋给空白符也会使得编译器对未使用变量错误保持沉默。修改后的版本如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package 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
}
为了副作用的导入
有时候,仅仅是为了某个包的副作用(无显式使用),而将这个包导入是很有用的(比如:导入一个包会运行这个包的init函数)。这个时候,我们可以这样做: 1
import _ "net/http/pprof"
但是,有时,有些接口检查确实是发生在运行时的。例如,encoding/json包中的Marshaler接口。当JSON编码器接收到一个实现了该接口的值时,编码器调用该值的marshaling方法来将其转换成JSON,而不是进行标准的转换。该编码器在运行时,使用诸如m, ok := val.(json.Marshaler)这样的类型断言来检查。
如果只是为了检查某个类型是否实现了某个接口,而不是为了实际上使用这个接口本身,也有可能是作为错误检查的一部分,我们可以使用空白符来忽略类型断言得到的值: 1
2
3if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}json.RawMessage需要一个自定义的JSON表示,那么它应该实现json.Marshaler。但是,并没有任何会触发编译器自动验证这种特征的静态转换。倘若该类型不小心没法满足这个接口的要求,那么JSON编码器仍然会工作,但是并不会使用自定义的实现。为了保证该实现是正确的,可以在包中使用一个使用了空白符的全局声明: 1
var _ json.Marshaler = (*RawMessage)(nil)
嵌入
Go并没有典型的类型驱动的子类概念,但是,通过在一个结构或者接口里嵌入类型,它提供了从某个实现“借点”东东的能力。
接口嵌入非常简单。例如: 1
2
3
4
5
6
7
8
9
10
11
12
13type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// 一个将Reader和Writer接口组合在一起的接口
// 这样,这个接口就可以做接口Reader和接口Writer可以做的事情啦
type ReadWriter interface {
Reader
Writer
}
相同的想法也可以应用到结构上。例如,bufio包有两个结构类型:bufio.Reader和bufio.Writer(当然,这两个接口实现了包io中类似的方法)。它还实现了一个reader/writer,这是通过使用嵌入,将一个Reader和一个Writer组合到一个结构中实现的:在结构中列出类型,但不为它们提供字段名 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 存储指向Reader和Writer的指针
// 实现了io.ReadWriter
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
/*
也可以写成:
type ReadWriter struct {
reader *Reader
writer *Writer
}
但是,要使用字段的方法并满足io接口,我们还需要额外提供一个像这样的方法:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
*/bufio.ReadWriter不仅仅拥有bufio.Reader和bufio.Writer的方法,它还满足所有三个接口:io.Reader, io.Writer和io.ReadWriter。
但是,嵌入和子类有一个非常重要的区别。当我们嵌入一个类型时,那个类型的方法就变成了外部类型的方法了。但当方法被调用的时候,方法的接收者是内部类型,而不是外部类型。以上面的例子为例,当bufio.ReadWriter的Read方法被调用的时候,就像上面第二种实现一样,方法的接收者是ReadWriter的reader字段,而不是ReadWriter自身。
如果我们想直接引用嵌入字段,那么,字段的类型名(忽略包限定符)作为字段名。 1
2
3
4
5type Job struct {
Command string
*log.Logger
}
// 这里,如果我们想要访问Job的一个实例job的*log.Logger,我们可以写成job.LoggerX将该类型更深的嵌套部分中的其他X字段隐藏起来。以上面Job的定义为例,如果log.Logger包含了一个名为Command的字段或者方法,那么,Job的Command字段将会占优势 2. 如果在同个嵌套级别上有相同的名字,那么,这通常是一个错误。如果Job结构包含其他也叫作Logger的字段或者方法,那么,嵌入log.Logger则是一种错误。但是,如果在类型定义之外,程序里绝不提及这重复的名字,那么没问题。(这样的话,即使被嵌套的类型定义修改从而导致了这种错误,那么也不会有什么问题,只要不使用即可)