golang-study/notes/8.接口篇.md
2021-09-11 18:30:18 +08:00

18 KiB
Raw Permalink Blame History

本章目录:

  • 0X00 Go语言基础之接口

    • 一个类型实现多个接口
    • 多个类型实现同一接口
    • 1.接口类型
    • 2.接口的定义
    • 3.接口类型变量
    • 4.接口实现之值接收者和指针接收者
    • 5.接口与类型
    • 6.接口嵌套
    • 7.空接口
    • 8.接口之类型断言

img

0X00 Go语言基础之接口

Q: 在开发编程中您有可能遇到以下场景?

答: 我不关心变量是什么类型,只关心能调用它的什么方法,此时我们可以采用接口(Interface)类型进行解决相关问题。

1.接口类型

描述: 在Go语言中接口interface是一种类型一种抽象的类型, 其定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

如 interface 是一组 method 的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

Tips: 为了保护你的Go语言职业生涯请牢记接口interface是一种类型。

Q: 为什么要使用接口? 在我们编程过程中会经常遇到:

  • 比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
  • 比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
  • 比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?

例如:面的代码中定义了猫和狗然后它们都会叫你会发现main函数中明显有重复的代码如果我们后续再加上猪、青蛙等动物的话我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢

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())  // 狗: 汪汪汪
}

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

2.接口的定义

描述: Go语言提倡面向接口编程,每个接口由数个方法组成,接口的定义格式如下:

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    
}

参数说明:

  • 接口名使用type将接口定义为自定义的类型名。Go语言的接口在命名时一般会在单词后面添加er如有写操作的接口叫Writer有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名当方法名首字母是大写且这个接口类型名首字母也是大写时这个方法可以被接口所在的包package之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

基础示例:

type writer interface{
    Write([]byte) error
}

Tips: 当你看到这个接口类型的值时你不知道它是什么唯一知道的就是可以通过它的Write方法来做一些事情。

Tips :实现接口的条件, 即一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说接口就是一个需要实现的方法列表。

3.接口类型变量

Q: 那实现了接口有什么用呢?

答: 接口类型变量能够存储所有实现了该接口的实例,接口类型变量实际上你可以看做一个是一个合约。

基础示例:

// 定义一个接口类型writer的变量w。
var w writer // 声明一个writer类型的变量w

Tips 观察下面的代码体味此处_的妙用

// 摘自gin框架routergroup.go
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{}  // 确保RouterGroup实现了接口IRouter

示例演示:

package main

import "fmt"

// 接口声明定义以及约定必须实现的方法
type speaker interface {
	speak()
	eat(string)
}

// 人结构体
type person struct{ name, language string }
func (p person) speak() {
	fmt.Printf("我是人类,我说的是%v, 我叫%v\n", p.language, p.name)
}
func (p person) eat(food string) { fmt.Printf("喜欢的食物: %v\n", food) }

// 猫结构体
type cat struct{ name, language string }
func (c cat) speak() {
	fmt.Printf("动物猫,说的是%v, 叫%v\n", c.language, c.name)
}
func (c cat) eat(food string) { fmt.Printf("喜欢的食物: %v\n", food) }

// 狗结构体
type dog struct{ name, language string }
func (d dog) speak() {
	fmt.Printf("动物狗,说的是%v, 叫%v\n", d.language, d.name)
}
func (d dog) eat(food string) { fmt.Printf("喜欢的食物: %v\n", food) }

func talk(s speaker) {
	s.speak()
}

// (1) 接口基础使用演示
func demo1() {
	p := person{"WeiyiGeek", "汉语"}
	c := cat{"小白", "喵喵 喵喵..."}
	d := dog{"阿黄", "汪汪 汪汪...."}
	talk(p)
	talk(c)
	talk(d)
}

// (2) 接口类型的使用(可看作一种合约)方法不带参数以及方法带有参数
func demo2() {
	// 定义一个接口类型writer的变量w。
	var s speaker
	fmt.Printf("Type %T\n", s) // 动态类型

	s = person{"接口类型-唯一", "汉语"} // 动态值
	fmt.Printf("\nType %T\n", s) // 动态类型
	s.speak()
	s.eat("瓜果蔬菜")

	s = cat{"接口类型-小白", "喵喵..."} // 动态值
	fmt.Printf("\nType %T\n", s) // 动态类型
	s.speak()
	s.eat("fish")

	s = dog{"接口类型-阿黄", "汪汪..."} // 动态值
	fmt.Printf("\nType %T\n", s) // 动态类型
	s.speak()
	s.eat("bone")
}

func main() {
	demo1()
	fmt.Println()
	demo2()
}

执行结果:

我是人类我说的是汉语, 我叫WeiyiGeek
动物猫说的是喵喵 喵喵..., 叫小白
动物狗说的是汪汪 汪汪...., 叫阿黄

Type <nil>

Type main.person
我是人类我说的是汉语, 我叫接口类型-唯一
喜欢的食物: 瓜果蔬菜

Type main.cat
动物猫说的是喵喵..., 叫接口类型-小白
喜欢的食物: fish

