一个接口类型的值被分两部分:一个具体类型(动态类型)和该类型的一个值(动态值)。在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 | var x interface{} = []int{1,2,3} |
含有空指针的接口
1 | const debug = true |
当设置debug为false是程序会崩溃。
1 | if out != nil { |
因为第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 | var buf io.Writer |