首页 > 技术文章 > Go语言学习20-接口

sukusec301 2022-03-01 17:30 原文

Go语言学习20-接口

先声明一下,接口是Go语言第二个劝退点。如果想不明白,就自己默念三遍:接口是一种类型,是一种引用类型。

最开始我们使用编程语言,是C语言这样的面向过程编程;后来面向过程逐渐不被人所选择,于是出现了C++,Java,PHP,Python等面向对象的编程语言。

用Java等语言编写到后面就会发现很臃肿,啥都是对象。

再后来,苹果出的swift语言和Go语言,又提出了一个新的理念,就是面向接口编程。PHP、Java等语言中也有接口的概念,不过在PHP和Java语言中需要显式声明一个类实现了哪些接口,在Go语言中使用隐式声明的方式实现接口。只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。

每次学到一个新的概念,都要去思考一下,为什么会有新的概念出现呢?新的概念解决了什么痛点呢?出现前和出现后有什么区别呢?

我们调用的print,传入任何的类型,都能打印出来;而我们学的函数,必须要有特定的类型才行。

比如,我买东西,我不管用什么支付,用的微信还是支付宝还是银联,只要能给我付钱即可,能不能把他们当作“支付方式”来处理呢?

比如,三角形,四边形,圆形都能计算周长和面积,能不能治把他们当成“图形”处理?

比如,销售、行政、程序员都能计算月薪,能不能统一把他们当成“员工”处理呢?

比如,数据库有多种,Mysql、Oracle、Mssql等等,特别多,存入一个人的信息都可以,能不能使用这些数据库的增删改查方法呢?

Go语言中为了解决类似上面的问题,就设计出了一个概念——接口,接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。粗浅的认知:接口是多种通用方法的集合。

0x00 接口引入—小试牛刀

1、首先我定义了狗、猫、人三种结构体类型。并且定义了三个对应方法,都是bark叫唤的意思

2、其次我定义了一个方法叫做打。我现在想打谁我就传进那个函数来打谁。

3、但是如果这时候x的类型是一个dog,那就只能传dog类型的;如果x的类型是一个cat,那就只能传cat类型的。。这就出现了弊端了。

4、但是我们发现了一个规律,就是这些个类型,都有bark方法。

5、我们希望,在main函数中,能够这样传入,来使用。

image-20220227130355491

type dog struct{}
type cat struct{}
type person struct{}

func (c cat) bark() {
	fmt.Println("喵喵喵!")
}
func (d dog) bark() {
	fmt.Println("wangwangwnag")
}
func (p person) bark() {
	fmt.Println("Yingyingying!")
}

func beat(x){
	//接收一个参数,传进来什么,我就打什么
	x.bark()	//
}
func main(){
	var c1  cat
	var d1  dog
    var p1 person
}

综上,我们得出个结论,一个场景:

我不关心一个变量是什么类型,我只关心能调用它的什么方法。

0x01 接口(interface)

接口是一种类型,是一种特殊的类型,引用类型。它规定、约束了变量的方法。

在编程中会遇到以下场景:我不关心一个变量是什么类型,我只关心能调用它的什么方法。

type speaker interface{
    bark()
}

这里面只规定了你要实现什么方法,而没有指定具体的类型。

type dog struct{}
type cat struct{}
type person struct{}

type speaker interface {
	bark() //将方法统一归类,方法签名,可以有一个,也可以多个
}

func (c cat) bark() {
	fmt.Println("喵喵喵!")
}
func (d dog) bark() {
	fmt.Println("wangwangwnag")
}
func (p person) bark() {
	fmt.Println("Yingyingying!")
}

func beat(x speaker) {
	//接收一个参数,传进来什么,我就打什么
	x.bark() //
}
func main() {
	var c1 cat
	var d1 dog
	var p1 person
	beat(c1)
	beat(d1)
	beat(p1)
}

0x02 接口示例练习

多品牌跑车

type falali struct {
	brand string
}

func (f falali) run() {
	fmt.Println("速度七十迈!")
}

type baoma struct {
	brand string
}

func (b baoma) run() {
	fmt.Println("全力奔跑,许下心愿!")
}

type car interface {
	run()
}

//drive函数接收一个car类型的变量,即c为接口类型。
//这个接口类型,不管什么结构体,只要有run方法,都是car
func drive(c car) {
	c.run()
}

func main() {
	f1 := falali{brand: "法拉利"}
	b1 := baoma{brand: "宝马"}
	drive(f1)
	drive(b1)
}

小总结一下:只要有统一的方法,不管你是什么类型,都可以传入进来,用接口类型,去约束具体类型的方法。

多元支付方式

Go语言中的这种设计符合程序开发中抽象的一般规律,例如在下面的代码示例中,我们的电商系统最开始只设计了支付宝一种支付方式:

type ZhiFuBao struct {
	// 支付宝
}