Type main.dog
动物狗说的是汪汪..., 叫接口类型-阿黄
喜欢的食物: bone

注意: 带参数和不带参数的函数,在接口中实现的不是同一个方法,所以当某个结构体中没有完全实现接口中的方法将会报错。

4.接口实现之值接收者和指针接收者

Q: 使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?

    1. 值接收者实现接口: 结构体类型和结构体指针类型的变量都可以存储由于因为Go语言中有对指针类型变量求值的语法糖结构体指针变量内部会自动求值取指针地址中存储的值)。
    1. 指针接收者实现接口: 只能存储结构体指针类型的变量

我们通过下面的例子进行演示:

package main

import (
	"fmt"
)

// 接口类型声明
// (1) 值接收者实现接口
type Mover interface {
	move()
}
type dog struct{}
func (d dog) move() { fmt.Println("值接收者实现接口 -> 狗...移动....")  } // 关键点

// 使用值接收者实现接口之后不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量.
func demo1() {
	var m1 Mover
	var d1 = dog{} // 值类型
	m1 = d1        // m1可以接收dog类型的变量
	fmt.Printf("Type : %#v \n", m1)
	m1.move()

	var d2 = &dog{} // 指针类型
	m1 = d2         // x可以接收指针类型的(*dog)类型的变量
	fmt.Printf("Type : %#v \n", m1)
	m1.move()
}

// (2)指针接收者实现接口
type Runer interface{ run() }
type cat struct{}
func (c *cat) run() { fmt.Println("指针接收者实现接口 -> 猫...跑....") }
// 此时实现run接口的是*cat类型所以不能给m1传入cat类型的c1此时x只能存储*dog类型的值。
func demo2() {
	var m1 Runer
	var c1 = cat{}
	//m1不可以接收dog类型的变量
	// m1 = c1 // 报错信息: cannot use c1 (variable of type cat) as Runer value in assignment: missing method run (run has pointer receiver)compilerInvalidIfaceAssign
	fmt.Printf("Type : %#v \n", c1)

	//m1只能接收*dog类型的变量
	var c2 = &cat{}
	m1 = c2
	fmt.Printf("Type : %#v \n", c2)
	m1.run()
}
func main() {
	demo1()
	fmt.Println()
	demo2()
}

执行结果:

Type : main.dog{}
值接收者实现接口 -> ...移动....
Type : &main.dog{}
值接收者实现接口 -> ...移动....

Type : main.cat{}
Type : &main.cat{}
指针接收者实现接口 -> .......

面试题: 注意这是一道你需要回答“能”或者“不能”的题! 问: 首先请观察下面的这段代码,然后请回答这段代码能不能通过编译?

package main

import "fmt"

type People interface {
	Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
	if think == "man" {
		talk = "你好,帅哥"
	} else {
		talk = "您好,美女"
	}
	return
}

func main() {
	var peo People = Student{} // 此处为关键点
	think := "woman"
	fmt.Println(peo.Speak(think))
}

答案: 是不行会报 ./interface.go:21:6: cannot use Student{} (type Student) as type People in assignment: Student does not implement People (Speak method has pointer receiver) (exit status 2)错误,由于指针接收者实现接口必须是有指针类型的结构体实例化对象以及其包含的方法。

5.接口与类型

一个类型实现多个接口

描述: 一个结构体类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。

例如: 狗可以叫也可以动,我们就分别定义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 a = dog{name: "旺财"}
	var x Sayer = a // 将dog类型赋予给Sayer接口类型的变量x此时它可以调用say方法
	var y Mover = a // 将dog类型赋予给Mover接口类型的变量y此时它可以调用move方法
	x.say() // 旺财会叫 汪汪汪
	y.move() // 旺财会动
}

多个类型实现同一接口

描述: Go语言中不同的类型还可以实现同一接口,比如我们前面Person、Cat、Dog结构体类型中实现的Speak()方法。

例如我们定义一个Mover接口它要求结构体类型中必须有一个move方法, 如狗可以动,汽车也可以动。

// Mover 接口
type Mover interface { move() }
type dog struct { name string }
type car struct { brand string }
// dog类型实现Mover接口
func (d dog) move() {	fmt.Printf("%s会跑\n", d.name) }
// car类型实现Mover接口
func (c car) move() { fmt.Printf("%s速度120迈\n", c.brand) }
func main() {
	var x Mover
	var a = dog{name: "旺财"}
  x = a
	x.move() // 旺财会跑
	var b = car{brand: "保时捷"}
	x = b
	x.move() // 保时捷速度120迈
}

非常注意: 并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

示例演示:

package main

import "fmt"

// 接口类型
type android interface {
	telephone(int64)
	music()
}

// 结构体声明 实现music方法
type mp3 struct{}
// 实现接口中的方法
func (m *mp3) music() { fmt.Println("播放音乐.....")}

// 结构体声明
type mobilephone struct {
	production string
	mp3        // 嵌入mp3结构体并拥有它的方法
}

// 实现接口中的方法
func (mb *mobilephone) telephone(number int64) { fmt.Printf("%v 手机, 正在拨打 %v 电话....\n", mb.production, number)}

