Go语言基础之接口

接口类型

一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。

接口的定义

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

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

其中:

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

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

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

当你看到一个Writer接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write方法来做一些事情。

实现接口的条件

接口就是规定了一个需要实现的方法列表,在 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接口。

通过接口实现多态

现在假设我们的代码世界里有很多小动物,下面的代码片段定义了猫和狗,它们饿了都会叫。

type Sayer interface {
    Say()
}

定义多个小动物结构体:

type Cat struct{}

func (c Cat) Say() {
	fmt.Println("喵喵喵~")
}

type Dog struct{}

func (d Dog) Say() {
	fmt.Println("汪汪汪~")
}

 这个时候调用Say可以通过多态方式

// MakeHungry 饿肚子了...
func MakeHungry(s Sayer) {
	s.Say()
}

接口类型变量

那实现了接口又有什么用呢?一个接口类型的变量能够存储所有实现了该接口的类型变量。

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

var x Sayer // 声明一个Sayer类型的变量x
a := Cat{}  // 声明一个Cat类型变量a
b := Dog{}  // 声明一个Dog类型变量b
x = a       // 可以把Cat类型变量直接赋值给x
x.Say()     // 喵喵喵
x = b       // 可以把Dog类型变量直接赋值给x
x.Say()     // 汪汪汪

值接收者和指针接收者

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

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

值接收者实现接口

我们定义一个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()

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

指针接收者实现接口

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

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

总结:其实这里还是针对的是go语言的指针变量和普通变量的区别。

类型与接口的关系

一个类型实现多个接口

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

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

上面的代码执行结果如下:

旺财会跑
宝马速度70迈

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

// 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。

此外在Go内置标准库database/sql中也有很多类似的结构体内嵌接口类型的使用示例,各位读者可自行查阅。

空接口

空接口的定义

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

package main

import "fmt"

// 空接口

// 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的值

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

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

接口值

由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体之外,还需要记录这个值属于的类型。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型动态值

接口值示例

我们接下来通过一个示例来加深对接口值的理解。

下面的示例代码中,定义了一个Mover接口类型和两个实现了该接口的DogCar结构体类型。

type Mover interface {
	Move()
}

type Dog struct {
	Name string
}

func (d *Dog) Move() {
	fmt.Println("狗在跑~")
}

type Car struct {
	Brand string
}

func (c *Car) Move() {
	fmt.Println("汽车在跑~")
}

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

var m Mover

此时,接口变量m是接口类型的零值,也就是它的类型和值部分都是nil,就如下图所示。

接口值示例

我们可以使用m == nil来判断此时的接口值是否为空。

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

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

m.Move() // panic: runtime error: invalid memory address or nil pointer dereference

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

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

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

接口值示例

然后,我们给接口变量m赋值为一个*Car类型的值。

var c *Car
m = c

这一次,接口值m的动态类型为*Car,动态值为nil

接口值示例

**注意:**此时接口变量mnil并不相等,因为它只是动态值的部分为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!")
	}
}

由于接口类型变量能够动态存储不同类型值的特点,所以很多初学者会滥用接口类型(特别是空接口)来实现编码过程中的便捷。只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。

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

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

小技巧: 下面的代码可以在程序编译阶段验证某一结构体是否满足特定的接口类型。

// 摘自gin框架routergroup.go
type IRouter interface{ ... }

type RouterGroup struct { ... }

var _ IRouter = &RouterGroup{}  // 确保RouterGroup实现了接口IRouter

上面的代码中也可以使用var _ IRouter = (*RouterGroup)(nil)进行验证。

参考文章:

https://www.fansimao.com/1006428.html

https://www.fansimao.com/1006445.html

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

强化学习原理python篇08——actor-critic

强化学习原理python篇08——actor-critic 前置知识TD ErrorREINFORCEQACAdvantage actor-critic (A2C) torch实现步骤第一步第二步第三步训练结果 Ref 本章全篇参考赵世钰老师的教材 Mathmatical-Foundation-of-Reinforcement-Learning Actor-Critic Methods 章节,请…

C#小结:ScottPlot 5.0在VS2022桌面开发的应用(以winform为例)

目录 一、官网文档地址 二、在VS2022中安装Scottplot 三、拖动Scottplot 四、使用Scottplot 五、效果图 一、官网文档地址 官网地址:ScottPlot 5.0 食谱 本文内容来自于官网,选取了官网的一些比较好用的功能展示,如需学习更多功能&a…

个人建站前端篇(二)项目采用服务端渲染SSR

SSR的优点 更好的SEO首屏加载速度更快,用户体验更好可以使用相同的语言以及相同的声明式、面向组件的心智模型来开发整个应用,而不需要在后端模板系统和前端框架之间来回切换。 Vue生态中的SSR通用解决方案 Nuxt是一个构建于 Vue 生态系统之上的全栈框…

虚拟机扩容后黑屏卡死解决方法

亲测有效,首先一般是在扩容后黑屏的,现象为开机后看到个横线光标不闪,黑屏,进入不了桌面。原因是硬盘已经满了,所以解决方法就是清理硬盘。所以首先还是要解决登录问题。 开机时按 esc 键进入 GNU GRUB,选择…

C#网络爬虫之TianyaCrawler实战经验分享

互联网时代的到来带来了大量的数据,而网络爬虫技术成为了获取这些数据的重要途径之一。如果你是一名C#开发者,那么你可能会对TianyaCrawler这个强大的网络爬虫框架感兴趣。本文将带你深入了解TianyaCrawler,分享它的技术概况、使用场景&#…

PHP集成开发环境 PhpStorm 2023 for mac中文激活版