// Pay 支付宝的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
  fmt.Printf("使用支付宝付款:%.2f元。\n", float64(amount/100))
}

// Checkout 结账
func Checkout(obj *ZhiFuBao) {
	// 支付100元
	obj.Pay(100)
}

func main() {
	Checkout(&ZhiFuBao{})
}

随着业务的发展,根据用户需求添加支持微信支付。

type WeChat struct {
	// 微信
}

// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
	fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}

在实际的交易流程中,我们可以根据用户选择的支付方式来决定最终调用支付宝的Pay方法还是微信支付的Pay方法。

// Checkout 支付宝结账
func CheckoutWithZFB(obj *ZhiFuBao) {
	// 支付100元
	obj.Pay(100)
}

// Checkout 微信支付结账
func CheckoutWithWX(obj *WeChat) {
	// 支付100元
	obj.Pay(100)
}

实际上,从上面的代码示例中我们可以看出,我们其实并不怎么关心用户选择的是什么支付方式,我们只关心调用Pay方法时能否正常运行。这就是典型的“不关心它是什么,只关心它能做什么”的场景。

在这种场景下我们可以将具体的支付方式抽象为一个名为Payer的接口类型,即任何实现了Pay方法的都可以称为Payer类型。

// Payer 包含支付方法的接口类型
type Payer interface {
	Pay(int64)
}

此时只需要修改下原始的Checkout函数,它接收一个Payer类型的参数。这样就能够在不修改既有函数调用的基础上,支持新的支付方式。

// Checkout 结账
func Checkout(obj Payer) {
	// 支付100元
	obj.Pay(100)
}

func main() {
	Checkout(&ZhiFuBao{}) // 之前调用支付宝支付

	Checkout(&WeChat{}) // 现在支持使用微信支付
}

0x03 接口详解

type 接口名 interface{
    方法名1(参数1,参数2...)(返回值1,返回值2...)
    方法名2(参数1,参数2...)(返回值1,返回值2...)
    ...
}

用来给变量\参数\返回值等设置类型用。

接口的实现

一个变量如果实现了接口中规定的所有的方法,那么这个变量就实现了这个接口,可以称为这个接口类型的变量。

还有就是我仅仅创建了两个方法,少了个fuck方法

image-20220227170600177

package main

import "fmt"

//1、先建立一个接口,动物。动物的行为方法在下方写上。
type animal interface {
	//1.1、这里面都是方法,所以括号里面可以添加任意的参数
	move()
	eat(s string) //eat(string)
}

//2、造动物
type cat struct {
	name string
	feet int8
}

//3、写两个猫的方法
func (c cat) move() {
	fmt.Println("走猫步!")
}
func (c cat) eat(food string) {
	fmt.Printf("猫吃:%s...\n", food)
}

type chicken struct {
	name string
	feet int8
}

//4、写两个鸡的方法
func (c chicken) move() {
	fmt.Println("鸡动!")
}
func (c chicken) eat(food string) {
	fmt.Printf("鸡吃:%s...", food)
}
func main() {
	//6、创建一个接口类型
	var a1 animal
	//5、创建一个猫
	bc := cat{
		name: "虹猫",
		feet: 4,
	}
	//7、这里将bc这个猫赋予给a1这个接口,但是无效,是因为
	//animal中的eat方法是带有string类型的参数的,而猫的eat方法无参数;还有就是我仅仅创建了两个方法,少了个fuck方法
	//所以a1 = bc失败,应该写成带有string参数的就好了
	a1 = bc
	a1.eat("小黄鱼") //一切正确后,输出:"猫吃:小黄鱼..."
	//fmt.Println(a1)

	//8、继续,新建一个鸡
	kfc := chicken{
		name: "战斗机!",
		feet: 2,
	}
	//9、将鸡赋予给a1这个接口,a1这个接口再调用里面的两个方法,最终成功输出
	a1 = kfc
	a1.move()
	a1.eat("虫子和沙子!")

}

image-20220227181330029

0x04 分析一下接口类型

看下面这张图,为什么p1不是接口类型,而是main.Japanesemain.Chinese

image-20220227200128483

接口保存的分为两部分,值的类型和值本身,我们打印出来的,是值的类型。所以上面的图,有两种类型,就是动态实现了。

这样才能保证接口能接受不同类型的数据和值。他只是一个约束性的,不是一个定量。

image-20220227200741082

main函数之外的方法,是值接收者。当main函数中,c2为指针类型,那么c2也是可以给a1进行赋值的。

type animal interface {
	move()
	eat(string)
}

type cat struct {
	name string
	feet int8
}
//这里的函数都是值接收者
func (c cat) move() {
	fmt.Println("走猫步...")
}

func (c cat) eat(food string) {
	fmt.Printf("猫吃%s...\n", food)
}
//我现在使用指针接收者
func main() {
	var a1 animal
	c1 := cat{"tom", 4}    //cat类型
	c2 := &cat{"jerry", 4} //*cat
	a1 = c1
	fmt.Println("a1:", a1)
	a1 = c2
	fmt.Println("a1_2:", a1)
}

