Go语言基础之接口定义
Go语言基础之接口定义
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
一、接口类型
在Go语言中接口(interface)是一种类型,一种抽象的类型,引用类型。
interface
是一组method
的集合,是duck-type programming
的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
二、 为什么要使用接口
type Cat struct{}
func (c Cat) Say() string { return "喵喵喵" }
type Dog struct{}
func (d Dog) Say() string { return "汪汪汪" }
func main() {
c := Cat{}
fmt.Println("猫:", c.Say())
d := Dog{}
fmt.Println("狗:", d.Say())
}
上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?
像类似的例子在我们编程过程中会经常遇到:
比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?
Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。
三、接口的定义
Go语言提倡面向接口编程。
每个接口由数个方法组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
- 接口名:使用
type
将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子:
type writer interface{
Write([]byte) error
}
当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。
四、实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
我们来定义一个Sayer
接口:
// Sayer 接口
type Sayer interface {
say()
}
定义dog
和cat
两个结构体:
type dog struct {}
type cat struct {}
因为Sayer
接口里只有一个say
方法,所以我们只需要给dog
和cat
分别实现say
方法就可以实现Sayer
接口了。
// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}
// cat实现了Sayer接口
func (c cat) say() {
fmt.Println("喵喵喵")
}
接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。
五、接口类型变量
那实现了接口有什么用呢?
接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer
类型的变量能够存储dog
和cat
类型的变量。
func main() {
var x Sayer // 声明一个Sayer类型的变量x
a := cat{} // 实例化一个cat
b := dog{} // 实例化一个dog
x = a // 可以把cat实例直接赋值给x
x.say() // 喵喵喵
x = b // 可以把dog实例直接赋值给x
x.say() // 汪汪汪
}
如果我们仅仅只是想验证一下,某个结构体是否实现了接口,可以这么做
// 1、如果下述代码可以赋值成功,则证明结构体实现了接口,因为此处我们只想验证,所以变量名无关紧要,推荐用_代表
var _ 接口类型 = &结构体{}
// 2、例如:
var _ DuckInterface = &PDuck{}
// 摘自gin框架routergroup.go 体味此处_的妙用
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter
任意的类型都可以赋值给空接口类型
type Empty interface {
}
//3 空接口(没有方法,所有类型其实都实现了空接口,于是:任意的类型都可以赋值给空接口类型)
var a Empty=10
a="ssddd"
a = TDuck{}
fmt.Println(a)
六、接口零值
接口的零值是 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
异常。
package main
type Describer interface {
Describe()
}
func main() {
var d1 Describer
d1.Describe()
}
在上述程序中,d1
等于 nil
,程序产生运行时错误 panic
: panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc8527] 。
七、总结
-
接口定义
- 接口名单词后面加
er
,表示是接口 - 接口名和方法名大写表示可以被包之外代码可以访问
type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 方法名2( 参数列表2 ) 返回值列表2 … }
- 接口名单词后面加
-
如果一个结构体实现接口中所有的方法,那么就实现了这个接口
-
接口是一种类型,引用类型,默认值:
nil
-
空接口可以接收任何类型
-
判断结构体是否实现了接口:
var _ 接口类型 = &结构体{}