GoLong的学习之路(十五)语法之接口(重要)

文章目录

  • 接口
    • 接口的定义
    • 实现接口的条件
    • 面向接口编程
    • 接口类型的变量
    • 值接收者和指针接收者
      • 接口
      • 值接收
      • 指针接收
      • 总结
    • 类型和接口的关系(多对多的关系)
      • 一个类型实现多个接口
      • 多种类型实现同一接口
    • 接口组合
        • `注意`
    • 空接口
      • 空接口的应用
  • 接口值
    • 类型断言
    • `注意`
      • 总结

接口

在Go语言中接口(interface)是一种类型,一种抽象的类型。(大差不差都,比如Java)

相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”

接口类型更注重“我能做什么”的问题。

接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。

接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。

当我们使用接口类型说明相比于它是什么更关心它能做什么(类似于封装)

接口的定义

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

实现接口的条件

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

首先定义一个Singer接口,它包含一个sing方法

// Singer 接口
type Singer interface {
	Sing()
}

我们有一个Bird 结构体类型如下:

type Bird struct {

}

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

// Sing Bird类型的Sing方法
func (b Bird) Sing() {
	fmt.Println("汪汪汪")
}

这样就称为Bird实现了Singer接口。

GoLong的学习之路(十五)语法之接口(重要)_第1张图片

在电商系统中我们允许用户使用多种支付方式(支付宝支付、微信支付、银联支付等),我们的交易流程中可能不太在乎用户究竟使用什么支付方式,只要它能提供一个实现支付功能的Pay方法让调用方 调用就好了。用户无需体验选卡的操作
(支付宝在购买时会根据你的卡里钱的额度是否正确,正确扣除,不正确,去寻找下一张卡)

面向接口编程

Java中也有接口的概念,只不过在java中需要显式声明一个类实现了那些接口,在go语言中使用隐式声明的方式实现接口。

只要一个类型(结构体)实现了接口中规定的所有方法,那么他就实现了这个接口。

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

之前是。支付宝支付占据天下,但是微信支付异军突起,也有很多用户使用微信支付,为了让增加客户的数量。那么微信支付也需要我们添加进入支付的功能中。

我们可以根据用户选择的支付方式来决定最终调用支付宝的Pay方法还是微信支付的Pay方法。
(选择使用哪个函数,实现支付)

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

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

在这种场景下我们可以将具体的支付方式抽象为一个名为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{}) // 现在支持使用微信支付
}

接口类型是Go语言提供的一种工具,在实际的编码过程中是否使用它由你自己决定,但是通常使用接口类型可以使代码更清晰易读。

接口类型的变量

那实现了接口又有什么用呢?一个接口类型的变量能够存储所有实现了该接口的类型变量。
在上面的示例中,WeChat 和 ZhiFuBao 类型实现了Payer 接口 ,那么此时一个Payer类型的变量就能接收这两种类型的变量。

var x Payer // 声明一个Payer 类型的变量x
a := WeChat {}  // 声明一个WeChat 类型变量a
b := ZhiFuBao {}  // 声明一个ZhiFuBao 类型变量b
x = a       // 可以把Cat类型变量直接赋值给x
x.Pay()     // 使用微信支付
x = b       // 可以把Dog类型变量直接赋值给x
x.Pay()     // 使用支付宝支付

值接收者和指针接收者

在定义结构体方法时既可以使用值接收者也可以使用指针接收者。那么对于实现接口说使用值接收和使用指针接收的区别?

其实二者达到的效果差不多。

且看:

接口

我们定义一个关于移动的接口

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

值接收

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

// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
	fmt.Println("狗会动")
}
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()

不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。

指针接收

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

总结

所以对于是值接收还是指针接收实现的接口都没有什么问题,但是我们并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意。


类型和接口的关系(多对多的关系)

一个类型实现多个接口

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

例如狗不仅可以叫,还可以动。

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

同一个类型实现不同的接口互相不影响使用。

var d = Dog{Name: "旺财"}

var s Sayer = d
var m Mover = d

s.Say()  // 对Sayer类型调用Say方法
m.Move() // 对Mover类型调用Move方法

多种类型实现同一接口

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

例如在我们的代码世界中不仅狗可以动,汽车也可以动。

// 实现Mover接口
func (d Dog) Move() {
	fmt.Printf("%s会动\n", d.Name)
}

// Car 汽车结构体类型
type Car struct {
	Brand string
}