image-20220301100632985

而如果main函数外面的函数都是指针接收者,那么main函数里面c1就无法给a1进行赋值了。而c2还可以赋值,因为c2就是指针类型的。

type animal interface {
	move()
	eat(string)
}

type cat struct {
	name string
	feet int8
}
//这里的函数都是指针接收者
func (c *cat) move() {
	fmt.Println("走猫步...")
}

func (c *cat) eat(food string) {
	fmt.Printf("猫吃%s...\n", food)
}
//我现在使用指针接收者
func main() {
	var a1 animal
	c1 := cat{"tom", 4}    //cat类型
	c2 := &cat{"jerry", 4} //*cat
	a1 = c1								//这里会报错!!!因为c1不是指针类型的!解决方案就是:a1 = &c1
	fmt.Println("a1:", a1)
	a1 = c2
	fmt.Println("a1_2:", a1)
}

使用值接收者和指针接收者的区别?

使用值接收者,实现接口、结构体类型和结构体指针类型的变量都能存。指针接收者实现接口只能存结构体指针类型的变量。

接口和类型的关系?

把接口当作一种约束即可

0x05 空接口

如果一个变量,实现了接口里面所有的方法,那么这个变量就实现了一个接口。

如果我这个变量什么都没有,相当于任何类型都实现这个接口。

没有必要起名字,就是空接口。通常定义成下面的格式:

interface{}		//空接口,没有必要使用type

所有的类型都实现了空接口,也就是任意类型的变量都能保存到空接口中。

空接口作为map的值

在学习map的时候,我们这样子定义和初始化map,能够看到,这里都是对应好的类型。string——>int,不能再使用别的类型了。

func main() {
	m1 := map[string]int{
		"name": 10,
		"fuck": 10,
		"shit": 30,
	}
	fmt.Println("m1", m1)
}

但是我们可以使用空接口来实现多种类型的map

func main() {
	m1 := map[string]interface{}{
		"name": 10,
		"fuck": "asdasd",
		"shit": []string{"nbeijing", "shanghai", "weishnida"},
	}
	fmt.Println("m1", m1)
}

空接口作为函数参数

同理,对于函数来说,我们传入的参数是有对应的类型的,不然就会报错。

func show(x int) {
	fmt.Printf("此类型为:%T, 此类型值为:%v\n, x, x)
}
func main() {
	show(10)
}

但是通过使用空接口作为函数参数,就可以达到万能的效果。

func show(x interface{}) {
	fmt.Printf("此类型为:%T, 此类型值为:%v\n", x, x)
}
func main() {
	show(10)
	show('a')
	show("asdad")
	show([...]string{"dad", "asd", "asd1"})
}

image-20220301165316214

类型断言

接口值可能赋值为任意类型的值,那我们如何从接口值获取其存储的具体数据呢?

我们可以借助标准库fmt包的格式化打印获取到接口值的动态类型。

var m Mover

m = &Dog{Name: "旺财"}
fmt.Printf("%T\n", m) // *main.Dog

m = new(Car)
fmt.Printf("%T\n", m) // *main.Car

fmt包内部其实是使用反射的机制在程序运行时获取到动态类型的名称。关于反射的内容我们会在后续章节详细介绍。

而想要从接口值中获取到对应的实际值需要使用类型断言,其语法格式如下。

x.(T)

其中:

  • x:表示接口类型的变量
  • T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个列子:如果对一个接口值有多个实际类型需要判断,推荐使用switch语句来实现。

//类型断言1
func assign(a interface{}) {
	fmt.Printf("%T\n", a)
	str, ok := a.(string)
	if !ok {
		fmt.Println("不好意思你猜错了!")
	} else {
		fmt.Println("传进来的是一个字符串!", str)
	}

}

//类型断言2
func assign2(a interface{}) {
	fmt.Printf("%T\n", a)
	switch t := a.(type) {
	case string:
		fmt.Println("是一个字符串:", t)
	case int:
		fmt.Println("是一个int", t)
	case int64:
		fmt.Println("是一个int64", t)
	case bool:
		fmt.Println("是一个bool", t)
	}

}

func main() {
	assign("hello")
	assign2("jhello!")
}

注意

由于接口类型变量能够动态存储不同类型值的特点,所以很多初学者会滥用接口类型(特别是空接口)来实现编码过程中的便捷。只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。

在 Go 语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。说白了,不要随便定义接口,只有多个结构体共同需要一个接口来约束时,再去使用这个约束,方法。

相信很多读者在刚接触到接口类型时都会有很多疑惑,请牢记接口是一种类型,一种抽象的类型。区别于我们在之前章节提到的那些具体类型(整型、数组、结构体类型等),它是一个只要求实现特定方法的抽象类型。

推荐阅读