好好学习,天天向上

Go, go|小白的go小抄:空白符和嵌入

空白符(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)
}
> 有时候,你也会在代码中看到error被丢弃,从而忽略该错误。这是一种很糟糕的做法。好孩子要经常检查返回的error哦。error的存在总是有其原因的。(*^__^*)

未使用的导入和变量

导入一个包或者声明一个变量,但是并不使用它们,在Go中,这是一种错误。但是,在代码频繁修改测试的情况下,常常会有未使用的导入和变量,而仅仅为了编译就把它们删掉是一件烦得不要不要的事情(而且可能稍后你还会用到它们)。

这个时候,空白符就闪亮登场了。

打个比方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"io"
"log"
"os"
)

func main(){
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
//TODO: use fd
}
在这个例子中,有两个未使用的导入fmtio,一个未使用的变量fd。要让编译器对这些未使用的东西保持沉默,我们可以使用一个空白符指向导入包的一个符号。类似地,将未使用变量赋给空白符也会使得编译器对未使用变量错误保持沉默。修改后的版本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}
> 通常来讲,用以让导入错误沉默的全局声明应该紧挨在导入后面,并且附上注释。这会让它们易于被发现,并且作为稍后清理的提醒。

为了副作用的导入

有时候,仅仅是为了某个包的副作用(无显式使用),而将这个包导入是很有用的(比如:导入一个包会运行这个包的init函数)。这个时候,我们可以这样做:

1
import _ "net/http/pprof"
#### 接口检查 在前面的文章中,我们可以看到,一个类型无需显式声明它实现了某个接口。只要这个类型实现了那个接口的方法,那么,我们就可以说,这个类实现了那个接口。实际上,大多数的接口转换是静态的,因此,会在编译时检查。

但是,有时,有些接口检查确实是发生在运行时的。例如,encoding/json包中的Marshaler接口。当JSON编码器接收到一个实现了该接口的值时,编码器调用该值的marshaling方法来将其转换成JSON,而不是进行标准的转换。该编码器在运行时,使用诸如m, ok := val.(json.Marshaler)这样的类型断言来检查。

如果只是为了检查某个类型是否实现了某个接口,而不是为了实际上使用这个接口本身,也有可能是作为错误检查的一部分,我们可以使用空白符来忽略类型断言得到的值:

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编码器仍然会工作,但是并不会使用自定义的实现。为了保证该实现是正确的,可以在包中使用一个使用了空白符的全局声明:
1
var _ json.Marshaler = (*RawMessage)(nil)
这种写法暗示,声明的存在只是为了进行类型检查,而不是要创建一个变量。注意,不要拿着锤子看啥都是钉子。一般来说,这样子的声明只用在代码中没有任何静态转换的情况下。

嵌入

Go并没有典型的类型驱动的子类概念,但是,通过在一个结构或者接口里嵌入类型,它提供了从某个实现“借点”东东的能力。

接口嵌入非常简单。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}
// 一个将ReaderWriter接口组合在一起的接口
// 这样,这个接口就可以做接口Reader和接口Writer可以做的事情啦
type ReadWriter interface {
Reader
Writer
}
> 只有接口可以被嵌入到接口中。

相同的想法也可以应用到结构上。例如,bufio包有两个结构类型:bufio.Readerbufio.Writer(当然,这两个接口实现了包io中类似的方法)。它还实现了一个reader/writer,这是通过使用嵌入,将一个Reader和一个Writer组合到一个结构中实现的:在结构中列出类型,但不为它们提供字段名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 存储指向ReaderWriter的指针
// 实现了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.Readerbufio.Writer的方法,它还满足所有三个接口:io.Reader, io.Writerio.ReadWriter

但是,嵌入和子类有一个非常重要的区别。当我们嵌入一个类型时,那个类型的方法就变成了外部类型的方法了。但当方法被调用的时候,方法的接收者是内部类型,而不是外部类型。以上面的例子为例,当bufio.ReadWriterRead方法被调用的时候,就像上面第二种实现一样,方法的接收者是ReadWriterreader字段,而不是ReadWriter自身。

如果我们想直接引用嵌入字段,那么,字段的类型名(忽略包限定符)作为字段名。

1
2
3
4
5
type Job struct {
Command string
*log.Logger
}
// 这里,如果我们想要访问Job的一个实例job的*log.Logger,我们可以写成job.Logger
嵌入类型会引进命名冲突问题,但是解决这个问题的规则也很简单。 1. 字段/方法X将该类型更深的嵌套部分中的其他X字段隐藏起来。以上面Job的定义为例,如果log.Logger包含了一个名为Command的字段或者方法,那么,JobCommand字段将会占优势 2. 如果在同个嵌套级别上有相同的名字,那么,这通常是一个错误。如果Job结构包含其他也叫作Logger的字段或者方法,那么,嵌入log.Logger则是一种错误。但是,如果在类型定义之外,程序里绝不提及这重复的名字,那么没问题。(这样的话,即使被嵌套的类型定义修改从而导致了这种错误,那么也不会有什么问题,只要不使用即可)

参考

请言小午吃个甜筒~~