Golang语言基础之接口(interface)及类型断言
作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。
目录
一.接口类型
1.为什么要使用接口
在电商系统中我们允许用户使用多种支付方式(支付宝支付、微信支付、银联支付等),我们的交易流程中可能不太在乎用户究竟使用什么支付方式,只要它能提供一个实现支付功能的Pay方法让调用方调用就可以了。
再比如我们需要在某个程序中添加一个将某些指标数据向外输出的功能,根据不同的需求可能要将数据输出到终端、写入到文件或者通过网络连接发送出去。在这个场景下我们可以不关注最终输出的目的地是什么,只需要它能提供一个Write方法让我们把内容写入就可以了。
Go语言中为了解决类似上面的问题引入了接口的概念,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。使用接口类型通常能够让我们写出更加通用和灵活的代码。
像类似的例子在我们编程过程中会经常遇到:
- 比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
- 比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
- 比如满减券、立减券、打折券都属于电商场景下常见的优惠方式,我们能不能把它们当成“优惠券”来处理呢?
接口类型是Go语言提供的一种工具,在实际的编码过程中是否使用它由你自己决定,但是通常使用接口类型可以使代码更清晰易读。
使用接口注意事项:
1.
2.接口的定义
接口概述:
1.在Go语言中接口(interface)是一种类型,接口定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节;
2.相较于之前章节中讲到的那些具体类型(字符串、整型,数组,切片、map,结构体等)更注重"我是谁",接口类型更注重"我能做什么"的问题;
3.接口可以定义一组方法,但不需要实现,不需要方法体,并且接口中不能包含任何变量,到某个自定义类型要使用的时候,再根据具体情况把这些方法具体实现出来;
4.PHP、Java等语言中也有接口的概念,不过在PHP和Java语言中需要显式(比如"implement关键字")声明一个类实现了哪些接口,但在Go语言中使用隐式声明的方式实现接口,只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。
接口的定义:
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
参数说明:
- 接口类型名:
Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。
接口名最好要能突出该接口的类型含义。
- 方法名:
当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:
参数列表和返回值列表中的参数变量名可以省略。
3.接口代码案例
package main
import (
"fmt"
)
// 定义接口,方法名称为"Say()",返回值为string类型。
type Animal interface {
Say() string
}
type Cat struct{}
type Dog struct{}
type Bird struct{}
func (c Cat) Say() string {
return "喵喵喵~"
}
func (d Dog) Say() string {
return "汪汪汪~"
}
func (s Bird) Say() string {
return "喳喳喳~"
}
func main() {
b := Bird{}
c := Cat{}
d := Dog{}
// 不使用接口的调用方式
fmt.Printf("鸟说: %s\n", b.Say())
fmt.Printf("猫说: %s\n", c.Say())
fmt.Printf("狗说: %s\n", d.Say())
fmt.Println("----- 分割线 ----- ")
var animalList []Animal
animalList = append(animalList, b, c, d)
// 遍历接口,调用接口
for _, item := range animalList {
fmt.Printf("%T说: %s\n", item, item.Say())
}
}
二.值接受者和指针接受者
1.值接受者实现接口
package main
import (
"fmt"
)
// KongTiao 定义的一个空调接口类型
type KongTiao interface {
ZhiLeng()
ZhiRe()
}
// GREE 格力结构体类型
type GREE struct {
Name string
Price float64
Temperature float64
}
// Haier 海尔结构体类型
type Haier struct {
Name string
Price float64
Temperature float64
}
// ZhiLeng 使用值接收者定义ZhiLeng方法实现KongTiao接口
func (g GREE) ZhiLeng() {
fmt.Printf("价格为[%.2f]的[%s]开始制冷,温度控制在%.2f℃\n", g.Price, g.Name, g.Temperature)
}
// ZhiRe 使用值接收者定义ZhiRe方法实现KongTiao接口
func (g GREE) ZhiRe() {
fmt.Printf("价格为[%.2f]的[%s]开始制热,温度控制在%.2f℃\n", g.Price, g.Name, g.Temperature)
}
func (h Haier) ZhiLeng() {
fmt.Printf("价格为[%.2f]的[%s]开始制冷,温度控制在%.2f℃\n", h.Price, h.Name, h.Temperature)
}
func (h Haier) ZhiRe() {
fmt.Printf("价格为[%.2f]的[%s]开始制热,温度控制在%.2f℃\n", h.Price, h.Name, h.Temperature)
}
func main() {
// 使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。
// 声明一个KongTiao接口类型的变量kt
var kt KongTiao
// geli是GREE类型
var geli = GREE{Name: "格力", Price: 2699.00, Temperature: 16}
// 可以将geli赋值给变量x
kt = geli
kt.ZhiLeng()
// haier是Haier指针类型
var haier = &Haier{Name: "海尔", Price: 2199.00, Temperature: 28.5}
// 也可以将haier赋值给变量kt
kt = haier
kt.ZhiRe()
}
2.指针接受者实现接口
package main
import (
"fmt"
)
// KongTiao 定义的一个空调接口类型
type KongTiao interface {
ZhiLeng()
ZhiRe()
}
// GREE 格力结构体类型
type GREE struct {
Name string
Price float64
Temperature float64
}
// Haier 海尔结构体类型
type Haier struct {
Name string
Price float64
Temperature float64
}
// ZhiLeng 使用指针接收者定义ZhiLeng方法实现KongTiao接口
func (g *GREE) ZhiLeng() {
fmt.Printf("价格为[%.2f]的[%s]开始制冷,温度控制在%.2f℃\n", g.Price, g.Name, g.Temperature)
}
// ZhiRe 使用指针接收者定义ZhiRe方法实现KongTiao接口
func (g *GREE) ZhiRe() {
fmt.Printf("价格为[%.2f]的[%s]开始制热,温度控制在%.2f℃\n", g.Price, g.Name, g.Temperature)
}
func (h *Haier) ZhiLeng() {
fmt.Printf("价格为[%.2f]的[%s]开始制冷,温度控制在%.2f℃\n", h.Price, h.Name, h.Temperature)
}
func (h *Haier) ZhiRe() {
fmt.Printf("价格为[%.2f]的[%s]开始制热,温度控制在%.2f℃\n", h.Price, h.Name, h.Temperature)
}
func main() {
// 结论: 使用指针接收者实现接口之后,只有对应的结构体指针类型的变量都可以赋值给该接口变量。
// 声明一个KongTiao接口类型的变量kt
var kt KongTiao
// geli是GREE指针类型
var geli = &GREE{Name: "格力", Price: 2699.00, Temperature: 16}
// 可以将geli赋值给变量x
kt = geli
kt.ZhiLeng()
// haier是Haier类型
// var haier = Haier{Name: "海尔", Price: 2199.00, Temperature: 28.5}
// 下面的代码无法通过编译,报错: "Haier does not implement KongTiao (method ZhiLeng has pointer receiver)"
// kt = haier // haier是Haier类型,并不是指针,不能将harier当成KongTiao类型
// kt.ZhiRe()
}
三.结构体实现接口类型
1.一个类型实现多个接口
package main
import "fmt"
type Telephone interface {
DaDianHua()
}
type YouXiTing interface {
PlayGame()
}
type Iphone struct {
Brand string
}
func (i Iphone) DaDianHua() {
fmt.Printf("%s品牌手机可以打电话\n", i.Brand)
}
func (i Iphone) PlayGame() {
fmt.Printf("%s品牌手机可以玩游戏哟~\n", i.Brand)
}
func main() {
// 一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
var phone = Iphone{Brand: "苹果"}
var dianhua Telephone = phone
var youxi YouXiTing = phone
// 对Telephone类型调用DaDianHua方法
dianhua.DaDianHua()
// 对YouXiTing类型调用PlayGame方法
youxi.PlayGame()
}
2.多种类型实现同一个接口
package main
import "fmt"
// CNI 定义一个名为"CNI"接口类型
type CNI interface {
ContainerNetworkInterface()
}
// Calico 定义一个名为"Calico"的结构体
type Calico struct {
Name string
Advantage string
}
// Flannel 定义一个名为"Flannel"的结构体
type Flannel struct {
Name string
Advantage string
}
// Calico类型实现CNI接口
func (c Calico) ContainerNetworkInterface() {
fmt.Printf("%s实现了Kubernetes的CNI接口,它的优势在于:%s\n", c.Name, c.Advantage)
}
// Flannel类型实现CNI接口
func (f Flannel) ContainerNetworkInterface() {
fmt.Printf("%s实现了Kubernetes的CNI接口,它的优势在于:%s\n", f.Name, f.Advantage)
}
func main() {
// Go语言中不同的类型还可以实现同一接口。
var obj CNI
// 将Calico结构体赋值给CNI接口是可以实现的
obj = Calico{Name: "Calico", Advantage: "容器的网络通信和网络策略"}
obj.ContainerNetworkInterface()
// 将Flannel结构体赋值给CNI接口是可以实现的
obj = Flannel{Name: "Flannel", Advantage: "容器的网络通信"}
obj.ContainerNetworkInterface()
}
3.嵌套结构体实现接口
package main
import (
"fmt"
)
// WeChat 微信
type WeChat interface {
Voice()
video()
}
// VoiceCall 语音电话
type VoiceCall struct {
Name string
}
// videoCall 视频电话
type videoCall struct {
//嵌入语音电话匿名结构体
VoiceCall
}
// 实现WeChat接口的Voice()方法
func (vo VoiceCall) Voice() {
fmt.Printf("'%s'微信号正在使用语音功能\n", vo.Name)
}
// 实现WeChat接口的video()方法
func (vi videoCall) video() {
fmt.Println("正在开启视频美颜,滤镜功能...")
}
func main() {
// 总结: 一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// 声明一个videoCall类型变量
var weixin videoCall
// 定义微信名称
weixin.Name = "JasonYin2020"
// 调用WeChat接口方法
weixin.Voice()
weixin.video()
}
四.接口使用注意事项
1.使用接口十个小技巧
- 1.接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量;
- 2.只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型,内置的数据类型起别名后依旧是可以让其实现接口的;
- 3.一个自定义类型可以实现多个接口;
- 4.一个接口(比如A接口)可以继承多个别的接口(比如B和C接口),这时如果想要实现A接口,也必须将B和C接口的所有方法全部实现;
- 5.interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil;
- 6.空接口是指没有定义任何方法的接口类型,所以可以理解所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋值给空接口;
- 7.使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量;
- 8.使用指针接收者实现接口之后,只有对应的结构体指针类型的变量都可以赋值给该接口变量;
- 9.一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现;
- 10.由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值之外,还需要记录这个值属于的类型;
2.空接口
package main
import "fmt"
// Object 定义一个名为"Object"的空接口,没有定义任何方法的接口类型。
type Object interface{}
type WeChat struct {
Name string
}
// 空接口作为函数的参数
func show(obj interface{}) {
fmt.Printf("type: %T \t value: %v\n", obj, obj)
}
func main() {
// Object不包含任何方法的空接口类型,因此任何类型都可以视为实现了空接口,也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
var obj Object
// 将字符串型赋值给"Object"接口类型
obj = "你好"
show(obj)
// int型赋值给"Object"接口类型
obj = 100
show(obj)
// float64型赋值给"Object"接口类型
obj = 3.1415926
show(obj)
// 布尔型赋值给"Object"接口类型
obj = true
show(obj)
// 自定义WeChat结构体类型赋值给"Object"接口类型
obj = WeChat{"JasonYin2020"}
show(obj)
// 使用空接口实现可以保存任意值的字典。
var teacher = make(map[string]interface{})
teacher["Name"] = "尹正杰"
teacher["Age"] = 18
teacher["Married"] = false
fmt.Println(teacher)
// 通常我们在使用空接口类型时不必使用type关键字声明
var none interface{} // 声明一个空接口类型变量none
fmt.Printf("none = %v\n",none)
}
3.接口值比较
package main
import "fmt"
type CNI interface {
ContainerNetworkInterface()
}
type Calico struct {
Name string
Advantage string
}
type Flannel struct {
Name string
Advantage string
}
func (c Calico) ContainerNetworkInterface() {
fmt.Printf("%s实现了Kubernetes的CNI接口,它的优势在于:%s\n", c.Name, c.Advantage)
}
func (f Flannel) ContainerNetworkInterface() {
fmt.Printf("%s实现了Kubernetes的CNI接口,它的优势在于:%s\n", f.Name, f.Advantage)
}
func main() {
/*
由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值之外,还需要记录这个值属于的类型。
也就是说接口值由"类型"和"值"组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型和动态值。
*/
// 此时,接口变量obj是接口类型的零值,也就是它的类型和值部分都是nil
var obj CNI
// 我们可以使用"obj == nil"来判断此时的接口值是否为空。
fmt.Printf("obj是否为空: %t, 类型为: %T, 数据为: %v\n", obj == nil, obj, obj)
// 我们不能对一个空接口值调用任何方法,否则抛出panic异常: "runtime error: invalid memory address or nil pointer dereference"
// obj.ContainerNetworkInterface()
// 接下来,我们将一个"*Calico"结构体指针赋值给变量obj,此时,接口值obj的动态类型会被设置为"*Calico",动态值为结构体变量的拷贝。
obj = &Calico{Name: "Calico", Advantage: "容器的网络通信和网络策略"}
fmt.Printf("obj是否为空: %t, 类型为: %T, 数据为: %v\n", obj == nil, obj, obj)
// 此时就可以调用接口的方法啦~
// obj.ContainerNetworkInterface()
obj = &Flannel{Name: "Flannel", Advantage: "容器的网络通信"}
fmt.Printf("obj是否为空: %t, 类型为: %T, 数据为: %v\n", obj == nil, obj, obj)
// obj.ContainerNetworkInterface()
// 接口值是支持相互比较的,当且仅当接口值的动态类型和动态值都相等时才相等。
var (
flannel CNI = new(Flannel)
calico CNI = new(Calico)
flannel2 CNI = new(Flannel)
)
fmt.Printf("flannel == calico ---> %t\n", calico == flannel)
fmt.Printf("flannel == flannel2 ---> %t\n", flannel == flannel2)
// 如果接口值保存的动态类型相同,但是这个动态类型不支持互相比较(比如切片),
// 那么对它们相互比较时就会引发: "panic: runtime error: comparing uncomparable type []int"
// var (
// x interface{} = []int{10, 20, 30}
// y interface{} = []int{10, 20, 30}
// )
// fmt.Printf("x == y ---> %t\n", x == y)
}
4.接口组合嵌套[了解即可]
接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io源码中就有很多接口之间互相组合的示例。
// src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
Reader
Writer
}
// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
Reader
Closer
}
// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
Writer
Closer
}
对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。
接口也可以作为结构体的一个字段,我们来看一段Go标准库sort源码中的示例。
// src/sort/sort.go
// Interface 定义通过索引对元素排序的接口类型
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// reverse 结构体中嵌入了Interface接口
type reverse struct {
Interface
}
通过在结构体中嵌入一个接口类型,从而让该结构体类型实现了该接口类型,并且还可以改写该接口的方法。
// Less 为reverse类型添加Less方法,重写原Interface接口类型的Less方法
func (r reverse) Less(i, j int) bool {
return r.Interface.Less(j, i)
}
Interface类型原本的Less方法签名为Less(i, j int) bool,此处重写为r.Interface.Less(j, i),即通过将索引参数交换位置实现反转。
在这个示例中还有一个需要注意的地方是reverse结构体本身是不可导出的(结构体类型名称首字母小写),sort.go中通过定义一个可导出的Reverse函数来让使用者创建reverse结构体实例。
func Reverse(data Interface) Interface {
return &reverse{data}
}
这样做的目的是保证得到的reverse结构体中的Interface属性一定不为nil,否则r.Interface.Less(j, i)就会出现空指针panic。
5.不要盲目使用接口类型
由于接口类型变量能够动态存储不同类型值的特点,所以很多初学者会滥用接口类型(特别是空接口)来实现编码过程中的便捷。
只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。
在Go语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。
相信很多读者在刚接触到接口类型时都会有很多疑惑,请牢记接口是一种类型,一种抽象的类型。
五.类型断言
1.类型断言概述
接口值可能赋值为任意类型的值,那我们如何从接口值获取其存储的具体数据呢?
我们可以借助标准库fmt包的格式化打印获取到接口值的动态类型,而fmt包内部其实是使用反射的机制在程序运行时获取到动态类型的名称。
Go语言里面有一个语法,可以直接判断是否是该类型的变量:
value,ok = element.(T)
这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
Type Switch是Go语言中一种特殊的switch语句,它比较的是类型而不是具体的值,它判断某个接口变量的类型,然后根据具体类型再做相应处理。
2.对单个值进行断言
package main
import "fmt"
// 定义SayHi接口
type SayHi interface {
SayHello()
}
type Chinese struct {
Name string
}
type American struct {
Name string
}
func (c Chinese) SayHello() {
fmt.Printf("你好,我的名字是: %s, 很高兴认识你。\n", c.Name)
}
func (c Chinese) DuiDuiZi() {
fmt.Printf("%s说: 宫廷玉液酒,180一杯!\n", c.Name)
}
func (a American) SayHello() {
fmt.Printf("Hi,My name is %s, And you ?\n", a.Name)
}
func greet(s SayHi) {
s.SayHello()
// 断言: 判断变量s是否能转成Chinese类型并且赋值给chinese变量
chinese, ok := s.(Chinese)
if ok {
chinese.DuiDuiZi()
} else {
fmt.Println("这不是中国人......")
}
// 简写形式
// if chinese, ok := s.(Chinese); ok {
// chinese.DuiDuiZi()
// } else {
// fmt.Println("这不是中国人......")
// }
}
func main() {
var array [2]SayHi
array[0] = Chinese{"女娲"}
array[1] = American{"自由女神"}
fmt.Println(array)
for _, item := range array {
greet(item)
fmt.Println("----- 分割线 -----")
}
}
3 对多个值进行断言
package main
import "fmt"
// 定义SayHi接口
type SayHi interface {
SayHello()
}
type Chinese struct {
Name string
}
type American struct {
Name string
}
func (c Chinese) SayHello() {
fmt.Printf("你好,我的名字是: %s, 很高兴认识你。\n", c.Name)
}
func (c Chinese) DuiDuiZi() {
fmt.Printf("%s说: 宫廷玉液酒,180一杯!\n", c.Name)
}
func (a American) SayHello() {
fmt.Printf("Hi,My name is %s, And you ?\n", a.Name)
}
func (a American) BengDi() {
fmt.Printf("%s说: 我喜欢蹦迪!\n", a.Name)
}
func greet(s SayHi) {
s.SayHello()
// 使用switch语句对多个值进行断言的固定语法,其中"type"属于Golang中的关键字。
switch s.(type) {
case Chinese:
chinese := s.(Chinese)
chinese.DuiDuiZi()
case American:
american := s.(American)
american.BengDi()
}
}
func main() {
var array [2]SayHi
array[0] = Chinese{"女娲"}
array[1] = American{"自由女神"}
fmt.Println(array)
for _, item := range array {
greet(item)
}
}
当你的才华还撑不起你的野心的时候,你就应该静下心来学习。当你的能力还驾驭不了你的目标的时候,你就应该沉下心来历练。问问自己,想要怎样的人生。
欢迎交流学习技术交流,个人微信: "JasonYin2020"(添加时请备注来源及意图备注)
作者: 尹正杰, 博客: https://www.cnblogs.com/yinzhengjie/p/18327819