一个接口类型的值被分两部分:一个具体类型(动态类型)和该类型的一个值(动态值)。在Go中类型只是编译时的一个概念,所以类型不是一个值。

在以下四个语句中w有着三个不同的值(最初和最后是同一个值)。在Go中接口引用类型(slice、指针、map、通道、函数)如果只声明,没有初始化的话,其值是nil。如果数组或者结构体这样的复合类型,零值是其所有元素所有成员的零值。

var w io.Writer

w = os.Stdout

w = new(bytes.Buffer)

w = nil

对接口来说它的的零值就是把它的动态类型和值都设置成nil。一个接口值是否为nil取决于动态类型,所以现在这是一个nil接口值。可以用w == nil或者w != nil来检测该接口值是否为nil,如果调用一个nil接口的任何方法都会导致崩溃。

w.Write([]byte("Xonlab")) // 崩溃;对空指针取引用

第二个语句把*os.File类型的值赋给了w

这次赋值把具体类型隐式转换为了接口类型,它的显示转化形式是io.Write(os.Stdout)。不论这种转换是显式还是隐式,它都可以转换操作数的_类型值_。接口值的动态类型会设置为指针类型*os.File的类型描述符,它的动态值设置为os.Stdout的副本,即一个指向代表进程的标准输出的os.File类型的指针。

调用该接口值的Write方法,会实际调用(*os.File).Write方法,即输出Xonlab

一般来讲,在编译时我们无法知道一个接口值的动态类型是什么,所以通过接口来做调用必然需要使用动态分发。编译器必须生成一段代码来从类型描述符拿到名为Write的方法地址,再间接调用该方法地址。调用的接受者就是接口值的动态值,即os.Stdout所以实际效果与直接调用等价:

os.Stdout.Write([]byte("Xonlab"))

第三个语句把一个*bytes.Buffer类型赋给了接口值:

w = new(byte.Buffer)

动态类型现在是*bytes.Buffer,动态值现在则是一个指向新分配缓冲区的指针

现在调用的是(*bytes.Buffer).Write方法,方法的接受者是缓冲区的地址。调用该方法把字符追加到了缓冲区。

接口值可以用==!=操作来做比较。如果两个接口值都是nil或者二者的动态类型、动态值都相同的话二者相等(使用动态类型的==来做比较)。因为接口值是可比较的,所以它们可以作为map的键,也可以作为switch的操作数。前面提到了动态值是用==来比较的,假如动态值不可比较(Slice)则程序会崩溃.

1
2
var x interface{} = []int{1,2,3}
fmt.Println(x == x) // 宕机:试图比较不可比较的类型 []int

含有空指针的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const debug = true

func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // 启用输出收集
}
f(buf)
if debug {
//...
}
}

//如果out不是nil,向其写入输出的数据
func f(out io.Writer){
//...
if out != nil {
out.Write([]byte("done!"\n))
}
}

当设置debug为false是程序会崩溃。

1
2
3
if out != nil {
out.Write([]byte("done!"\n)) //空指针
}

因为第4行声明了buf,debug为false不会给buf内存地址,但是17行判空的时候,它是认为out != nil的,因为它判断的是动态类型是否为nil而不是判断动态值是否为nil,此时的buf就是一个含有空指针的接口

如前所述,动态分发机制决定了我们一定会调用(*bytes.Buffer).Write,只不过这次接受者为空。对于一些类型,比如os.File,空接受值是合法的,但对于*bytes.Buffer不行。所以一旦方法被调用就一定会空指针。

最关键的问题在于,尽管一个空的*bytes.Buffer指针拥有的方法满足了该接口,但它不满足接口所需的一些行为。特别是,这个调用违背了(*bytes.Buffer).Write的一个隐式的前置条件,即接受者不能为空,所以把空指针赋给这个接口就是一个错误。解决方案就是把buf的类型修改为io.Writer,从而避免在最开始的时候吧一个功能不完整的值赋给一个接口。

1
2
3
4
5
var buf io.Writer
if debug {
buf = new(bytes.Buffer)
}
f(buf)