PhpStorm 2023 for Mac是一款功能强大的PHP集成开发环境(IDE),旨在帮助开发者更高效地编写、调试和测试PHP代码。该软件针对Mac用户设计,提供了丰富的功能和工具,以简化开发过程并提高开发效率。 软件下载:…

如何用MapTalks IDE来发布网站?

简介 MapTalks IDE 全称 MapTalks集成设计环境(Integrated Design Environment),是由MapTalks技术团队开发的新一代web地图设计软件。 通过MapTalks IDE,您可以自由的创建二维和三维地图,在其中载入或创建地理数据&a…

【Vue】2-8、Axios 网络请求

cdn&#xff1a;<script src"https://unpkg.com/axios/dist/axios.min.js"></script> 注&#xff1a;使用 CDN 链接就可以不需要去下载对应的 js 文件到本地&#xff0c;只需要联网即可使用&#xff0c;可以减少项目的体积 <!DOCTYPE html> <…

开阳630hv100的代码编译以及软件制作步骤

打开项目功能步骤&#xff1a; 编译awtk功能&#xff1a; 选中awtk工程&#xff0c;先编译一次awtk sdk&#xff08;如下图的3和4步骤&#xff09;&#xff1b; 编译项目代码&#xff08;如下图步骤5和6&#xff09;&#xff1b; 编译完成后&#xff0c;软件路径&#xff1a;…

CORE伊士曼服务平台重磅发布CORE Pattern数字机裁功能

中国&#xff0c;上海&#xff0c;2024年1月——近日&#xff0c;全球特种材料公司伊士曼宣布&#xff0c;全新汽车数字化机裁功能——CORE Pattern&#xff0c;现已登陆CORE伊士曼服务平台。这项创新数字解决方案&#xff0c;专为中国本土的高性能膜市场开发&#xff0c;旨在打…

(十六)串口UART

文章目录 UART简介传输数据帧和波特率定时器1作为串口1波特率发生器串口部分相关寄存器TMODAUXRPCONSCONSBUF 串口1工作模式1&#xff1a;8位UART&#xff0c;波特率可变总体工作原理如何简单接收一个字符和发送数据一步之遥的设置现象演示 UART简介 通用异步收发传输器(Unive…

使用Logstash将MySQL中的数据同步至Elasticsearch

目录 1 使用docker安装ELK 1.1 安装Elasticsearch 1.2 安装Kibana 1.3 安装Logstash 2 数据同步 2.1 准备MySQL表和数据 2.2 运行Logstash 2.3 测试 3 Logstash报错(踩坑)记录 3.1 记录一 3.1.1 报错信息 3.1.2 报错原因 3.1.3 解决方案 3.2 记录二 3.2.1 报错信…

榜单!高阶智驾冲刺10%搭载率,哪些玩家占据自研感知「高地」

得「感知」者&#xff0c;是智能化尤其是智能驾驶技术变革快速演进期的受益者。尤其是对于车企来说&#xff0c;规控自研易&#xff0c;感知自研难。 尤其是过去几年时间&#xff0c;基于机器学习和深度学习&#xff0c;TransformerBEV技术进一步提高对异常行为的预测准确性&am…

证券开户怎么联系专属客户经理?新手必看!

证券开户联系专属客户经理的方式有很多&#xff0c;可以通过手机网上找客户经理&#xff0c;现在这种方式是最多的&#xff0c;比如咱们网站都是各大券商专业的客户经理&#xff0c;在线联系就可以帮您安排。您自己也可以挑选自己觉得好的券商和客户经理&#xff0c;然后再沟通…

文本生成高清、连贯视频,谷歌推出时空扩散模型

谷歌研究人员推出了创新性文本生成视频模型——Lumiere。 与传统模型不同的是&#xff0c;Lumiere采用了一种时空扩散&#xff08;Space-time&#xff09;U-Net架构&#xff0c;可以在单次推理中生成整个视频的所有时间段&#xff0c;能明显增强生成视频的动作连贯性&#xff…

基于MongoDB实现聊天记录的存储

一、mongodb简介 1.1 mongodb简介 MongoDB是一个基于分布式文件存储的数据库&#xff0c;使用C语言编写。它旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB介于关系数据库和非关系数据库之间&#xff0c;是非关系数据库当中功能最丰富、最像关系数据库的。 Mong…

oracle 结果集操作符(求交集、并集、差集)

结果集的操作符 求并集&#xff1a;将两个结果集合并成一个结果集返回 union是求并集去重 union all是求并集不去重 select 1 as A from dual union select 1 as B from dual; select 1 as A from dual union all select 1 as B from dual;求交集&#xff1a;将两个结果集中公…

Unity 访问者模式(实例详解)

文章目录 实例1&#xff1a;简单的形状与统计访客实例2&#xff1a;游戏对象组件访问者实例4&#xff1a;Unity场景对象遍历与清理访客实例5&#xff1a;角色行为树访问者 访问者模式&#xff08;Visitor Pattern&#xff09;在Unity中主要用于封装对一个对象结构中各个元素的操…

【开发实践】python使用 moviepy实现mp4转gif(含可视化界面)

一、分析介绍 MoviePy是一个用于视频编辑和处理的Python库。它提供了一种简单而直观的方式来创建、编辑和合成视频&#xff0c;同时也支持添加音频和图像。 以下是MoviePy的一些主要功能和用法示例&#xff1a; 创建视频剪辑&#xff1a; from moviepy.editor import VideoFi…

批量将本地图片转为webp

-I 后是当前图片的路径目录 -O 后是 输出的webp目录 npx webp-batch-convert -I images -O images_webp npx webp-batch-convert -I icon -O icon_webp 在 assets 目录内 执行上面的命令 直接转换