接口类型不会和特定实现细节绑定在一起,都是其他类型实现了接口,而且实现接口也很灵活,只要实现了接口定义的方法即可。这样可以创建一个新的接口类型满足已经存在的具体类型,但是不会改变这些类型的定义,因为只需要定义新的接口的方法符合已经存在的具体类型已经实现的方法就可以了。

接口约定

具体类型可以通过它的方法提供额外的行为操作,当你得到一个具体类型就知道它是什么以及它可以干什么。

但是接口类型是抽象的类型,因为你不知道它是什么,只知道它可以干什么,因为它自己定义了一系列的方法。

接口类型定义了一种约定,约定需要调用者提供具体类型的值,但是这个具体类型实现了接口定义的方法,即满足这个接口。那么就可以正常工作,因为可以正常调用接口的方法。感觉和Python中很相似,就是不管你是什么,只要你有这个方法就行了,鸭子类型。走路看起来像鸭子,叫起来也像鸭子,那么就是个鸭子,因为我只需要走路和叫这两种方法。。

接口类型

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是接口类型的实例。

1
2
3
4
5
6
7
type Reader interface {
Read(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}

实现接口的条件

一个类型如果有一个接口需要的所有方法,那么这个类型就实现了这个接口。

对于每一个命名过的具体类型T,一些方法的接收者是类型T本身,一些是指针。那么对于T类型的变量上调用T的方法是可以的,因为编译器会隐式的获取它的地址。但是因为T类型的值没有*T指针的方法,因为它就只实现了更少的接口(缺少一部分方法的实现)。比如这样

1
2
3
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method 因为类型T的值没有实现相应方法
s.String() // 可以调用成功因为编译器隐式的获取它的地址

将一个具体类型赋值给接口,会导致只有接口类型暴露的方法可以被调用,所以接口类型封装和隐藏了具体类型和它的值。比如对于interface{}空接口来说,其没有定义任何方法,所以不能直接对它持有的值做操作,但是可以使用类型断言来获取interface{}中值的方法。

接口实现取决于接口定义的方法和具体类型实现的方法是否匹配,所以没有必要定义具体类型和它实现接口之间的关系。只要实现了方法,就满足接口。

同时对于bytes.Buffer来说是引用类型,因此其零值会是nil,即nil指针,类型是bytes.Buffer,而且通过(*bytes.Buffer)(nil)也可以进行显式的转换,那么这个nil也实现了io.Writer接口。

同样除了指针类型满足接口,同样也可以被其他引用类型实现。比如slice, map都可以实现接口。

接口值

Go中,变量总会被一个定义明确的值初始化,一个接口的零值就是它的类型和值的部分是nil。一个接口值基于它的动态类型被描述为空或者非空,那么刚开始声明的接口值就是空的接口值,比如

1
2
var w io.Writer
w == nil // true

如果执行了w = os.Stdout那么这个赋值过程调用了一个具体类型到接口类型的隐式转换,和io.Writer(os.Stdout)是等价的,而且都会刻画出操作的类型和值。那么接口值的动态类型就是*os.File,动态值是os.Stdout的拷贝。这样调用w.Write()就相当于(*os.File).Write()

两个接口值相等当且仅当它们都是nil值或者它们的动态类型相同并且值也根据动态类型来判断是相等的。如果两个值的动态类型是一样的,但是动态类型是不可比较的,那么就不能比较。

可以使用反射来获取接口动态类型的名称。

一个不包含值的nil接口值和一个刚好包含nil指针的接口值是不同的。因为包含nil指针的接口值是有类型的,只是包含空指针值的非空接口。

类型断言

类型断言是一个使用在接口值上的操作,x.(T),x表示一个接口的类型,T表示一个类型。一个类型断言检查它的操作对象的动态类型是否和断言的类型匹配。

如果T是一个具体类型,那么检查x的动态类型是否和T相同,如果成功了,类型断言的结果是x的类型是T,就可以从操作对象中获取具体的值。

如果T是一个接口类型,那么成功之后就会改变可以获取的方法集合,通常是更大。但是动态值没法获取到。