接口
1 什么是接口?
在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。
在Go语言中接口(interface)是一种类型,一种抽象的类型。相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”,接口类型更注重“我能做什么”的问题。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。
在 Go 语言中,接口就是方法签名(Method Signature)的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。这与面向对象编程(OOP)的说法很类似。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。
例如,WashingMachine
是一个含有 Cleaning()
和 Drying()
两个方法的接口。任何定义了 Cleaning()
和 Drying()
的类型,都称它实现了 WashingMachine
接口。
2 接口的声明与实现
2.1 接口的声明
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er
,如有写操作的接口叫Writer
,有关闭操作的接口叫closer
等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子,定义一个包含Write
方法的Writer
接口。
type Writer interface{
Write([]byte) error
}
当你看到一个Writer
接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write
方法来做一些事情。
2.2 实现接口的条件
接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
我们定义的Singer
接口类型,它包含一个Sing
方法。
// Singer 接口
type Singer interface {
Sing()
}
我们有一个Bird
结构体类型如下。
type Bird struct {}
因为Singer
接口只包含一个Sing
方法,所以只需要给Bird
结构体绑定一个Sing
方法就可以满足Singer
接口的要求。
// Sing Bird类型的Sing方法
func (b Bird) Sing() {
fmt.Println("汪汪汪")
}
这样就称为Bird
实现了Singer
接口。
让我们编写一段代码,完整的来实现一个接口。
package main
import (
"fmt"
)
//声明接口
type VowelsFinder interface {
FindVowels() []rune
}
//声明类型
type MyString string
//为MyString类型声明方法
func (ms MyString) FindVowels() []rune {
var vowels []rune //声明[]rune类型切片
for _, rune := range ms {
if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
vowels = append(vowels, rune)
}
}
return vowels
}
func main() {
name := MyString("Sam Anderson") //name是MyString类型变量
var v VowelsFinder //v是VowelsFinder接口变量
v = name //把name赋值给v,MyString 实现了 VowelsFinder 接口,因此赋值是合法的,证明接口是一种类型
fmt.Printf("Vowels are %c", v.FindVowels())
}
在上面程序的第 8 行,创建了一个名为 VowelsFinder
的接口,该接口有一个 FindVowels() []rune
的方法。
在接下来的一行,我们创建了一个 MyString
类型。
在第 15 行,我们给接受者类型(Receiver Type) MyString 添加了方法 FindVowels() []rune。现在,我们称 MyString 实现了 VowelsFinder 接口。这就和其他语言(如 Java)很不同,其他一些语言要求一个类使用 implement 关键字,来显式地声明该类实现了接口。而在 Go 中,并不需要这样。如果一个类型包含了接口中声明的所有方法,那么它就隐式地实现了 Go 接口。
在第 28 行,v
的类型为 VowelsFinder
,name
的类型为 MyString
,我们把 name
赋值给了 v
。由于 MyString
实现了 VowelFinder
,因此这是合法的。在下一行,v.FindVowels()
调用了 MyString
类型的 FindVowels
方法,打印字符串 Sam Anderson
里所有的元音。该程序输出 Vowels are [a e o]
。
3 接口的实际用途
前面的例子教我们创建并实现了接口,但还没有告诉我们接口的实际用途。在上面的程序里,如果我们使用 name.FindVowels()
,而不是 v.FindVowels()
,程序依然能够照常运行,但接口并没有体现出实际价值。
因此,我们现在讨论一下接口的实际应用场景。
我们编写一个简单程序,根据公司员工的个人薪资,计算公司的总支出。为了简单起见,我们假定支出的单位都是美元。
package main
import (
"fmt"
)
type SalaryCalculator interface {
CalculateSalary() int
}
type Permanent struct {
empId int
basicpay int
pf int
}
type Contract struct {
empId int
basicpay int
}
//salary of permanent employee is sum of basic pay and pf
func (p Permanent) CalculateSalary() int {
return p.basicpay + p.pf
}
//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {
return c.basicpay
}
/*
total expense is calculated by iterating though the SalaryCalculator slice and summing
the salaries of the individual employees
*/
func totalExpense(s []SalaryCalculator) {
expense := 0
for _, v := range s {
expense = expense + v.CalculateSalary()
}
fmt.Printf("Total Expense Per Month $%d", expense)
}
func main() {
pemp1 := Permanent{1, 5000, 20}
pemp2 := Permanent{2, 6000, 30}
cemp1 := Contract{3, 3000}
employees := []SalaryCalculator{pemp1, pemp2, cemp1}//Permanent和Contract都实现了接口,因此可以添加到接口类型的切片中
totalExpense(employees)
}
上面程序的第 7 行声明了一个 SalaryCalculator
接口类型,它只有一个方法 CalculateSalary() int
。
在公司里,我们有两类员工,即第 11 行和第 17 行定义的结构体:Permanent
和 Contract
。长期员工(Permanent
)的薪资是 basicpay
与 pf
相加之和,而合同员工(Contract
)只有基本工资 basicpay
。在第 23 行和第 28 行中,方法 CalculateSalary
分别实现了以上关系。由于 Permanent
和 Contract
都声明了该方法,因此它们都实现了 SalaryCalculator
接口。
第 36 行声明的 totalExpense
函数体现出了接口的妙用。该函数接收一个 SalaryCalculator
接口的切片([]SalaryCalculator
)作为参数。在第 49 行,我们向 totalExpense
函数传递了一个包含 Permanent
和 Contact
类型的切片。在第 39 行中,通过调用不同类型对应的 CalculateSalary
方法,totalExpense
可以计算得到支出。
这样做最大的优点是:totalExpense
可以扩展新的员工类型,而不需要修改任何代码。假如公司增加了一种新的员工类型 Freelancer
,它有着不同的薪资结构。Freelancer
只需传递到 totalExpense
的切片参数中,无需 totalExpense
方法本身进行修改。只要 Freelancer
也实现了 SalaryCalculator
接口,totalExpense
就能够实现其功能。
该程序输出 Total Expense Per Month $14050
。
4 为什么要使用接口?
为了更好的理解接口,我们以简单的例子来进行分析。
现在假设我们的代码世界里有很多小动物,下面的代码片段定义了猫和狗,它们饿了都会叫。
package main
import "fmt"
type Cat struct{}
func (c Cat) Say() {
fmt.Println("喵喵喵~")
}
type Dog struct{}
func (d Dog) Say() {
fmt.Println("汪汪汪~")
}
func main() {
c := Cat{}
c.Say()
d := Dog{}
d.Say()
}
这个时候又跑来了一只羊,羊饿了也会发出叫声。
type Sheep struct{}
func (s Sheep) Say() {
fmt.Println("咩咩咩~")
}
我们接下来定义一个函数,函数传入一个动物对象,对象调用Say()方法就会触发叫声。
// MakeCatHungry 猫饿了会喵喵喵~
func MakeCatHungry(c Cat) {
c.Say()
}
// MakeSheepHungry 羊饿了会咩咩咩~
func MakeSheepHungry(s Sheep) {
s.Say()
}
接下来会有越来越多的小动物跑过来,我们的代码世界该怎么拓展呢?如果每个小动物都定义一个这样的函数,代码冗余和重复。
在这个函数下,我们可不可以把所有动物都当成一个“会叫的类型”来处理呢?当然可以!使用接口类型就可以实现这个目标。 我们的代码其实并不关心究竟是什么动物在叫,我们只是在代码中调用它的Say()
方法,这就足够了。
我们可以定义一个Sayer
类型,它必须实现一个Say()
方法,只要饿肚子了,我们就调用Say()
方法。
type Sayer interface {
Say()
}
然后我们定义一个通用的MakeHungry
函数,接收Sayer
类型的参数。
// MakeHungry 饿肚子了...
func MakeHungry(s Sayer) {
s.Say()
}
我们通过使用接口类型,把所有会叫的动物当成Sayer
类型来处理,只要实现了Say()
方法都能当成Sayer
类型的变量来处理。
package main
import "fmt"
type Cat struct{}
func (c Cat) Say() string{
return "喵喵喵~"
}
type Dog struct{}
func (d Dog) Say() string{
return "汪汪汪~"
}
typt Sayer interface{ //定义一个接口Sayer,有一个Say() 方法,返回值类型是string, Cat和Dog的方法有返回值
Say() string
}
func MakeHungry(s Sayer) string{
s.Say() string
}
func main() {
c := Cat{}
d := Dog{}
var animalList = []animal
animalList = append(animalList, c ,d) //把Cat和Dog的实例添加到切片
for _, item := range animalList{
ret := MakeHungry(item) //切片循环出来的value就是Cat、Dog的实例, 因为都实现了Sayer接口,因此都可以直接当做Sayer类型的参数传给函数
fmt.Println(ret)
}
}
在电商系统中我们允许用户使用多种支付方式(支付宝支付、微信支付、银联支付等),我们的交易流程中可能不太在乎用户究竟使用什么支付方式,只要它能提供一个实现支付功能的Pay
方法让调用方调用就可以了。
再比如我们需要在某个程序中添加一个将某些指标数据向外输出的功能,根据不同的需求可能要将数据输出到终端、写入到文件或者通过网络连接发送出去。在这个场景下我们可以不关注最终输出的目的地是什么,只需要它能提供一个Write
方法让我们把内容写入就可以了。
Go语言中为了解决类似上面的问题引入了接口的概念,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。使用接口类型通常能够让我们写出更加通用和灵活的代码。Go语言也是鸭子类型,同一类事物多种形态,就是面向对象中多态的用法。
5 面向接口编程
PHP、Java等语言中也有接口的概念,不过在PHP和Java语言中需要显式声明一个类实现了哪些接口,在Go语言中使用隐式声明的方式实现接口。只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。
Go语言中的这种设计符合程序开发中抽象的一般规律,例如在下面的代码示例中,我们的电商系统最开始只设计了支付宝一种支付方式:
type ZhiFuBao struct {
// 支付宝
}
// Pay 支付宝的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
fmt.Printf("使用支付宝付款:%.2f元。\n", float64(amount/100))
}
// Checkout 结账
func Checkout(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{})
}
随着业务的发展,根据用户需求添加支持微信支付。
type WeChat struct {
// 微信
}
// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}
在实际的交易流程中,我们可以根据用户选择的支付方式来决定最终调用支付宝的Pay方法还是微信支付的Pay方法。如果后续扩展其他的支付方式,都需要定义对应的checkout函数,代码冗余且重复。
// Checkout 支付宝结账
func CheckoutWithZFB(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
// Checkout 微信支付结账
func CheckoutWithWX(obj *WeChat) {
// 支付100元
obj.Pay(100)
}
实际上,从上面的代码示例中我们可以看出,我们其实并不怎么关心用户选择的是什么支付方式,我们只关心调用Pay方法时能否正常运行。这就是典型的“不关心它是什么,只关心它能做什么”的场景。
在这种场景下我们可以将具体的支付方式抽象为一个名为Payer
的接口类型,即任何实现了Pay
方法的都可以称为Payer
类型。
// Payer 包含支付方法的接口类型
type Payer interface {
Pay(int64)
}
此时只需要修改下原始的Checkout
函数,它接收一个Payer
类型的参数。这样就能够在不修改既有函数调用的基础上,支持新的支付方式。
// Checkout 结账
func Checkout(obj Payer) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{}) // 之前调用支付宝支付
Checkout(&WeChat{}) // 现在支持使用微信支付
}
像类似的例子在我们编程过程中会经常遇到:
-
比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
-
比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
-
比如满减券、立减券、打折券都属于电商场景下常见的优惠方式,我们能不能把它们当成“优惠券”来处理呢?
接口类型是Go语言提供的一种工具,在实际的编码过程中是否使用它由你自己决定,但是通常使用接口类型可以使代码更清晰易读。
6 接口类型变量
那实现了接口又有什么用呢?一个接口类型的变量能够存储所有实现了该接口的类型变量。
例如在上面的示例中,Dog
和Cat
类型均实现了Sayer
接口,此时一个Sayer
类型的变量就能够接收Cat
和Dog
类型的变量。
var x Sayer // 声明一个Sayer类型的变量x
a := Cat{} // 声明一个Cat类型变量a
b := Dog{} // 声明一个Dog类型变量b
x = a // 可以把Cat类型变量直接赋值给x
x.Say() // 喵喵喵
x = b // 可以把Dog类型变量直接赋值给x
x.Say() // 汪汪汪
7 空接口
7.1 空接口的定义
没有包含方法的接口称为空接口。空接口表示为 interface{}
。由于空接口没有方法,因此所有类型都实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
package main
import "fmt"
// Any 不包含任何方法的空接口类型
type Any interface{}
// Dog 狗结构体
type Dog struct{}
func main() {
var x Any //定义空接口类型变量 x
x = "你好" // 字符串型
fmt.Printf("type:%T value:%v\n", x, x)
x = 100 // int型
fmt.Printf("type:%T value:%v\n", x, x)
x = true // 布尔型
fmt.Printf("type:%T value:%v\n", x, x)
x = Dog{} // 结构体类型
fmt.Printf("type:%T value:%v\n", x, x)
}
通常我们在使用空接口类型时不必使用type
关键字声明,可以像下面的代码一样直接使用interface{}
。
var x interface{} // 声明一个空接口类型变量x
7.2 空接口的应用
7.2.1 空接口作为函数的参数
函数一旦确定了参数类型,传参时只能接收该类型的参数。使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
7.2.2 空接口作为map的值
map的vlaue类型一旦确定,必须是同一个类型,使用空接口实现可以保存任意值的字典。
// 空接口作为map值
var studentInfo = make(map[string]interface{}) //值类型是空接口类型,因此任意类型的值都可以存储到map
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
8 接口值
由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值之外,还需要记录这个值属于的类型。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型
和动态值
。
我们接下来通过一个示例来加深对接口值的理解。
下面的示例代码中,定义了一个Mover
接口类型和两个实现了该接口的Dog
和Car
结构体类型。
type Mover interface {
Move()
}
type Dog struct {
Name string
}
func (d *Dog) Move() {
fmt.Println("狗在跑~")
}
type Car struct {
Brand string
}
func (c *Car) Move() {
fmt.Println("汽车在跑~")
}
首先,我们创建一个Mover
接口类型的变量m
。
var m Mover
此时,接口变量m
是接口类型的零值,也就是它的类型和值部分都是nil
,就如下图所示。
我们可以使用m == nil
来判断此时的接口值是否为空。
fmt.Println(m == nil) // true
注意:我们不能对一个空接口值调用任何方法,否则会产生panic。
m.Move() // panic: runtime error: invalid memory address or nil pointer dereference
接下来,我们将一个*Dog
结构体指针赋值给变量m
。
m = &Dog{Name: "旺财"}
此时,接口值m
的动态类型会被设置为*Dog
,动态值为结构体变量的拷贝。
然后,我们给接口变量m
赋值为一个*Car
类型的值。
var c *Car
m = c
这一次,接口值m
的动态类型为*Car
,动态值为nil
。
注意:此时接口变量m
与nil
并不相等,因为它只是动态值的部分为nil
,而动态类型部分保存着对应值的类型。
fmt.Println(m == nil) // false
接口值是支持相互比较的,当且仅当接口值的动态类型和动态值都相等时才相等。
var (
x Mover = new(Dog)
y Mover = new(Car)
)
fmt.Println(x == y) // false
但是有一种特殊情况需要特别注意,如果接口值保存的动态类型相同,但是这个动态类型不支持互相比较(比如切片),那么对它们相互比较时就会引发panic。
var z interface{} = []int{1, 2, 3}
fmt.Println(z == z) // panic: runtime error: comparing uncomparable type []int
9 类型断言
9.1 类型断言语法
接口值可能赋值为任意类型的值,那我们如何从接口值获取其存储的具体数据呢?我们可以借助标准库fmt
包的格式化打印获取到接口值的动态类型。
var m Mover
m = &Dog{Name: "旺财"}
fmt.Printf("%T\n", m) // *main.Dog
m = new(Car)
fmt.Printf("%T\n", m) // *main.Car
而fmt
包内部其实是使用反射的机制在程序运行时获取到动态类型的名称。关于反射的内容我们会在后续章节详细介绍。
而想要从接口值中获取到对应的实际值需要使用类型断言,类型断言用于提取接口的底层值(Underlying Value)。在语法 i.(T)
中,接口 i
的具体类型是 T
,该语法用于获得接口的底层值。
其中:
- i:表示接口类型的变量
- T:表示断言
i
可能是的类型。
该语法返回两个参数,第一个参数是i
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
举个例子:
var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
fmt.Println("类型断言成功")
v.Name = "旺财" //变量v是*Dog类型
} else {
fmt.Println("类型断言失败")
}
当我们用空接口接收函数传参时,虽然很便利,但因为可以接收任意类型的函数参数,如果在外部包调用并给函数传参,我们无法判断参数的具体类型,进而不能对变量进一步做对应操作,这时候就需要用到类型断言,通过 if
判断执行相应的代码。
但是,如果对一个接口值有多个实际类型需要判断,if
判断的书写相当繁琐,推荐使用类型选择来实现。
9.2 类型选择
类型选择用于将接口的具体类型与很多 case
语句所指定的类型进行比较。它与一般的 switch
语句类似。唯一的区别在于类型选择指定的是类型,而一般的 switch
指定的是值。
类型选择的语法类似于类型断言。类型断言的语法是 i.(T)
,而对于类型选择,类型 T
由关键字 type
代替。下面看看程序是如何工作的。
package main
import (
"fmt"
)
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf("I am a string and my value is %s\n", i.(string))
case int:
fmt.Printf("I am an int and my value is %d\n", i.(int))
default:
fmt.Printf("Unknown type\n")
}
}
func main() {
findType("Naveen")
findType(77)
findType(89.98)
}
在上述程序的第 8 行,switch i.(type)
表示一个类型选择。每个 case 语句都把 i
的具体类型和一个指定类型进行了比较。如果 case 匹配成功,会打印出相应的语句。该程序输出:
I am a string and my value is Naveen
I am an int and my value is 77
Unknown type
i.(type)
有一个返回的类型对应的值,因此switch i.(type)
语法也可以定义变量接收返回值
func findType(i interface{}) {
switch v:= i.(type) { //变量v用于接收 i转化为T类型后的值
case string:
fmt.Printf("I am a string and my value is %s\n", v)
case int:
fmt.Printf("I am an int and my value is %d\n", v)
default:
fmt.Printf("Unknown type\n")
}
}
还可以将一个类型和接口相比较。如果一个类型实现了接口,那么该类型与其实现的接口就可以互相比较。为了阐明这一点,下面写一个程序。
package main
import "fmt"
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() {
fmt.Printf("%s is %d years old", p.name, p.age)
}
func findType(i interface{}) { //参数空接口,可以接收任意类型,因此需要类型断言,判断参数类型
switch v := i.(type) {
case Describer: //判断传过来的参数是不是Describer类型
v.Describe()
default:
fmt.Printf("unknown type\n")
}
}
func main() {
findType("Naveen")
p := Person{
name: "Naveen R",
age: 25,
}
findType(p)
}
在上面程序中,结构体 Person
实现了 Describer
接口。在第 19 行的 case 语句中,v
与接口类型 Describer
进行了比较。p
实现了 Describer
,因此满足了该 case 语句,于是当程序运行到第 32 行的 findType(p)
时,程序调用了 Describe()
方法。该程序输出:
unknown type
Naveen R is 25 years old
10 实现接口:指针接受者与值接受者
在结构体那一章节中,我们介绍了在定义结构体方法时既可以使用值接收者也可以使用指针接收者。那么对于实现接口来说使用值接收者和使用指针接收者有什么区别呢?接下来我们通过一个例子看一下其中的区别。
我们定义一个Mover
接口,它包含一个Move
方法。
// Mover 定义一个接口类型
type Mover interface {
Move()
}
10.1 值接收者实现接口
我们定义一个Dog
结构体类型,并使用值接收者为其定义一个Move
方法。
// Dog 狗结构体类型
type Dog struct{}
// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
fmt.Println("狗会动")
}
此时实现Mover
接口的是Dog
类型。
var x Mover // 声明一个Mover类型的变量x
var d1 = Dog{} // d1是Dog类型
x = d1 // 可以将d1赋值给接口变量x
x.Move()
var d2 = &Dog{} // d2是*Dog类型(指针)
x = d2 // 也可以将d2赋值给变量x
x.Move() // 实际是取指针存储的地址对应的值(即Dog类型的值)===> *d2.Move() 语法糖简化为 d2.Move(),能调用Move()方法,因此实现了接口Mover
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。
10.2 指针接收者实现接口
我们再来测试一下使用指针接收者实现接口有什么区别。
// Cat 猫结构体类型
type Cat struct{}
// Move 使用指针接收者定义Move方法实现Mover接口
func (c *Cat) Move() {
fmt.Println("猫会动")
}
此时实现Mover
接口的是*Cat
类型,我们可以将*Cat
类型的变量直接赋值给Mover
接口类型的变量x
。
var c1 = &Cat{} // c1是*Cat类型
x = c1 // 可以将c1当成Mover类型
x.Move()
但是不能给将Cat
类型的变量赋值给Mover
接口类型的变量x
。
// 下面的代码无法通过编译
var c2 = Cat{} // c2是Cat类型
x = c2 // 不能将c2当成Mover类型,编译器报错
由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。但是我们并不总是能对一个值求址,值类型在函数和方法里copy的是值的副本,对一个副本求地址并不是原来的地址,所以对于指针接收者实现的接口要额外注意。
11 类型与接口的关系
11.1 一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如狗不仅可以叫,还可以动。我们完全可以分别定义Sayer
接口和Mover
接口,具体代码示例如下。
// Sayer 接口
type Sayer interface {
Say()
}
// Mover 接口
type Mover interface {
Move()
}
Dog
既可以实现Sayer
接口,也可以实现Mover
接口。
type Dog struct {
Name string
}
// 实现Sayer接口
func (d Dog) Say() {
fmt.Printf("%s会叫汪汪汪\n", d.Name)
}
// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}
同一个类型实现不同的接口互相不影响使用。一旦转到某个接口上,只能使用该接口的方法,自身属性和方法需类型断言后才能使用。
var d = Dog{Name: "旺财"}
var s Sayer = d
var m Mover = d
s.Say() // 对Sayer类型调用Say方法
m.Move() // 对Mover类型调用Move方法
11.2 多种类型实现同一接口
Go语言中不同的类型还可以实现同一接口。例如在我们的代码世界中不仅狗可以动,汽车也可以动。我们可以使用如下代码体现这个关系。
// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}
// Car 汽车结构体类型
type Car struct {
Brand string
}
// Move Car类型实现Mover接口
func (c Car) Move() {
fmt.Printf("%s速度70迈\n", c.Brand)
}
这样我们在代码中就可以把狗和汽车当成一个会动的类型来处理,不必关注它们具体是什么,只需要调用它们的Move
方法就可以了。
var obj Mover
obj = Dog{Name: "旺财"}
obj.Move()
obj = Car{Brand: "宝马"}
obj.Move()
一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
12 接口的嵌套
尽管 Go 语言没有提供继承机制,但可以通过嵌套其他的接口,创建一个新接口。
我们来看看这如何实现。
type Animal interface {
eat()
sleep()
}
type Duck interface {
Animal //嵌套Animal类型,相当于Duck继承了Animal父类的方法
speak()
run()
}
//如果一个结构体实现Duck接口,必须实现Duck的方法以及其父类(嵌套接口)的所有方法
type PDuck struct {
name string
sex string
age int
}
func (p PDuck)speak() {
}
func (p PDuck)run() {
}
func (p PDuck)sleep() {
}
func (p PDuck)eat() {
}
func main() {
var a Animal
var d Duck
var c PDuck = PDuck{}
a=c //既然实现了Duck接口,肯定也实现了Animal接口,因此既可以赋值给Duck类型变量,也可以赋值给Animal类型变量
d=c //a只能调用Animal的两个方法,d可以调用所有4个方法
}
13 接口的零值
接口是引用类型,接口的零值是 nil
。对于值为 nil
的接口,其底层值(Underlying Value)和具体类型(Concrete Type)都为 nil
。
package main
import "fmt"
type Describer interface {
Describe()
}
func main() {
var d1 Describer
if d1 == nil {
fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
}
}
上面程序里的 d1
等于 nil
,程序会输出:
d1 is nil and has type <nil> value <nil>
对于值为 nil
的接口,由于没有底层值和具体类型,当我们试图调用它的方法时,程序会产生 panic
异常。