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

2023-10-28 05:04

本文主要是介绍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接口。

在这里插入图片描述

在电商系统中我们允许用户使用多种支付方式(支付宝支付、微信支付、银联支付等),我们的交易流程中可能不太在乎用户究竟使用什么支付方式,只要它能提供一个实现支付功能的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类型的变量xvar 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 = ds.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 Moverobj = 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.gotype 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 {ReaderWriter
}// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {ReaderCloser
}// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {WriterCloser
}

对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。

接口也可以作为结构体的一个字段

Go标准库sort源码中的示例。

// src/sort/sort.go// Interface 定义通过索引对元素排序的接口类型
type Interface interface {Len() intLess(i, j int) boolSwap(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 Anyx = "你好" // 字符串型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"] = 18studentInfo["married"] = falsefmt.Println(studentInfo)

在这里插入图片描述

接口值

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

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

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

在这里插入图片描述
定义了一个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

在这里插入图片描述
我们可以使用m == nil来判断此时的接口值是否为空

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

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

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

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

在这里插入图片描述
然后,我们给接口变量m赋值为一个*Car类型的值。

这一次,接口值m的动态类型为*Car,动态值为nil
在这里插入图片描述
注意
此时接口变量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 Moverm = &Dog{Name: "旺财"}
fmt.Printf("%T\n", m) // *main.Dogm = 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的学习之路(十五)语法之接口(重要)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/290884

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

线性代数|机器学习-P36在图中找聚类

文章目录 1. 常见图结构2. 谱聚类 感觉后面几节课的内容跨越太大,需要补充太多的知识点,教授讲得内容跨越较大,一般一节课的内容是书本上的一章节内容,所以看视频比较吃力,需要先预习课本内容后才能够很好的理解教授讲解的知识点。 1. 常见图结构 假设我们有如下图结构: Adjacency Matrix:行和列表示的是节点的位置,A[i,j]表示的第 i 个节点和第 j 个

Node.js学习记录(二)

目录 一、express 1、初识express 2、安装express 3、创建并启动web服务器 4、监听 GET&POST 请求、响应内容给客户端 5、获取URL中携带的查询参数 6、获取URL中动态参数 7、静态资源托管 二、工具nodemon 三、express路由 1、express中路由 2、路由的匹配 3、路由模块化 4、路由模块添加前缀 四、中间件