接口
接口(interface)定义了一个对象的行为规范,之定义规范不实现,由具体的对象来实现规范的细节。
接口类型
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface 是一组method 的集合,是duck-type programming 的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我们就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
为什么要使用接口
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函数中明显有重复的代码,如果我们后续再加上猪、羊等动物的话,代码还会一直重复下去,name能不能把它们当成"能叫的动物"来处理呢?
像类似的例子我们在编码过程中经常会遇到:
比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理?
比如三角形,四边形,圆形都能计算周长和面积,能不能把它们当成“图形”来处理呢?
比如销售、行政、程序员都能计算月薪,能不能把他们当成“员工”来处理呢?
Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别我们之前所有的具体类型,接口是一种抽象的类型。
接口的定义
Go语言提倡面向接口编程。每个接口由数个方法组成。
type 接口类型名 interface{
方法名1(参数列表1) 返回值列表1
方法名2(参数列表2) 返回值列表2
...
}
- 接口名:使用type将接口定义为自定义类型名。Go语言的接口在命名时,一般会在单词后面添加er,如果有写操作的接口叫
Writer ,有字符串功能的接口叫Stringer ,接口名最好要突出该接口类型的含义。
- 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量可以省略
示例:
type writer interface{
Write([]byte) error
}
当看到这个接口类型的值时,我们不知道它是什么,唯一知道的是可以通过它的Write方法来做一些事情。
实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
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 类型的变量。
type Cat struct {}
func (c Cat) Say(){
fmt.Println("汪汪汪")
}
type Dog struct{}
func (d Dog) Say(){
fmt.Println("喵喵喵")
}
type Sayer interface {
Say()
}
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() // 喵喵喵
}
值接收者和指针接收者实现接口的区别
定义一个Mover 接口和一个dog 结构体
type Mover interface{
move()
}
type dog struct{
name string
}
值接收者实现接口
func (d dog) move(){
fmt.Printf("%s在快乐的奔跑\n",d.name)
}
func main() {
var x Mover
var wangcai dog = dog{
name: "旺财",
}
x = wangcai // 旺财是dog类型
x.move() // 旺财在快乐的奔跑
fugui := &dog{
name: "富贵",
}
x = fugui // 富贵是*dog类型
x.move() // 富贵在快乐的奔跑
}
从上面的代码可以发现,使用值接收者实现接口后,不管是dog结构体还是结构体指针dog类型的变量都可以复制给该接口变量。
因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui 内部自动求值*fugui
指针接收者实现接口
func(d *dog) move(){
fmt.Printf("%s在快乐的奔跑", d.name)
}
func main() {
var x Mover
//wangcai := dog{ // 旺财是dog类型
// name: "旺财",
//}
// x = wangcai // move method has pointer receiver // x不可以接收dog类型
// x.move()
fugui := &dog{ // 富贵是*dog类型
name: "富贵",
}
x = fugui
x.move() // 富贵在快乐的奔跑
}
此时实现Mover 接口的是*dog 类型,所以不能给x 传入dog 类型的wangcai,此时只能存储*dog 类型的值。
面试题
观察下面的这段代码,然后回答这段代码能不能通过编译?
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{} // var peo People = &Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
类型与接口的关系
一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立。例如狗可以叫,也可以动,那么我们就可以定义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)
}
func main() {
var (
x Sayer
y Mover
)
a := dog{
name:"旺财",
}
x = a
y = a
x.say()
y.move()
}
多个类型实现同一接口
Go语言中不同的类型还可以实现同一接口。
// Mover 接口
type Mover interface{
move()
}
type dog struct{
name string
}
type cat struct{
name string
}
// dog类型实现Mover接口
func (d dog) move(){
fmt.Printf("%s在欢乐的奔跑\n", d.name)
}
// car类型实现Mover接口
func (c car) move(){
fmt.Printf("%s在懒洋洋的散步\n", c.name)
}
这时候我们在代码中就可以把狗和猫当成动物来处理,不需要关注他们具体是什么,只需要调用它们的move 方法就可以。
func main(){
var x Mover
var a = dog{name:"旺财"}
var b = cat{name:"Tom"}
x = a
x.move() // 旺财在欢乐的奔跑
x = b
x.move() // Tom在懒洋洋的散步
}
并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
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("洗刷刷")
}
func main() {
var w WashingMachine
var h haier
w = h
w.dry() // 甩一甩
w.wash() // 洗刷刷
}
接口嵌套
接口与接口间可以通过嵌套创造出新的接口。
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
type cat struct{
name string
}
func (c cat) move(){
fmt.Printf("%s在懒洋洋的散步\n", c.name)
}
func (c cat) say(){
fmt.Printf("%s在喵喵的叫\n", c.name)
}
func main() {
c := cat{
name: "Tom",
}
var a animal
a = c
a.say() // Tom在喵喵的叫
a.move() // Tom在懒洋洋的散步
}
空接口
空接口的定义
空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
func main() {
// 定义一个空接口x
var x interface{}
s := "多想和你一样臭不要脸"
x = s
fmt.Printf("type: %T value:%v\n",x,x) // type: string value:多想和你一样臭不要脸
i := 100
x = i
fmt.Printf("type:%T value:%v\n",x,x) // type:int value:100
b := true
x = b
fmt.Printf("type:%T value:%v\n",x,x) // type:bool value:true
}
空接口的应用
空接口作为函数的参数
使用空接口可以实现函数接收任意类型的参数。
func show(a interface{}){
fmt.Printf("type:%T value:%v\n",a ,a)
}
func main() {
show([...]string{"Negan","Egon"}) //type:[2]string value:[Negan Egon]
show([]string{"Negan","Egon"}) //type:[]string value:[Negan Egon]
}
空接口座位map的值
使用空接口实现可以保存任意值的字典
// 空接口作为map的值
func main() {
studentInfo := make(map[string]interface{})
studentInfo["name"] = "Negan"
studentInfo["age"] = 68
studentInfo["married"] = false
fmt.Println(studentInfo) // map[age:68 married:false name:Negan]
}
类型断言
空接口可以存储任意类型的值,那如何获取其存储的具体数据呢?
接口值
一个接口的值(简称接口值)是由一个具体类型 和具体类型的值 两部分组成。这两部分分别称为接口的动态类型 和动态值 。
想要判断空接口的值,可以使用类型断言
x.(T)
// x表示类型为interface{}的变量
// T表示断言,x可能的类型
该语法返回两个参数,第一个参数是x 转换为T 类型后的变量,第二个值是一个布尔值,若为true 则表示断言成功,为false 则表示断言失败。
func main() {
var x interface{}
x = "hello Negan"
v,ok := x.(string)
if ok{
fmt.Println(v)
}else{
fmt.Println("类型断言失败")
}
}
上面的示例中如果要断言多个就需要写多个if 判断,这个时候我们可以使用switch 语句实现。
func justifyType(x interface{}){
switch v:=x.(type) {
case string:
fmt.Printf("x is string,value is %s\n",v)
case int:
fmt.Printf("x is string,value is %s\n",v)
case bool:
fmt.Printf("x is string,value is %s\n",v)
default:
fmt.Printf("x is string,value is %s\n",v)
}
}
func main() {
justifyType("hello Negan") //x is string,value is hello Negan
}
因为空接口可以存储任意类型值的特点,所以空接口在Go中使用十分广泛,
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口,不要为了接口而写接口,那样只能回增加不必要的抽象,导致不需要的运行损耗。
|
请发表评论