Golang入门笔记-08-接口

Golang入门笔记-08-接口

1. 使用接口的原因

我们来看一段代码:

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())
}

上述代码中定义了狗 Dog 和猫 Cat 以及它们对应的方法 Say(),若要继续添加鸭 Duck 和其对应的方法 Say(),重复代码会非常多。我们可不可以从这三种动物类型中抽象出方法 Say(),这就是接口的作用,接口可以定义抽象方法,来规范我们的代码。

2. 接口的定义和使用

Go 语言非传统面对对象语言,没有类和继承的概念。但在 Go 语言中可以通过接口来实现面对对象的一些特性,接口是用来描述对象行为的,可以在其中定义一些抽象方法,这些方法不能被实现,同时接口中不能包含变量

Go 语言中接口 interface 是一种抽象类型。

接口的定义格式:

type Namer interface {
	Method1(params) returnType
	Method2(params) returnType
}

一般接口以 [e]r 结尾,例如 WriterLogger 等。若 er 结尾不合适时,可以采用 able 结尾,例如 Recoverable

举个例子:

type Writer interface {
	Writer([]byte) error
}

若一个对象实现了接口的所有方法,那么就实现了这个接口。

我们来定义一个 Sayer 接口:

type Sayer interface {
	say()
}

定义 dogdat 两个结构体:

type dog struct{}

type cat struct{}

dogcat 实现接口 Sayersay 方法:

由于 Sayer 接口中只有一个 say 方法,所以 dogcat 实现了 Sayer 接口。

func (d dog) say() {
	fmt.Println("汪汪汪")
}

func (c cat) say() {
	fmt.Println("喵喵喵")
}

接口变量可以接收任何实现了该接口的实例,如:

func main() {
	var sayer Sayer // 声明接口类型变量 sayer
	c := cat{}
	d := dog{}

	sayer = c   // 把 cat 对应实例 c 赋值给 sayer
	sayer.say() // 通过接口变量调用 say 方法,等价于 c.say()

	sayer = d   // 把 dog 对应实例 d 赋值给 sayer
	sayer.say() // 通过接口变量调用 say 方法,等价于 d.say()
}

上述代码运行结果为:

喵喵喵
汪汪汪

3. 值接收和指针接收实现接口的区别

现在有一个 Sayer 接口和 Cat 结构体:

type Sayer interface {
	say()
}

type cat struct{}

3.1 值接收实现接口

func (c cat) say() {
	fmt.Println("喵喵喵")
}

创建值实例 c 和指针实例 c2,然后通过 Sayer 接口变量 sayer 来调用方法 say()

func main() {
	var sayer Sayer // 声明接口类型变量 sayer
	c := cat{}
	c2 := &cat{}

	sayer = c
	sayer.say()

	sayer = c2
	sayer.say()
}

上述代码运行结果为:

喵喵喵
喵喵喵

从上述例子中,我们可以发现:当以值接收形式实现接口时,不论是值实例还是指针实例,都可以赋值给接口变量。

3.2 指针接收实现接口

我们来看下指针接收来实现接口:

func (c *cat) say() {
	fmt.Println("喵喵喵")
}

func main() {
	var sayer Sayer // 声明接口类型变量 sayer
	c := cat{}      // cat 的实例
	c2 := &cat{}    // *cat 的实例

	sayer = c // 不能将 cat 的实例赋值给 sayer
	sayer.say()

	sayer = c2
	sayer.say()
}

运行后,编译器会提示 cat does not implement Sayer (say method has pointer receiver) 错误,因为实现 Sayer 接口的是 *cat,所以不能将 cat 的实例 c 赋值给接口变量 sayer ,也就是说 sayer 只能接收指针实例。

4. 类型和接口的关系

4.1 类型和接口的关系

一个类型实现多个接口

一个类型可以实现多个接口,且这些接口相互独立。动物既会叫,也会动,可以定义两个接口,然后让 cat 分别实现这两个接口:

package main

import "fmt"

type Sayer interface {
	say()
}

type Mover interface {
	move()
}

type cat struct{}

func (c cat) say() {
	fmt.Println("喵喵喵")
}

func (c cat) move() {
	fmt.Println("猫动了")
}

func main() {
	var sayer Sayer
	var mover Mover
	c := cat{}

	sayer = c
	mover = c
	sayer.say()
	mover.move()
}

上述代码运行结果为:

喵喵喵
猫动了
多个类型实现同一接口

一个接口可以被多个不同类型实现,现在有一个接口 Mover,猫 cat 和狗 dog 都会动,让它们分别实现 Mover 接口:

package main

import "fmt"

type Mover interface {
	move()
}

type cat struct{}

type dog struct{}

func (c cat) move() {
	fmt.Println("小猫咪溜了")
}

func (d dog) move() {
	fmt.Println("二狗子跑了")
}

func main() {
	var m Mover
	c := cat{}
	d := dog{}

	m = c
	m.move()
	m = d
	m.move()
}

上述代码运行结果为:

小猫咪溜了
二狗子跑了

一个接口的方法不一定要由一个类型完全实现,可以通过在类型中嵌入其他类型来实现,如:

// 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("洗刷刷")
}

5. 接口嵌套

接口可以通过嵌套生成新的接口,如:

// Sayer 接口
type Sayer interface {
	say()
}

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

// 接口嵌套
type animal interface {
	Sayer
	Mover
}

嵌套接口使用方式与普通接口一样,我们让 cat 来实现 animal 接口:

func (c cat) say() {
	fmt.Println("喵喵喵")
}

func (c cat) move() {
	fmt.Println("小猫咪溜了")
}

func main() {
	var a animal
	c := cat{}

	a = c
	a.say()
	a.move()
}

上述代码运行结果为:

喵喵喵
小猫咪溜了

6. 空接口

6.1 空接口的定义

空接口是指未定义任何方法的接口,因此任何类型都实现了空接口(空接口有点类似于 Java 中的基类 Object)。

空接口类型变量可以存储任意类型的值:

package main

import "fmt"

func main() {
	var i interface{}
	s := "空接口"
	n := 25
	b := true

	i = s
	fmt.Println(s)
	i = n
	fmt.Println(n)
	i = b
	fmt.Println(i)
}

上述代码运行结果为:

空接口
25
true

6.2 空接口的应用

函数传参

使用空接口可以接收任意类型的参数:

func show(x interface{}) {
	fmt.Printf("type:%T value:%v\n", x, x)
}
Map 的值

使用空接口可以实现保存任意类型的值的 Map:

var studentInfo = make(map[string]interface{})
studentInfo["name"] = "张三"
studentInfo["age"] = 24
studentInfo["married"] = false
fmt.Println(studentInfo)

7. 类型断言

我们如何获取空接口变量的值和具体类型呢?可以采用类型断言来实现。

一个接口的值是由 具体类型具体类型的值 组成的,分别称为接口的 动态类型动态值

我们来看一个例子:

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

来看下接口变量 w 的变化:

类型断言

若要判断空接口类型变量的值,可以使用类型断言,其格式为:

x.(T)
  • x 为空接口类型变量
  • T 表示断言 x 可能的类型

上述断言语句将返回两个参数,第一个参数为 x 转化为 T 类型后的值,第二个值为布尔型,若为 true 表明断言成功,若为 false 表明断言失败。

我们来看一个例子:

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

若要进行多次断言,可以采用 switch 语句:

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!")
	}
}

注意:当有多个类型存在相同的方法时才适合使用接口,不要为了刻意使用接口而使用接口,这样会增加运行时的损耗。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://cangmang.xyz/articles/1642848037115