Golang接口使用教程详解

Golang
397
0
0
2023-03-12
目录
  • 前言
  • 一、概述
  • 二、接口类型
  • 2.1 接口的定义
  • 2.2 实现接口的条件
  • 2.3 为什么需要接口
  • 2.4 接口类型变量
  • 三、值接收者和指针接收者
  • 3.1 值接收者实现接口
  • 3.2 指针接收者实现接口
  • 四、类型与接口的关系
  • 4.1 一个类型实现多个接口
  • 4.2 多种类型实现同一接口
  • 五、接口嵌套
  • 六、空接口
  • 七、类型断言
  • 总结

前言

go语言并没有面向对象的相关概念,go语言提到的接口和java、c++等语言提到的接口不同,它不会显示的说明实现了接口,没有继承、子类、implements关键词。

一、概述

在 Go 语言中接口包含两种含义:它既是方法的集合, 同时还是一种类型。在Go 语言中是隐式实现的,意思就是对于一个具体的类型,不需要声明它实现了哪些接口,只需要提供接口所必需的方法。

go语言通过隐性的方式实现了接口功能,相对比较灵活。

Go语言接口的特点

  • interface 是方法或行为声明的集合
  • interface接口方式实现比较隐性,任何类型的对象实现interface所包含的全部方法,则表明该类型实现了该接口。
  • interface还可以作为一种通用的类型,其他类型变量可以给interface声明的变量赋值。
  • interface 可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值。

二、接口类型

2.1 接口的定义

每个接口类型由任意个方法签名组成,接口的定义格式如下:

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

说明

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

举个例子,定义一个包含Write方法的Writer接口。

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

2.2 实现接口的条件

接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。

示例

定义的Eater接口类型,它包含一个Eat方法。

// Eater 接口
type Eater interface {
	Eat()
}

有一个Dog结构体类型如下。

type Dog struct {}

因为Eater接口只包含一个Eat方法,所以只需要给Dog结构体添加一个Eat方法就可以满足Eater接口的要求。

//Dog类型的Eat方法
func (d Dog) Eat() {
	fmt.Println("吃骨头!")
}

这样就称为Dog实现了Eater接口。

完整代码

// Eater 接口
type Eater interface {
	Eat()
}

type Dog struct {}

//Dog类型的Eat方法
func (d Dog) Eat() {
	fmt.Println("吃骨头!")
}

func main() {
	dog := Dog{}
	dog.Eat()
}

2.3 为什么需要接口

多数情况下,数据可能包含不同的类型,却会有一个或者多个共同点,这些共同点就是抽象的基础。

示例

// Eater 接口
type Eater interface {
	Eat()
}

type Dog struct {}

//Dog类型的Eat方法
func (d Dog) Eat() {
	fmt.Println("狗狗喜欢吃骨头!")
}

type Cat struct {}

func (c Cat) Eat(){
	fmt.Println("小猫喜欢吃鱼!")
}

func main() {
	dog := Dog{}
	dog.Eat()
	cat := Cat{}
	cat.Eat()
}

从动物身上,可以抽象出来一个eat方法,这样即使在扩展其它动物进来,也只需要实现Eater 接口中的Eat()方法就可以完成对这个动作的调用。

接口可以理解为某一个方面的抽象,可以是多对一的(多个类型实现一个接口),这也是多态的体现。

2.4 接口类型变量

一个接口类型的变量能够存储所有实现了该接口的类型变量。

例如在上面的示例中,DogCat类型均实现了Eater接口,此时一个Eater类型的变量就能够接收CatDog类型的变量。

var x Eater // 声明一个Eater类型的变量x
a := Cat{}  // 声明一个Cat类型变量a
b := Dog{}  // 声明一个Dog类型变量b
x = a       // 可以把Cat类型变量直接赋值给x
x.Eat()     // 小猫喜欢吃鱼!
x = b       // 可以把Dog类型变量直接赋值给x
x.Eat()     // 狗狗喜欢吃骨头!

三、值接收者和指针接收者

通过下方一个示例来演示实现接口使用值接收者和使用指针接收者有什么区别。

定义一个Mover接口,它包含一个Move方法。

// Mover 定义一个接口类型
type Mover interface {
	Move()
}

3.1 值接收者实现接口

我们定义一个Dog结构体类型,并使用值接收者为其定义一个Move方法。

// Dog 狗结构体类型
type Dog struct{}

// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
	fmt.Println("狗会动")
}

此时实现Mover接口的是Dog类型。

var x Mover    // 声明一个Mover类型的变量x

