通过关键字type和interface,我们可以声明出接口类型。接口类型的类型字面量与结构体类型的看起来有些相似,它们都用花括号包裹一些核心信息。只不过,结构体类型包裹的是它的字段声明,而接口类型包裹的是它的方法定义。
接口类型声明中的这些方法所代表的就是该接口的方法集合。一个接口的方法集合就是它的全部特征。对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征(即全部的方法),那么它就一定是这个接口的实现类型:
type Pet interface {
SetName(name string)
Name() string
Category() string
}
这里声明了一个接口类型Pet,它包含3个方法定义。这3个方法共同组成了接口类型Pet的方法集合。只要一个数据类型的方法集合中有3个方法,那么它就就一定是Pet接口类型的实现。这是一种无浸入式的接口实现方式。这种方式还有一个专有名词,叫“Duck typing”,中文常译作“鸭子类型”。
下面的是上一篇结尾的那个例子,不过Cat换成了Dog:
package main
import "fmt"
type Pet interface {
SetName(name string)
Name() string
Category() string
}
type Dog struct {
name string // 名字。
}
func (dog *Dog) SetName(name string) {
dog.name = name
}
func (dog Dog) Name() string {
return dog.name
}
func (dog Dog) Category() string {
return "dog"
}
func main() {
// 示例1。
dog := Dog{"little pig"}
_, ok := interface{}(dog).(Pet)
fmt.Printf("Dog implements interface Pet: %v\n", ok)
_, ok = interface{}(&dog).(Pet)
fmt.Printf("*Dog implements interface Pet: %v\n", ok)
fmt.Println()
// 示例2。
var pet Pet = &dog
fmt.Printf("This pet is a %s, the name is %q.\n",
pet.Category(), pet.Name())
}
声明的Dog有3个方法,其中2个是值方法Name和Category,还有一个指针方法SetName。Dog类型本身的方法集合中只有2个方法,就是所有的值方法。而它的指针类型*Dog方法集合包含了3个方法,就是它拥有Dog类型附带的所有值方法和指针方法。而这3个方法正好是Pet接口中,所以*Dog类型就成为了Pet接口的实现类型。
在上面,示例2的那一小段代码,把main主函数开头声明的Dog类型的变量dog,把它的指针赋值给了类型为Pet的变量pet。这里的变量pet的值,可以被叫做它的实际值(也称动态值)。该值的类型可以被叫做这个表量的实际类型(也称动态类型)。
动态类型的叫法是相对于静态类型而言的。对于变量pet,它的静态类型就是Pet,并且不会改变。但是他的动态会随着赋给他的动态值而变化。这里的动态类型是*Dog类型,而动态值就是&dog的值(就是dog的地址)。
下面的示例定义了简单的结构体和接口类型:
package main
import "fmt"
type Pet interface {
Name() string
}
type Dog struct {
name string
}
// 如果这是一个值方法?
func (d *Dog) SetName (name string) {
d.name = name
}
func (d Dog) Name() string {
return d.name
}
func main() {
dog := Dog{"Snoopy"}
fmt.Println(dog.Name())
var pet Pet = dog // 这个如果是一个取址表达式?
dog.SetName("Goofy ")
fmt.Println(dog.Name())
fmt.Println(pet.Name())
}
这里的SetName方法必须是指针方法。因为如果是值方法,接受者就是dog的副本,该方法改变的也只是副本的name的值,不会影响的dog变量本身。如果是指针方法,那么当SetName方法执行后,dog的name字典就就被改变了。
然后接着看接下来的一层。dog赋值给了pet,然后dog的name字段确实变了,但是这里pet里还是原来的值。这里的原因和上面的一样的。如果使用一个变量给另外一个变量赋值,那么真正赋值给后者的,其实是一个副本。这里如果是把&dog赋值给pet,那么pet的值也就会跟着dog的进行变化了。
上面可以这么理解,但是严格来讲,即使像前面那样把dog的值赋给了pet,pet的值与dog的值也是不同的。在给一个接口变量赋值的时候,该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中。无论从存储的内容还是存储的结构来看,pet的值与dog的值都是不同的。不过可以认为,此时的pet的值中包含了dog的值的副本。
这里要讨论的是接口变量在声明情况下才真正为nil:
package main
import "fmt"
type Pet interface {
Name() string
}
type Dog struct {
name string
}
func (d Dog) Name() string {
return d.name
}
func main() {
var dog *Dog
fmt.Println(dog)
fmt.Println(dog == nil) // true
var pet Pet = dog
// var pet Pet = nil // 注释掉上面的,试试这句
fmt.Println(pet)
fmt.Println(pet == nil) // false
// fmt.Printf("%T", pet) // 打印动态类型
这里先声明了一个*Dog类型的变量dog,并没有对他进行初始化。所以它的值就是nil。
然后把dog赋值给了接口类型pet,此时判断pet是否为nil时返回的false。
这里确实把值为nil的dog变量赋值给了pet。pet的动态值也确实是nil,但是pet的值不是nil。动态值只是pet值的一部分,还有动态类型。pet的动态类型是*Dog,可以通过fmt.Printf函数和占位符%T打印变量的类型。另外reflect包的TypeOf函数也可以起到类似的作用。
如果把nil直接赋值给pet的话,那么pet就是真正的nil了。在Go语言里,字面量nil表示的值叫做无类型的nil。这个是真正的nil,因为他的类型也是nil。而例子中,虽然dog的值是nil,但是当把这个变量赋值pet的时候,其实是赋值给pet的值是一个*Dog类型的nil值。对于接口变量,需要动态值和动态类型都是nil,那才是真正的nil。要想让一个接口变量的值真正为nil,可以把一个字面的nil赋值给它,或者只声明接口而不做初始化也可以。
接口类型的间的嵌入也被称为接口的组合。组合的接口之间不能有同名的方法存在,如果有同名的方法就会产生冲突而无法通过编译。与结构体类型间的嵌入很相似,只要把一个接口类型的名称直接写的另一个接口类型的成员列表中就可以进行组合:
type Animal interface {
ScientificName() string
Category() string
}
type Pet interface {
Animal
Name() string
}
组合后,Animal接口包含的所有方法也就是成为了Pet接口的方法。
Go语言团队鼓励我们声明体量较小的接口,并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。相比于包含很多方法的大接口而言,小接口可以更加专注地表达某一种能力或某一类特征,同时也更容易被组合在一起。善用接口组合和小接口可以让你的程序框架更加稳定和灵活。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。