Go的接口类型
文章目录
一、接口类型
1.1 接口的实现类型
对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征(即全部的方法),那么它就一定是这个接口的实现类型。
这是一种无侵入的接口实现方式,这种方式还有一个专有名词,叫“Duck typing”,中文常译做鸭子类型。
“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
1.2 判定实现了接口类型的某个方法
怎么判定一个数据类型的某一个方法实现的就是某个接口类型中的某个方法呢?
- 两个方法的方法签名要完全一致;
- 两个方法的名称要一模一样;
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() {
// 示例一
dog := Dog{"little dog"}
_, ok := interface{}(dog).(Pet)
fmt.Printf("dog 实现了 Pet类型: %v\n", ok)
_, ok = interface{}(&dog).(Pet)
fmt.Printf("*dog 实现了 Pet类型:%v\n", ok)
fmt.Println()
// 示例二:
var pet Pet = &dog
fmt.Printf("这个pet是%s, 名字是:%v\n", pet.Category(), pet.Name())
}
1.3 接口类型的:静态类型、动态类型、动态值(也就是实际值)
在上面的代码中:
var pet Pet = &dog
对于变量pet,它的静态类型为Pet,并且永远是Pet。它的动态类型随着我们赋予它的动态值而变化。
我们把赋给它的值叫做它的实际值(也叫动态值),而该值的实际类型可以叫做这个变量的实际类型(也称动态类型)。
我们在给一个接口类型的变量赋予实际的值之前,它的动态类型是不存在的。
二、给一个接口变量赋值
package main
import "fmt"
type Pet interface {
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() {
// 示例一
dog := Dog{"little dog"}
fmt.Printf("dog 的名称是 %v\n", dog.Name())
var pet Pet = dog
dog.SetName("moster")
fmt.Printf("dog 名称为:%v\n", dog.Name())
fmt.Printf("pet 是一个:%v, 它的名字是:%v\n", pet.Category(), pet.Name())
fmt.Println()
}
2.1 上面示例一中dog的名字变了,为何pet的名字没有变
一个通用的规则:如果我们使用一个变量给另一个变量赋值,那么真正赋值给后者的,并不是前者持有的那个值,而是该值的一个副本。
dog1 := Dog{"little pig"}
dog2 := dog1
dog1.name = "moster"
上面这段代码是那条通用规则的又一个体现。
但是只用上面的通用规则进行回答,那就只回答了一半。另一半要从接口类型值的存储方式和结构说起。
- 接口类型本身是无法被值化的。我们在赋予它实际的值之前,它的值一定会是nil,这也是它的零值。
当我们给一个接口变量赋值的时候,该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中。
这个专用的数据结构叫做iface,iface的实例会包含两个指针,一个是指向类型信息的指针,另一个是指向动态值的指针。这里的类型信息是由另一个专用数据结构的实例承载的,其中包含了动态值的类型,以及使它实现了接口的方法和调用它们的途径,等等。
2.2 接口变量的值在什么情况下才真正为nil?
只要我们把一个有类型的nil赋值给接口变量,那么这个变量的值就一定不会是那个真正的nil。
package main
import (
"fmt"
"reflect"
)
type Pet interface {
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() {
// 示例一
var dog1 *Dog
fmt.Println("dog1 是 nil")
dog2 := dog1
if dog2 == nil {
fmt.Println("dog2 是 nil")
} else {
fmt.Println("dog2 不是 nil")
}
var pet Pet = dog2
if pet == nil {
fmt.Println("pet 是nil")
} else {
fmt.Println("pet 不是 nil")
}
fmt.Printf("pet 的类型是 %T\n", pet)
fmt.Printf("pet 的类型是 %s\n", reflect.TypeOf(pet).String())
fmt.Printf("dog1 的类型是 %T\n", dog1)
fmt.Printf("dog2 的类型是 %T\n", dog2)
fmt.Println()
}
对于上面的例子,GO语言会识别出赋予pet的值是一个*Dog类型的nil,然后Go语言就会用一个iface实例包装它,包装后的产物肯定就不是nil了。
三、实现接口间的组合
接口类型间的嵌入也称为接口的组合。
只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译,即使同名方法的签名彼此不同也会产生冲突。因此,接口的组合根本不可能导致屏蔽现象的出现。
type Animal interface {
ScientificName() string
Category() string
}
type Named interface {
Name() string
}
type Pet interface {
Animal
Named
}
GO语言团队鼓励我们声明体量小的接口,并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。
这是因为相比于包含很多方法的大接口而言,小接口可以更专注于表达某一种能力或某一类特性,同时也更容易被组合在一起。