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

news2024/11/19 5:38:46

文章目录

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

接口

在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类型的变量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)

在这里插入图片描述

接口值

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

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

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

在这里插入图片描述
定义了一个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 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)进行验证。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1141895.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Cross-modal Variational Alignment of Latent Spaces

方法 潜空间LS 辅助信息 作者未公布代码

matlab simulink 直线一级倒立摆控制(自起摆和稳态控制)

1、内容简介 略 6-可以交流、咨询、答疑 2、内容说明 控制器设计 自起摆建模 规定正方向:顺时针为角度(力矩)正方向,向右为位移正方向。 在规定的正方向条件下,图 1 所示摆杆的角度φ为正值, 下车向右加…

会议高清直播录播系统

怎么两个会场同步直播?部署一套会议高清直播录播系统。 鉴于线上线下同步培训,培训课件内容及互动情况都需要实时同步给终端学员,并且两个会场同步直播这些实时的培训画面。在局域网中,直播录播系统可以通过以下方式实现&#xf…

基于 51 的点阵屏显示 8*8 点阵仿真实验

点亮第一个 8*8 点阵 1. 首先在 Proteus 下选择我们需要的元件 AT89C52、74LS138、MATRIX-8*8-GREEN(在这里使用绿色的点阵)。在Proteus 6.9 中8*8 的点阵总共有四种颜色,分别为MATRIX-88-GREEN ,MATRIX-8*8-BLUE ,MATRIX-8*8-ORANGE,MATRIX-88-RED在这里请大家牢…

数组的最长递减子序列

求一个数组的最长递减子序列 如{9,4,3,2,5,4,3,2}的最长递减子序列为{9,5,4,3,2} 思路:动态规划 构建与原数组同等容量的辅助数组dp,记…

vue3学习(十四)--- vue3中css新特性

文章目录 样式穿透:deep()scoped的原理 插槽选择器:slotted()全局选择器:global()动态绑定CSScss module 样式穿透:deep() 主要是用于修改很多vue常用的组件库(element, vant, AntDesigin),虽然配好了样式但是还是需要更改其他的样式就需要用…

《合成孔径雷达成像算法与实现》Figure5.12

clc clear close all%% 距离向参数 R_eta_c 850e3; % 景中心斜距%% 方位向参数 V_r 7100; % 等效速度 T_a 0.64; …

测试开发面经:面了6家公司,最终收割3个Offer

上个月花了一周时间面了6家公司,最终收割3个Offer。通过这6次面试,得到的最宝贵的经验是:快刀斩乱麻。展开说就是:给自己一点点压力,在短时间内迅速面试、迅速反馈、迅速提高,然后迅速进入下一场面试。 以下…

STM32H750之FreeRTOS学习--------(一)初识RTOS

FreeRTOS 一、初识RTOS 裸机:裸机又称为前后台系统,前台系统指的中断服务函数,后台系统指的大循环,即应用程序 实时性差,程序轮流执行delayCPU空等待,效率低程序混乱,臃肿,功能都放在while循环…

1python模块和库

终于到模块和库了,有一点感觉小激动,但是!还是想继续努力,最近很多事情不是很顺,我加油学吧! 要调用模块,首先你要写一个封装成功的东西 1、先写一个库 class Tool:staticmethoddef t1():pri…

PHP递归实现无限级分类

什么是无限级分类? 无限级分类是一种对商品或信息进行分类的方式,在这种分类方式中,每个分类都可以再次细分出更多的子分类,形成无限的级别 应用场景: 一个电商网站的分类可以是:服装、鞋类、家居用品等…

使用Gateway解决跨域问题时配置文件不生效的情况之一

首先html文件只有一个发送ajax请求 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content&q…

二维码智慧门牌管理系统升级解决方案:要素类型

文章目录 前言一、点要素二、线要素三、面要素四、结语 前言 在二维码智慧门牌管理系统的升级中&#xff0c;我们着重解决了要素类型的问题。要素根据实体特征的不同&#xff0c;我们将其分为点、线、面三种要素类型。这样的分类方式使得管理更加科学、有序&#xff0c;大大提…

拜托,一定要让社区工作者刷到啊

社区工作者的福音来了&#xff0c;真的要试试啊 我愿称之为写作天花板&#xff01;只要输入你要写的内容&#xff0c;几秒钟就出来一篇&#xff0c;关键都不带重复的。 亲测好用&#xff0c;不用再绞尽脑汁的写方案&#xff0c;写总结啦&#xff0c;快试试啊

通过电商API接口分析电商平台客户消费行为

分析目的 通过电商API接口获取的数据分析销售数据来了解在线销售业务的消费情况&#xff0c;分析用户消费数据来分析用户的消费行为&#xff0c;为用户推荐相匹配的商品。 分析问题 店铺销售情况 每月成交额 每月销售金额 每月消费人数 每月订单数量 每月客单价 不同省…

鸡尾酒学习——环游世界

1、材料&#xff1a;白朗姆、龙舌兰、威士忌、金酒、伏特加、蓝橙力娇酒、柠檬汁、红石榴糖浆、橙汁、冰块&#xff1b; 2、口感&#xff1a;酸苦涩口味&#xff0c;下层感觉是在喝橙汁&#xff0c;上层在喝酒&#xff0c;适合喜欢喝橙汁以及酒的人&#xff0c;恰巧我不喜欢这两…

第五天:前端页面展示不出来

0.问题展示 但是我这个 展示不来 1.前端页面 渲染 不出来问题 首先想到的是&#xff0c;前端传值的问题然后排查到前端 传值是正确的传值的话&#xff0c;开始debugger 页面没有&#xff0c;然后找他的父页面&#xff0c;父页面是有值的然后找父 方法内部debugger发现第三步…

【疑问解决】在自动装箱中Integer赋予一个常量1,为什么会出现==判断true和flase的情况(JDK源码、内部缓冲)

问题来源自讲课时的Integer练习中 当时第一反应是false true true 因为第一段的输出为flase毋庸置疑了&#xff0c;因为已经new了两个新的堆空间&#xff0c;当然指向不同的空间了 但是第二段第三段就没有头绪了&#xff0c;自动装箱了难道不是执行同一个空间吗…

Go结构体接口反射

文章目录 Go结构体&接口&并发&反射一、结构体struct0、Type关键字1、struct定义及使用2、struct tag3、struct匿名成员3、struct继承4、struct方法 二、接口interface1、接口定义和使用2、空接口3、接口多态及嵌套 三、Go并发1、go协程2、chan管道3、单向管道4、Se…

oracle19c配置驱动

1.遇到的问题 下载jar包 https://www.oracle.com/database/technologies/appdev/jdbc-ucp-19c-downloads.html 执行命令 mvn install:install-file -DgroupIdcom.oracle -DartifactIdojdbc19 -Dversion19.3.0.0 -Dpackagingjar -Dfileojdbc8.jar2.配置驱动 # 数据源配置 data…