func main() {
	// android 接口类型
	var a android
	// 指针类型结构体变量mb
	var mp = &mobilephone{production: "小米"}
	a = mp
	fmt.Printf("Type : %#v\n", a) // android 接口类型变量输出
	a.telephone(10086)
	a.music()
}

执行结果:

Type : &main.mobilephone{production:"小米", mp3:main.mp3{}}
小米 手机, 正在拨打 10086 电话....
播放音乐.....

6.接口嵌套

描述: 接口与接口间可以通过嵌套创造出新的接口,嵌套得到的接口的使用与普通接口一样这里我们让cat实现animal接口。

示例演示:

// Sayer 接口
type Sayer interface {say()}
// Mover 接口
type Mover interface {move()}
// 接口嵌套
type animal interface {
	Sayer
	Mover
}
// cat 结构体
type cat struct {
	name string
}
// 接口方法的实现
func (c cat) say() {fmt.Printf("%v 喵喵喵",c.name)}
func (c cat) move() {fmt.Printf("%v 猫会动",c.name)}
func main() {
	var x animal
	x = cat{name: "花花"}
	x.move() //喵喵喵
	x.say()  //猫会动
}

7.空接口

空接口的定义 描述: 空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口, 该类型的变量可以存储任意类型的变量。他会在我们以后GO编程中常常出现。

例如:

// interface 是关键字,并不是类型。
// 方式1.但一般不会采用此种方式
var empty interface{}

// 方式2.我们可以直接忽略接口名称(空接口类型)
interface{}

空接口的应用

    1. 空接口作为函数的参数: 使用空接口实现可以接收任意类型的函数参数。
    1. 空接口作为map的值: 使用空接口实现可以保存任意值的字典。

示例演示:

package main

import "fmt"

// (1) 空接口作为函数参数
func showType(a interface{}) { fmt.Printf("参数类型:%T, 参数值:%v\n", a, a) }
func main() {
	// (2) 空接口作为map的值
	var m1 map[string]interface{}     // 类似于Java中的 Map<String,Object> m1
	m1 = make(map[string]interface{}) // 为Map申请一块内存空间
	// 可以存储任意类型的值
	m1["name"] = "WeiyiGeek"
	m1["age"] = 20
	m1["sex"] = true
	m1["hobby"] = [...]string{"Computer", "NetSecurity", "Go语言编程学习"}

	fmt.Printf("#空接口作为map的值\n%#v", m1)
	fmt.Println(m1)

	fmt.Printf("\n#空接口作为函数参数\n")
	showType(nil)
	showType([]byte{'a'})
	showType(true)
	showType(1024)
	showType("我是一串字符串")
}

执行结果:

#空接口作为map的值
map[string]interface {}{"age":20, "hobby":[3]string{"Computer", "NetSecurity", "Go语言编程学习"}, "name":"WeiyiGeek", "sex":true}
map[age:20 hobby:[Computer NetSecurity Go语言编程学习] name:WeiyiGeek sex:true]

#空接口作为函数参数
参数类型:<nil>, 参数值:<nil>
参数类型:[]uint8, 参数值:[97]
参数类型:bool, 参数值:true
参数类型:int, 参数值:1024
参数类型:string, 参数值:我是一串字符串

Tips : 因为空接口可以存储任意类型值的特点所以空接口在Go语言中的使用十分广泛。

8.接口之类型断言

描述: 空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

接口值 描述: 一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的,这两部分分别称为接口的动态类型动态值

我们来看一个具体的例子:

var w io.Writer
w = nil
w = os.Stdout
w = new(bytes.Buffer)

请看下图分解:

WeiyiGeek.动态类型与动态值

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:x.(T),其中:

    1. x表示类型为interface{}的变量
    1. T表示断言x可能是的类型。

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

示例演示

package main
import "fmt"
// 示例1.采用if进行判断断言
func assert(x interface{}) {
	v, ok := x.(string) // v 接受是string类型
	if ok {
		fmt.Printf("assert successful : %v, typeof %T\n", v, v)
	} else {
		fmt.Printf("assert failed 非 string 类型! : %v, typeof %T\n", x, x)
	}
}
func demo1() {
	var x interface{}
	x = "WeiyiGeek"
	assert(x) // assert successful : WeiyiGeek, typeof string
	x = 1024
	assert(x) // assert failed 非 string 类型! : 1024, typeof int
}

// 示例2.如果要断言多次就需要写多个if判断这个时候我们可以使用switch语句来实现
func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a stringvalue is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type")
	}
}
func demo2() {
	var x interface{}
	x = "i'm string"
	justifyType(x)
	x = 225
	justifyType(x)
	x = true
	justifyType(x)
}

func main() {
	demo1()
	fmt.Println()
	demo2()
}

执行结果:

assert successful : WeiyiGeek, typeof string
assert failed  string 类型! : 1024, typeof int

x is a stringvalue is i'm string
x is a int is 225
x is a bool is true

接口总结: 描述: 关于需要注意的是只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口,不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。