接口类型与其他数据类型不同,它没法被实例化。既不能通过调用new()
或make()
函数创建出一个接口类型的值,也无法用字面量来表示一个接口类型的值。
对于某一个接口类型,如果没有任何数据类型可以作为它的实现,那么该接口的值就不可能存在。
接口类型的类型字面量与结构体类型看起来相似,用花括号包裹一些核心信息:
接口类型声明中的方法代表的就是该接口的方法集合,一个接口的方法集合就是它的全部特征。
对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征,那么它就一定是这个接口的实现类型,如下所示:
type Pet interface {
SetName(name string)
Name() string
Category() string
}
// 只要一个数据类型的方法集合中有上述3个方法,那它就是Pet接口的实现类型
// 这是一种无侵入式的接口实现方式
判断一个数据类型的某个方法实现的是某个接口类型中的方法:
dog := Dog{"little pig"}
var pet Pet = &dog
// &dog是动态值,*dog是动态类型
// pet的类型是静态类型
pet的静态类型永远不变,而动态类型会随着赋值的变化而变化。在给一个接口类型的变量赋予实际值之前,它的动态类型是不存在的。
如果使用一个变量给另外一个变量赋值,那么真正赋值给后者的,并不是前者持有的那个值,而是该值的一个副本。
接口类型本身是无法被值化的,在赋予接口变量动态值之前,它的值一定是nil
(它的零值)。当给接口变量赋值时,该变量的动态类型和动态值一起被存储在一个专用的数据结构(iface)中。这个接口变量的值其实是这个专用数据结构的实例,而不是赋予给接口变量的那个动态值。
所以接口变量与动态值肯定是不同的,无论是存储的内容还是存储的结构都不同。
专用数据结构(iface)实例会包含两个指针,一个是指向类型信息(这里的类型信息有另一个专用数据结构的实例承载,包含动态类型以及使他实现了接口的方法和调用它们的途径)的指针,另一个是指向动态值的指针。
var dog1 *Dog // 声明*Dog类型的变dog1,没有初始化,dog1的值为nil
fmt.Println("The first dog is nil. ")
dog2 := dog1 // dog2的值也为nil
fmt.Println("The second dog is nil. ")
var pet Pet = dog2 // pet的动态值不为nil,pet的动态类型为*Dog
if pet == nil {
fmt.Println("The pet is nil. ")
} else {
fmt.Println("The pet is not nil. ")
}
// dog2的值是真正的nil,把dog2赋值给pet时,Go语言把值和类型放在一起考虑
// 因此pet的动态值不是nil
// Go语言识别出赋予pet的值是一个*Dog类型的nil值,
// Go语言用iface的实例包装它,包装后pet的值肯定不是nil
在Go语言中,把字面量为nil表示的值叫做无类型的nil,这是真正的nil,它的类型也是nil。
把一个有类型的nil值赋给接口变量,那么这个变量的值一定不会是那个真正的nil。
让接口变量的值为真正的nil的方法:
接口类型之间的嵌入称为接口的组合,与结构体类型的组合相比,接口类型组合不会出现方法之间的屏蔽。只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译,即使同名方法的签名不同也不行。
与结构体组合相似,把接口类型的名称直接写到另一个接口类型的成员类别中,如下所示:
type Animal interface {
ScientificName() string
Category() string
}
type Pet interface {
Animal // 嵌入
Name() string
}
// Animal接口包含的所以方法成为了Pet接口的方法
Go语言团队鼓励声明体量较小的接口,并建议通过接口组合来扩展程序、增加程序的灵活性。相比于包含很多方法的大接口,小接口可以专用地表达某一种能力或某一类特征,也更容易被组合在一起。