EffectiveGo-5-空白标识符、内嵌

Parts of Effective Go

空白标识符

空白标识符可以被赋予或声明为任何类型的任何值,其值会被无害的丢弃
类似于Unix中的/dev/null,表示只写的值,在需要变量但不需要实际值的地方用作占位符
可使用空白标识符来丢弃无关的值

1
2
3
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
1
2
3
4
5
6
// 错误的方式
// 使用空白标识符忽略错误而丢弃错误值的代码
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}

未使用的导入和变量

若导入某个包或声明某个变量而不使用它就会产生错误
未使用的包会让程序膨胀并拖慢编译速度
已初始化但未使用的变量不仅会浪费计算能力,还可能暗藏更大的Bug
在开发过程中,经常会产生未使用的导入和变量
空白标识符可以忽略编译报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"io"
"log"
"os"
)
// For debugging; delete when done.
var _ = fmt.Printf
var _ io.Reader
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}

按照惯例,在导入并加以注释后,再使用全局声明导入错误静默,这样可是让它们更容易找到,并作为以后清理它的提醒

为副作用而导入

前例中fmtio这种未使用的导入总应在最后被使用或者移除
但有时导入某个包只是为了其副作用,而没有任何明确的使用
例如,在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.Marshsler)
若只需要判断某个类型是否实现了某个接口,而不需要实际使用接口本身(可能是错误检查部分),就使用空白标识符来忽略类型断言的值

1
2
3
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接口被更改,此包将无法通过编译,从而提示开发者注意到它需要更新

在这种结构中出现空白标识符,即表示该声明的存在只是为了类型检查
不要为满足接口就将它用于任何类型,作为约定,仅当代码中不存在静态类型转换时才能用这种声明,毕竟这是种罕见的情况

内嵌

Go并不提供典型的、类型驱动的子类化概念,但通过将类型内嵌到结构体或接口中,它就能“借鉴”部分实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.Reader
type Reader interface {
Read(p []byte) (n int, err error)
}
// io.Writer
type Writer interface {
Write(p []byte) (n int, err error)
}
// io包也导出了一些其它接口,以此说明对象所需实现的方法
// io.ReadWriter 就是个包含了 Reader接口 和 Writer接口 的接口
type ReadWriter interface {
Reader
Writer
}

ReadWriter能够做任何ReaderWriter可以做到的事情,它的内嵌接口的联合体(它们必须是不相交的方法集)
只有接口能被嵌入到接口中

同样的思考方法可以应用在结构体中,但其意义更加深远
bufio包中有bufio.Readerbufio.Writer这两哥结构体类型,它们每一个都实现了与io包中相同意义的接口
此外,bufio还通过结合reader/writer并将其内嵌到结构体中,实现了带缓冲的reader/writer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ReadWriter 存储了指向 Reader 和 Writer 的指针
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.Readerbufio.Writer的方法,还同时满足下列三个接口io.Reader io.Writer io.ReadWriter

当内嵌一个类型时,该类型的方法会成为外部类型的方法,但当它们被调用时,该方法的接收者是内部类型,而不是外部类型
bufio.ReadWriterRead方法被调用时,接收者是ReadWriterreader字段,而不是ReadWriter本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Job struct {
Command string
*log.Logger
}
// Job 类型现在有了 Log Logf *log.Logger
// 一旦初始化后,就能记录Job了
job.Log("starting now...")
// Logger 是 Job 结构体的常规字段
// 可在 Job 的构造函数中,通过一般方式来初始化
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
// 或者通过复合字段
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
// 若要直接引用内嵌字段
// 可以忽略包限定名
// 直接将该字段的类型名作为字段名
func (job *Job) Logf(format string, args... interface) {
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

内嵌类型会引入命名冲突的问题
字段或方法X会隐藏该类型中更深层嵌套的其它项X
log.Logger包含一个名为Commant的字段或方法,JobCommant字段会覆盖它
若相同的嵌套层级上出现同名冲突,通常会产生一个错误
Job结构体中包含为Logger的字段或方法,再将log.Logger内嵌到其中的话就会产生错误
若重名永远不会在该类型定义之外的程序中使用,那就不会出错
这种限定很够在外部嵌套类型发生修改时提供保护
因此,就算添加的字段与另一个子类型中的字段相冲突,只要这两个相同的字段永远不会被使用就没问题