Go语言基础之接口(面向对象编程下)
1 接口
1.1 接口介绍
接口(interface)是Go语言中核心部分,Go语言提供面向接口编程,那么接口是什么?
现实生活中,有许多接口的例子,比如说电子设备上的充电接口,这个充电接口能干什么,在接口设计时就定义好了,比如说这个接口既能充电可以进行数据的传输;之后只需电子设备是实现这个接口的功能,就像手机上的Type-C接口既可以充电又可以数据传输。
在Golang中接口(interface)是一种类型,一种抽象的类型。在接口类型中可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。对于某个自定义类型可以使用这个接口,但是必须实现这个接口中定义的所有方法。从这点来看,接口不关心事物的属性,只关心事物具有的行为。
1.2 为什么要使用接口
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())
}
上面的代码中定义二楼猫和狗,它们都会叫,都有Say()
这个方法,你会发现main
函数中明显有重复的代码,无果后续再有其他动物的话,这样的代码还会一直重复下去。那我们能不能这样考虑问题呢,把这些动物归结成“能叫的动物“,只是不同的动物有不同的叫法。
像类似的例子在编程中经常遇到:
比如一个网上商城可能使用支付宝、微信、银联等方式在线支付,那能不能把它们当成“支付方式“来处理呢?
再比如三角形、四边形、圆形都能计算周长和面积,那能不能把它们当成“图形”来处理呢?
对于上面的这些问题,Go语言中提供了接口类型。当看到一个接口类型的值是,我们不知道它是什么,唯一知道的是通过它的方法能做什么。
1.3 基本语法
type 接口名 interface {
method1(参数列表) 返回值列表
method2(参数列表) 返回值列表
}
//实现接口所有方法
func (t 自定义类型) method1(参数列表) 返回值列表 {
//方法实现
}
func (t 自定义类型) method2(参数列表) 返回值列表 {
//方法实现
}
接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想。
Golang中的皆苦,不需要显式的实现,只要一个变量,含有接口类型中的所有方法,那么这个变量就实现了这个接口。
1.4 接口的使用案例
对于1.2中的示例,使用接口实现:
//定义一个Sayer的接口
type Sayer interface {
Say()
}
//定义两个结构体Cat和Dog
type Cat struct {}
type Dog struct {}
// 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{}
b := Dog{}
x = a //只有自定义类型实现了某个接口,才能将自定义类型的变量赋值给接口类型变量
x.Say()
x = b
x.Say()
}
1.5 值类型接收者和指针接收者实现接口的区别
通过下面的例子来看下二者的区别:
//声明一个Mover的接口
type Mover interface {
move()
}
//声明一个dog的结构体
type dog struct {}
1.5.1 值类型接收者实现接口
值类型接收者dog
实现Mover
接口:
func (d dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var dog1 = dog{}
x = dog1
var dog2 = &dog{}
x = dog2 //x可以接收*dog类型
x.move
}
从上面的代码可以看出,对于struct
如果接收者是值类型,不管是结构体还是结构体指针类型的变量,只要这个结构体实现对应的接口,那么这两个变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,*dog2
等价于dog2
。
1.5.2 指针类型接收者实现接口
指针类型的接收者*dog
实现接口:
func (d *dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var dog1 = dog{}
x = dog1 //x不可以接收dog类型,会报dog类型没有实现Mover接口
var dog2 = &dog{}
x = dog2 //x可以接收*dog类型
x.move
}
从上面的示例可知,如果struct
的接收者是指针类型,只能将结构体指针赋值给接口类型的变量。
1.5.3 面试题
下面代码能否编译?为什么?
type People interface {
Speak(string) string
}
type Student struct{}
func (stu *Student) Speak(think string) (talk string) {
if think == "sb" {
talk = ""
} else {
talk = "您好"
}
return
}
func main() {
var peo People = Student{}
think := ""
fmt.Println(peo.Speak(think))
}
1.6 空接口
1.6.1 空接口的定义
空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量:
func main() {
// 定义一个空接口x
var x interface{}
s := "Hello viktor"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
1.6.2 空接口的应用
使用空接口可以接收任意类型的函数参数:
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
使用空接口可以保存任意值的字典:
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "viktor"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
1.7 接口总结及注意事项
-
接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(参考1.5中的示例);
-
接口中的所有的方法都没有方法体,即都是没有实现的方法;
-
在Golang中,一个自定义类型需要将某个接口的所有方法都实现,则称这个自定义类型实现了该接口;
-
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型;
-
一个自定义类型可以实现多个接口;
-
Golang接口中不能有任何变量;
-
一个接口(比如A接口)可以嵌套多个别的接口(比如B,C接口),这时如果要实现A接口,必须将B,C接口的方法也全部实现;
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
//cat结构体实现animal接口
type cat struct {
name string
}
func (c cat) say() {
fmt.Println("喵喵喵")
}
func (c cat) move() {
fmt.Println("猫会动")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}
-
interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么就会输出
nil
; -
空接口
interface{}
没有任何方法,所以所有类型都 实现了空接口,即我们可以把任何一个变量赋给空接口。
2 接口 vs 继承
从一个事物的角度来看,比如一个篮球运动员或者大学生:
篮球运动员或者大学生可以分别继承运动员或者学生的一些属性;但是,篮球运动员或者大学生有可能会有一些相同的行为,比如会说英语,那么就可以定义一个会说英语的接口,分别让二者实现接口。
接口和继承的区别:
-
接口和继承解决的问题不同:继承的价值主要在于,解决代码的复用性和可维护性;接口的价值主要在于,设计,设计好各种规范(方法),让其它自定义类型去实现这些方法;
-
接口比继承更加灵活。继承是满足
is - a
关系,而接口只需满足like - a
的关系; -
接口在一定程度上实现代码解耦。
3 面向对象编程-多态
3.1 基本介绍
变量(实例)具有多种形态。面向对象的第三大特征,在Go语言中,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
3.2 快速入门案例
//声明/定义一个接口
type Usb interface {
//声明了两个没有实现的方法
Start()
Stop()
}
type Phone struct {
}
//让Phone 实现 Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作。。。")
}
type Camera struct {
}
//让Camera 实现 Usb接口的方法
func (c Camera) Start() {
fmt.Println("相机开始工作~~~。。。")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作。。。")
}
//计算机
type Computer struct {
}
//编写一个方法Working 方法,接收一个Usb接口类型变量
//只要是实现了 Usb接口 (所谓实现Usb接口,就是指实现了 Usb接口声明所有方法)
func (c Computer) Working(usb Usb) {//通过usb接口变量来调用Start和Stop方法
usb.Start()
usb.Stop()
}
func main() {
//测试
//先创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}
//关键点
computer.Working(phone)
computer.Working(camera)
}
在上面的代码中,Working(usb Usb)
方法,既可以接收手机变量,又可以接收相机变量,就体现了Usb
接口多态的特性
3.3 接口体现多态的两种形式
多态参数
在一个函数或者是一个方法的参数如果是一个接口类型,那么该参数可以接收实现了该接口的所有的自定义类型。如3.2中的案例。
多态数组
自定义类型只要实现了接口,那么都可以存放在接口的数组中。看如下案例:
package main
import (
"fmt"
)
//声明/定义一个接口
type Usb interface {
//声明了两个没有实现的方法
Start()
Stop()
}
type Phone struct {
name string
}
//让Phone 实现 Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作。。。")
}
type Camera struct {
name string
}
//让Camera 实现 Usb接口的方法
func (c Camera) Start() {
fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作。。。")
}
func main() {
//定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
//这里就体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
fmt.Println(usbArr)
}
4 类型断言
4.1 基本介绍
在前面的所有示例中,都是将一个变量(示例)赋值给一个接口。由于一个接口可以被多个自定义类型实现,我们都知道在Go语言中不同类型之前是不能赋值的,此时与这样的一个需求,将接口类型的变量赋值给自定义类型的变量,比如下面的代码,该如何实现?
type Point struct {
x, y int
}
func main() {
var a interface{}
var point Point = Point{1,2}
a = point //ok
//如何将a赋给一个Point变量?
var b Point
b = a //?
fmt.Println(b)
}
类比:可以将接口理解成一个很大的容器,当把一个自定义类型赋给接口,就相当于把它放入这个大容器里面,由于这个容器里面可以放很多不同的自定义类型,当想要把刚才的那个放入容器里面的自定义类型赋给其它的自定义类型,就需要先找到它。这个找的过程就是类型断言。
类型断言的基本语法:
x.(T)
其中:
-
x:表示类型为
interface{}
的变量 -
T:表示断言
x
可能是的类型
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
对上面代码的改进:
type Point struct {
x, y int
}
func main() {
var a interface{}
var point Point = Point{1,2}
a = point //ok
//如何将a赋给一个Point变量?
var b Point
// b = a //?
b = a.(Point) //类型断言,表示判断a是否指向Point类型的变量,如果是就转成Point类型并赋给b变量,否则报错
fmt.Println(b)
}
4.2 类型断言的使用
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,具体如下:
func main() {
var x interface{}
var b float32 = 6.6
x = b2 //空接口,可以接收任意类型
//x -> flaot32 [使用类型断言]
y := x.(float32)
fmt.Printf("y的类型是 %T 值是%v\n", y, y)
}
对于上面代码,在进行类型断言时,如果类型不匹配,就会报panic
,因此进行类型短延时,要确保原来的空接口指向的就是断言的类型。
断言时也可以带上检测机制,如下示例 :
func main() {
var x interface{}
var b2 float32 = 2.1
x = b2 //空接口,可以接收任意类型
// x=>float32 [使用类型断言]
//类型断言(带检测的)
if y, ok := x.(float32); ok {
fmt.Println("convert success")
fmt.Printf("y 的类型是 %T 值是=%v", y, y)
} else {
fmt.Println("convert fail")
}
fmt.Println("继续执行...")
}
4.3 类型断言的最佳实践
循环判断传入参数的类型:
//编写一个函数,可以判断输入的参数是什么类型
func TypeJudge(items... interface{}) {
for index, x := range items {
switch x.(type) {
case bool :
fmt.Printf("第%v个参数是 bool 类型,值是%v\n", index, x)
case float32 :
fmt.Printf("第%v个参数是 float32 类型,值是%v\n", index, x)
case float64 :
fmt.Printf("第%v个参数是 float64 类型,值是%v\n", index, x)
case int, int32, int64 :
fmt.Printf("第%v个参数是 整数 类型,值是%v\n", index, x)
case string :
fmt.Printf("第%v个参数是 string 类型,值是%v\n", index, x)
default :
fmt.Printf("第%v个参数是 类型 不确定,值是%v\n", index, x)
}
}
}
func main() {
var n1 float32 = 1.1
var n2 float64 = 2.3
var n3 int32 = 30
var name string = "tom"
address := "北京"
n4 := 300
TypeJudge(n1, n2, n3, name, address, n4, stu1, stu2)
}