// Move Car类型实现Mover接口
func (c Car) Move() {
	fmt.Printf("%s速度70迈\n", c.Brand)
}

这样我们在代码中就可以把狗和汽车当成一个会动的类型来处理,不必关注它们具体是什么,只需要调用它们的Move方法就可以了。

var obj Mover

obj = Dog{Name: "旺财"}
obj.Move()

obj = Car{Brand: "宝马"}
obj.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("洗刷刷")
}

接口组合

接口与接口之间可以通过互相嵌套形成新的接口类型

例如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。

空接口

空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。

// 空接口
// Any 不包含任何方法的空接口类型
type Any interface{}
// Dog 狗结构体
type Dog struct{}
func main() {
	var x Any

	x = "你好" // 字符串型
	fmt.Printf("type:%T value:%v\n", x, x)
	x = 100 // int型
	fmt.Printf("type:%T value:%v\n", x, x)
	x = true // 布尔型
	fmt.Printf("type:%T value:%v\n", x, x)
	x = Dog{} // 结构体类型
	fmt.Printf("type:%T value:%v\n", x, x)
}

通常我们在使用空接口类型时不必使用type关键字声明,可以像下面的代码一样直接使用interface{}

var x interface{}  // 声明一个空接口类型变量x

空接口的应用

使用空接口实现可以接收任意类型的函数参数。

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

使用空接口实现可以保存任意值的字典。

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

在这里插入图片描述

接口值

什么是接口值?
由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值之外,还需要记录这个值属于的类型。

也就是说接口值由“类型”“值”组成。

鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型动态值

GoLong的学习之路(十五)语法之接口(重要)_第2张图片
定义了一个Mover接口类型和两个实现了该接口的Dog和Car结构体类型。

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

//结构体Dog
type Dog struct {
	Name string
}
//定义的结构体的方法
func (d *Dog) Move() {
	fmt.Println("狗在跑~")
}
//结构体Car
type Car struct {
	Brand string
}
//定义的结构体的方法
func (c *Car) Move() {
	fmt.Println("汽车在跑~")
}

首先,我们创建一个Mover接口类型的变量m

var m Mover

此时,接口变量m是接口类型的零值,也就是它的类型和值部分都是nil

GoLong的学习之路(十五)语法之接口(重要)_第3张图片
我们可以使用m == nil来判断此时的接口值是否为空

注意:我们不能对一个空接口值调用任何方法,否则会产生panic

接下来,我们将一个*Dog结构体指针赋值给变量 m

m = &Dog{Name: "旺财"}

此时,接口值m的动态类型会被设置为*Dog,动态值为结构体变量的拷贝。

GoLong的学习之路(十五)语法之接口(重要)_第4张图片
然后,我们给接口变量m赋值为一个*Car类型的值。

这一次,接口值m的动态类型为*Car,动态值为nil
GoLong的学习之路(十五)语法之接口(重要)_第5张图片
注意
此时接口变量m与nil并不相等,因为它只是动态值的部分为nil,而动态类型部分保存着对应值的类型。

fmt.Println(m == nil) // false

接口值是支持相互比较的,当且仅当接口值的动态类型和动态值都相等时才相等。

var (
	x Mover = new(Dog)
	y Mover = new(Car)
)
fmt.Println(x == y) // false

但是有一种特殊情况需要特别注意,如果接口值保存的动态类型相同,但是这个动态类型不支持互相比较(比如切片),那么对它们相互比较时就会引发panic。

var z interface{} = []int{1, 2, 3}
fmt.Println(z == z) // panic: runtime error: comparing uncomparable type []int

类型断言

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

我们可以借助标准库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则表示断言失败。
var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
	fmt.Println("类型断言成功")
	v.Name = "富贵" // 变量v是*Dog类型
} else {
	fmt.Println("类型断言失败")
}

如果对一个接口值有多个实际类型需要判断,推荐使用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!")
	}
}

注意

由于接口类型变量能够动态存储不同类型值的特点,很多人会滥用接口类型(特别是空接口)来实现编码。

其实这种情况最好是在当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。

切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。

缺点

缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。

总结

接口是一种类型,一种抽象的类型。区别于我们在之前章节提到的那些具体类型(整型、数组、结构体类型等),它是一个只要求实现特定方法的抽象类型。

技巧

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

也可以使用var _ IRouter = (*RouterGroup)(nil)进行验证。

你可能感兴趣的:(GoLong,学习,golang)