写在前面
在上一篇文章《Golang入门学习之方法(method)》当中,我们学习了Golang当中的方法的定义与运用,在接下来的这篇文章当中,我们将一起来学习Goalng的接口(interface)。
接口的定义
接口是定义了一组需要被实现的方法的抽象类型,实现接口的数据类型可以视为接口的实例。接口由一组方法与一个接口类型组成。声明格式如下:
type 接口名 interface{
方法名(形参列表) (返回值列表)
...
}
按照Golang的编程风格(约定),只包含一个方法的接口的名字由方法名加 [e]r
后缀组成,例如 Printer
、Reader
、Writer
、Logger
、Converter
等等。还有一些不常用的方式(当后缀 er
不合适时),比如 Recoverable
,此时接口名以 able
结尾,或者以 I
开头(像 Java
中那样)。
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。
请看下面这个例子:
首先,我们在interfaces包中声明一个SpiritualRootAble
(表示具备修行的能力,现了灵根接口的凡人即可修炼)接口
src/go_code/interface/interfaces/spiritual_root.go
package interfaces
// 灵根接口,实现了灵根接口的凡人即可修炼
type SpiritualRootAble interface {
// 生成灵根
GenSpiritualRootNames() string
// 获取生成的灵根
SpiritualRoot() string
// 修行方法
Practice()
}
其次,我们在model包当中声明一个凡人结构体(mortal
) 并实现SpiritualRootAble
接口
src/go_code/interface/model/mortal.go
package model
import (
"crypto/rand"
"fmt"
"math/big"
)
// 凡人
type Mortal struct {
name ,
gender ,
spiritualRoot string
age int
}
func NewMortal(name, gender string, age int) Mortal {
mortal:=Mortal{
name: name,
gender: gender,
age: age,
}
mortal.spiritualRoot = mortal.GenSpiritualRootNames()
return mortal
}
func (recv Mortal) SpiritualRoot() string {
if &recv.spiritualRoot == nil{
return "没有灵根"
}
return recv.spiritualRoot
}
func (recv Mortal)Practice() {
fmt.Println(recv.name,"开始修行...")
}
func (recv Mortal) GenSpiritualRootNames() string{
gsrn := []string{
"金灵根","水灵根","木灵根","火灵根","土灵根","没有灵根",
}
index, _ := rand.Int(rand.Reader, big.NewInt(5))
return gsrn[index.Int64()]
}
最后,我们在main中使用他们
src/go_code/interface/main/main.go
package main
import (
"fmt"
"go_code/interface/interfaces"
"go_code/interface/model"
)
func main() {
// 声明了一个SpiritualRootAble接口类型的变量
var sr interfaces.SpiritualRootAble
// 降生了一个凡人
mortal := model.NewMortal("韩立","男性",1)
// 接口变量指向凡人实例
sr = mortal
// 获取凡人的灵根
fmt.Println(sr.SpiritualRoot())
// 凡人开始修炼
sr.Practice()
}
输出:
火灵根
韩立 开始修行...
可以看到:
在Golang中并需要显示地声明类型实现了某一接口(不需要如同Java那样class implemments interfacesName
),只要类型实现了接口当中定义的方法集,类型即是实现了该接口。
实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
一个类型可以实现多个接口(实际上mortal
还实现了空接口,关于空接口的内容请继续往下看)。
接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。
空接口
空接口即为不包含任何方法的接口。任何类型都实现了空接口。空接口有点类似于Java当中的Object的概念
type Any interface {}
接口嵌套接口
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。
比如接口 File
包含了 ReadWrite
和 Lock
的所有方法,它还额外有一个 Close()
方法。
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
Golang中的类型断言
对于一个接口类型变量varI
中可以包含任何类型的值,so,必须有一种方式来检测它的动态类型,也就是运行时变量var中存储的值的实际类型。而这,就是类型断言。
v := varI.(T)
类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:
if v,ok :=varI.(T);ok{
do something
}
如果转换合法,v
是 varI
转换到类型 T
的值,ok
会是 true
;否则 v
是类型 T
的零值,ok
是 false
,也没有运行时错误发生。
如果我们只是需要判断varI
是否为T类型而不需要获取类型T的值时,可以这样做:
if _,ok := varI.(T);ok{
}
我们继续沿用上面的例子讲类型断言这部分的内容
package main
import (
"fmt"
"go_code/interface/interfaces"
"go_code/interface/model"
)
func main() {
// 声明了一个SpiritualRootAble接口类型的变量
var sr interfaces.SpiritualRootAble
// 降生了一个凡人
mortal := model.NewMortal("韩立","男性",1)
// 接口变量指向凡人实例
sr = mortal
// 类型断言
if v,ok :=sr.(model.Mortal);ok{
fmt.Println(v)
}
}
输出:
&{韩立 男性 金灵根 1}
值得注意的是,我们在实现SpiritualRootAble
时方法的receiver类型为*Mortal
,即凡人的指针类型。因此实际上实现SpiritualRootAble接口的是*Mortal,因此在进行类型断言时T为*Mortal
。我们在使用类型断言的时候要注意这一点,不然编译器会报错。
类型断言的应用
在Golang中,我们如何测试一个值是否实现了某一接口呢?答案就是通过类型断言
var m interfaceTypeName
if _,ok := m.(interfaceTypeName);ok {
fmt.Println(ok)
}
总结
接口可以理解为一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。
在golang中:
- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。
写在最后
关于Golang当中接口的知识点我就简单介绍到这里。本文当中涉及到的例子可以点击此处下载。如果我的学习笔记能够给你带来帮助,还请多多点赞鼓励。文章如有错漏之处还请各位小伙伴帮忙斧正。从下一篇文章开始,我们将开启筑基系列的学习,具体涉及到的知识点有:反射、文件操作、数据交换、错误处理、Go协程(goroutine)和通道(channel)等内容。欢迎各位小伙伴订阅我的博客👊。