var d1 = Dog{} // d1是Dog类型
x = d1         // 可以将d1赋值给变量x
x.Move()

var d2 = &Dog{} // d2是Dog指针类型
x = d2          // 也可以将d2赋值给变量x
x.Move()

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。

3.2 指针接收者实现接口

我们再来测试一下使用指针接收者实现接口有什么区别。

// Cat 猫结构体类型
type Cat struct{}

// Move 使用指针接收者定义Move方法实现Mover接口
func (c *Cat) Move() {
	fmt.Println("猫会动")
}

此时实现Mover接口的是*Cat类型,我们可以将*Cat类型的变量直接赋值给Mover接口类型的变量x

var c1 = &Cat{} // c1是*Cat类型
x = c1          // 可以将c1当成Mover类型
x.Move()

但是不能给将Cat类型的变量赋值给Mover接口类型的变量x

// 下面的代码无法通过编译
var c2 = Cat{} // c2是Cat类型
x = c2         // 不能将c2当成Mover类型

由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。但是我们并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意。

四、类型与接口的关系

4.1 一个类型实现多个接口

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

示例

动物不仅有吃的属性,还有动的属性,可以通过定义两个接口,让同一个动物分别实现这两种属性

// Eater 接口
type Eater interface {
	Eat()
}

// Mover 接口
type Mover interface {
	Move()
}

type Dog struct {}

//Dog类型的Eat方法
func (d Dog) Eat() {
	fmt.Println("狗狗喜欢吃骨头!")
}

//Dog类型的Move方法
func (d Dog) Move(){
	fmt.Println("狗狗喜欢玩耍!")
}

func main() {
	//初始化结构体
	dog := Dog{}

	//dog实现了Eater和Mover两个接口
	eat := dog
	move := dog

	eat.Eat()	//对Eater类型调用Eat方法
	move.Move()	//对Mover类型调用Move方法
}

程序中的结构体Dog分别实现了Eater和Mover两个接口中的方法。

4.2 多种类型实现同一接口

Go语言中不同的类型还可以实现同一接口。

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

// WashingMachine 洗衣机
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() {
	h := haier{}
	h.dry()
	h.wash()
}

五、接口嵌套

接口与接口之间可以通过互相嵌套形成新的接口类型。例如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。

六、空接口

Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口。

空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。

示例

func main() {
	//定义一个空接口x,x变量可以接收任意的数据类型
	var x interface{}
	str := "Hello Go"
	x = str
	fmt.Printf("type:%T,value:%v\n",x,x)

	num := 10
	x = num
	fmt.Printf("type:%T,value:%v\n",x,x)

	bool := true
	x = bool
	fmt.Printf("type:%T,value:%v\n",x,x)
}

运行结果

type:string,value:Hello Go
type:int,value:10
type:bool,value:true

1、空接口作为函数的参数

// 空接口作为函数参数
func show(a interface{}) {
	fmt.Printf("type:%T value:%v\n", a, a)
}

func main() {
	show(1)
	show(true)
	show(3.14)
	var mapStr = make(map[string]string)
	mapStr["name"] = "Leefs"
	mapStr["age"] = "12"
	show(mapStr)
}

运行结果

type:int value:1
type:bool value:true
type:float64 value:3.14
type:map[string]string value:map[age:12 name:Leefs]

2、map的值实现空接口

func main() {
	// 空接口作为 map 值
	var studentInfo = make(map[string]interface{})
	studentInfo["name"] = "Jeyoo"
	studentInfo["age"] = 18
	studentInfo["married"] = false
	fmt.Println(studentInfo)
}

运行结果

map[age:18 married:false name:Jeyoo]

3、切片实现空接口

func main() {
	var slice = []interface{}{"Jeyoo", 20, true, 32.2}
	fmt.Println(slice)
}

运行结果

[Jeyoo 20 true 32.2]

七、类型断言

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

如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式:

x.(T)

说明

  • x: 表示类型为 interface{}的变量
  • T: 表示断言 x 可能是的类型

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

示例

func main() {
	var x interface{}
	x = "Hello GO"
	v, ok := x.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("类型断言失败")
	}
}

上面的示例中如果要断言多次就需要写多个 if 判断,这个时候我们可以使用 switch 语句来 实现:

注意:类型.(type)只能结合 switch 语句使用

// justifyType 对传入的空接口类型变量x进行类型断言
func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string,value 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!")
	}
}

由于接口类型变量能够动态存储不同类型值的特点,所以很多初学者会滥用接口类型(特别是空接口)来实现编码过程中的便捷。

只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。

总结

在 Go 语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。

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