一、概述

接口是面向对象编程的重要概念,接口是对行为的抽象和概括,在主流面向对象语言Java、C++,接口和类之间有明确关系,称为“实现接口”。这种关系一般会以“类派生图”的方式进行,经常可以看到大型软件极为复杂的派生树,随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
Go语言接口模型非常特别,就目前观察是独创。go接口设计是非侵入式,只要类型方法是接口方法的超集,那么就认为类型实现了接口,两者之间不需要显示关联,当然也没有implements关键字,称为隐式实现。相比Java、C++主流面向对象语言需要显示实现接口,go的方式更加灵活、松散、耦合更低,当然也更加隐晦、代码可读性降低,在实际开发中体验也不如 Java、C++好。在不修改类型定义情况下,可以为其添加接口,这在Java、C++下是不可思议的。go的接口满足鸭子模型,所谓鸭子类型:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。
这种设计解决一些问题,松散的关系不再有复杂派生树,看似降低了复杂度。但是带来了一些列新问题,如接口不支持默认实现,修改接口定义隐式破坏实现关系,面向接口编程场景下容易类型错误。降低影响方法就是接口抽象粒度尽量小,这样又降低了接口的抽象能力,导致架构碎片化,不利于大型软件架构设计。综合考虑 Go 接口模型设计是否优秀见仁见智了。

二、基本使用

接口定义,描述一堆方法的集合,给出方法声明即可,不能有默认实现,也不能有变量

  1. type User interface {
  2. Say()
  3. GetName() string
  4. }

定了一个user接口,包含两个方法

任何类型都可以实现这两个方法,不需要显示使用implements关键字。满足两个条件,与接口方法签名完全一致,是接口方法的超集。即可判定类型实现了接口。

  1. type Sales struct { // 定义类型
  2. name string
  3. }
  4. func (p *Sales) GetName() string { // 接口方法一
  5. return p.name
  6. }
  7. func (p *Sales) Say() { // 接口方法二
  8. fmt.Println("Hi, I'm", p.name)
  9. }
  10. func (p *Sales) peddle() {
  11. fmt.Printf("%s peddle", p.name)
  12. }

Sales类型满足两个条件,可判断实现了User接口。从代码上看两者没有直接关联,这就是隐式实现。

按照上面的两个条件,Sales也实现了如下接口

  1. type Person interface {
  2. GetName() string
  3. }

可以看到非常松散,就是这么简洁。再次强调只要满足两个条件:与接口方法签名一致,是接口方法的超集,即可判定类型实现了接口。从类型自身角度看,完全不知道自己实现了哪些接口。

通过实例调用方法

  1. var sales Sales = Sales{name: "tom"}
  2. sales.Say()

通过接口调用方法,只要类型实现了接口,就可以赋值给接口变量,并使用接口调用方法

  1. var user User = &Sales{name: "tom"} // 赋值给接口变量,注意是地址
  2. user.Say() // 通过接口调用方法
  3. fmt.Printf("%T\n", user) // *main.Sales

重要:接口是引用类型,和指针一样,只能指向实例的地址。

接口主要目标是解耦,通常称为面向接口编程,主流使用方式,函数形参是接口类型,调用时候传递接口变量,这也是接口存在的意义。

  1. func PrintName(user User) { // 形参是User接口类型
  2. fmt.Println("姓名:", user.GetName())
  3. }
  4. var sales User = &Sales{name: "tom"}
  5. PrintName(sales) // 传入user接口变量

形参是接口类型,可传入所有实现了该接口的实例,不在依赖具体类型。

在标准库中大量使用接口。比如排序是普片需求,标准库提供了排序函数,形参是接口类型,任何实现了该接口的类型,都可直接使用排序函数

  1. type Interface interface { // 排序接口
  2. Len() int
  3. Less(i, j int) bool
  4. Swap(i, j int)
  5. }
  6. func Sort(data Interface) { // 标准库排序函数
  7. ...
  8. }

和结构体一样,接口也支持继承特性

  1. type User interface {
  2. Say()
  3. GetName() string
  4. }
  5. type Admin interface {
  6. User // 继承User接口
  7. TeamName string // 自有属性
  8. }

需要实现包括继承的所有方法,才判定实现了该接口

其他自定义类型也可以实现接口,如下X类型实现了Plus接口

  1. type Plus interface {
  2. incr()
  3. }
  4. type X int
  5. func (x *X) incr() {
  6. *x += 1
  7. fmt.Println(*x)
  8. }

三、接口断言

在接口变量上操作,用于检查接口类型变量是否实现了期望的接口或者具体的类型。使用接口的本质,就是实例类型和接口类型之间转换,而是否允许转换就依赖接口断言,也可称为显示类型转换

  1. value, ok := x.(T)

x 表示接口变量,T 表示具体类型(也可是接口类型),可根据该布尔值判断 x 是否为 T 类型。

  • 如果 T 是实例类型,类型断言会检查 x 的动态类型是否满足 T。如果成功返回 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果成功返回 值是 T 的接口值。
  • 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。

简单总结,尝试把 x 转换为 T 类型,赋值给 value 变量。

使用上面案例进行断言

  1. var tony User = &Sales{name: "tony"} // tony 赋值给接口变量
  2. if value, ok := tony.(User); ok { // true, 接口类型断言
  3. value.Say()
  4. }
  5. _, ok = tony.(*Sales) // true, 具体类型断言, 注意这里使用了指针类型

注意如果不接收第二个返回值(也就是 ok),断言失败时会直接造成一个 panic,对nil断言同样也会 panic。

  1. admin := user.(Admin) // Admin是管理员接口,断言失败panic

具体类型变量断言,可以先转为为接口,然后再进行断言

  1. user1 := Sales{"tom"}
  2. var data interface{} = user1 // 转换为空接口变量
  3. if _, ok := data.(Sales); ok { // 再进行断言
  4. fmt.Println("yes")
  5. }

断言常见使用场景,异常捕获时判定错误类型

  1. func ProtectRun(entry func()) {
  2. defer func() {
  3. err := recover() // 获取错误类型
  4. switch err.(type) { // 断言错误类型, 不同类型的错误采取不同的处理方式
  5. case runtime.Error:
  6. fmt.Println("runtime error:", err)
  7. default:
  8. fmt.Println("error:", err)
  9. }
  10. }()
  11. ...
  12. }

四、接口转换

go语言基本数据类型转换比较严格,所有基础类型不支持隐式转换,如下案例都不支持

  1. s := "a" + 1
  2. // 不同长度的整型, 也支持自动转换
  3. var i int = 10
  4. var n int8 = 20
  5. m := i+n

go只能显示转换

  1. s := "a" + string(1) // a1
  2. var i int = 10
  3. var n int8 = 20
  4. m := i + int(n) // 30

使用接口的本质就是类型转换,赋值时转换为接口变量,执行时候转换为实例。 go 语言对于接口类型的转换则非常的灵活,实例和接口之间的转换、接口和接口之间的转换都可能是隐式的转换

  1. var f os.File
  2. var a io.ReadCloser = &f // 隐式转换, *os.File 满足 io.ReadCloser 接口
  3. var b io.Reader = a // 隐式转换, io.ReadCloser 满足 io.Reader 接口
  4. var c io.Closer = a // 隐式转换, io.ReadCloser 满足 io.Closer 接口
  5. var d io.Reader = a.(io.Reader) // 显式转换(断言), io.Closer 不满足 io.Reader 接口,注意这里没有接收第二个参数,失败会panic

有时候对象和接口之间太灵活了,导致需要人为地限制这种无意之间的适配。常见的做法是定义一个含特殊方法来区分接口。比如 runtime 包中的 Error 接口就定义了一个特有的 RuntimeError 方法,用于避免其它类型无意中适配了该接口

  1. type runtime.Error interface {
  2. error
  3. RuntimeError()
  4. }

不过这种做法只是君子协定,如果有人刻意伪造接口也是很容易的。再严格一点的做法是给接口定义一个私有方法。只有满足了这个私有方法的对象才可能满足这个接口,而私有方法的名字是包含包的绝对路径名的,因此只能在包内部实现这个私有方法才能满足这个接口。测试包中的 testing.TB 接口就是采用类似的技术

  1. type testing.TB interface {
  2. Error(args ...interface{})
  3. Errorf(format string, args ...interface{})
  4. ...
  5. private()
  6. }

不过这种通过私有方法禁止外部对象实现接口的做法也是有代价的:首先是这个接口只能包内部使用,外部包正常情况下是无法直接创建满足该接口对象的;其次,这种防护措施也不是绝对的,恶意的用户依然可以绕过这种保护机制。

通过嵌入匿名的 testing.TB 接口来伪造私有的 private 方法,因为接口方法是延迟绑定,编译时 private 方法是否真的存在并不重要。

  1. type TB struct {
  2. testing.TB
  3. }
  4. func (p *TB) Fatal(args ...interface{}) {
  5. fmt.Println("TB.Fatal disabled!")
  6. }
  7. func main() {
  8. var tb testing.TB = new(TB)
  9. tb.Fatal("Hello, playground")
  10. }

在自己的 TB 结构体类型中重新实现了 Fatal 方法,然后通过将对象隐式转换为 testing.TB 接口类型(因为内嵌了匿名的 testing.TB 对象,因此是满足 testing.TB 接口的),然后通过 testing.TB 接口来调用我们自己的 Fatal 方法。

五、空接口

接口定义没有声明任何方法,称为空接口,按照go规范任何类型都实现了空接口,因为都满足了两个实现条件。这就比较有意思了,空接口可以等于任何值,类似Java中的Object对象。

  1. var data interface{} // 定义空接口变量
  2. data = 1
  3. fmt.Printf("type=%T, value=%v\n", data, data)
  4. data = "hello"
  5. fmt.Printf("type=%T, value=%v\n", data, data)

输出如下

  1. type=int, value=1
  2. type=string, value=hello

函数形参是空接口类型,就表示可接收任何类型,然后再在断言,不同的类型,采用不同的逻辑,在开发框架层时经常使用

  1. func assertion(T interface{}) {
  2. switch T.(type) {
  3. case User:
  4. fmt.Println("user")
  5. case Admin:
  6. fmt.Println("admin")
  7. default:
  8. fmt.Println("default")
  9. }
  10. }

T.(type)能在switch中使用,可以理解定制的语法糖,否则需要使用if逐个类型断言

空接口在标准库空也有普遍使用,比如panic函数终止程序时,可传递空接口类型的参数,捕获错误时可获取

  1. type any = interface{}
  2. func panic(v any)

go基础-接口的更多相关文章

  1. MMORPG大型游戏设计与开发(服务器 AI 基础接口)

    一个模块都往往需要统一的接口支持,特别是对于非常大型的模块,基础结构的统一性非常重要,它往往决定了其扩展对象的通用性.昨天说了AI的基本概述以及组成,作为与场景模块中核心一样重要的地位,基础部分的设计 ...

  2. .net微信公众号开发——基础接口

    作者:王先荣    本文讲述微信公众号开发中基础接口的使用,包括以下内容:    (1)获取许可令牌(AccessToken):    (2)获取微信服务器地址:    (3)上传.下载多媒体文件:  ...

  3. php获取微信基础接口凭证Access_token

    php获取微信基础接口凭证Access_token的具体代码,供大家参考,具体内容如下 access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token.开发者需要进 ...

  4. face_recognition 基础接口

    face_recognition 基础接口 face_recognition使用世界上最简单的人脸识别库,在Python或命令行中识别和操作人脸. 使用dlib最先进的人脸识别技术构建而成,并具有深度 ...

  5. ABP开发框架前后端开发系列---(6)ABP基础接口处理和省份城市行政区管理模块的开发

    最近没有更新ABP框架的相关文章,一直在研究和封装相关的接口,总算告一段落,开始继续整理下开发心得.上次我在随笔<ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目 ...

  6. PHP 面向对对象基础(接口,类)

    介绍PHP面向对象的基础知识 1. 接口的定义interface ,类定义class,类支持abstract和final修饰符,abstract修饰为抽象类,抽象类 不支持直接实例化,final修饰的 ...

  7. Java 基础 接口和多态

    接口 接口的概念 接口是功能的集合,同样可看做是一种数据类型,是比抽象类更为抽象的”类”. 接口只描述所应该具备的方法,并没有具体实现,具体的实现由接口的实现类(相当于接口的子类)来完成.这样将功能的 ...

  8. JDBC的基础接口及其用法

    JDBC基础 所谓JDBC即是:Java DataBase Connectivity,java与数据库的连接.是一些用来执行SQL语句的Java API. 我们进行JDBC的编程,主要常用的几个概念: ...

  9. Python基础-接口与归一化设计、抽象类、继承顺序、子类调用父类,多态与多态性

    一.接口与归一化设计 Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能). 由 ...

  10. Java学习关于集合框架的基础接口--Collection接口

     集合框架(Collection  Framework)是Java最强大的子系统之一,位于java.util 包中.集合框架是一个复杂的接口与和类层次,提供了管理对象组的最新技术.Java集合框架标准 ...

随机推荐

  1. 好用工具: windows terminal

    直接在微软商店搜索该软件即可,本文介绍无法使用微软商店的情况. 解题思路 当我们无法下载某个软件时,可直接去Github中寻找该项目,知道该软件资源并下载. 下载地址 https://github.c ...

  2. git: failed to push some refs to

    错误原因 没有添加readme文件 解决方案 git pull --rebase origin master 至此问题解决

  3. django执行makemigrations报AttributeError: 'str' object has no attribute 'decode'

    顺着报错文件点进去,找到query = query.decode(errors='replace')将decode修改为encode即可.

  4. pywintypes.com_error: (-2147418111, '被呼叫方拒绝接收呼叫。', None, None)

    将打开的excel全部关闭,即可解决问题.

  5. Canvas好难,如何让研发低成本实现Web端流程图设计功能

    摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 相信大家在职场中经常会用到流程图,在互联网行业,绘制流程 ...

  6. 原来ES7~12分别增加了这些属性呀

    ES6也称为ES2015,于2015年发布,此后每年都有新增一些属性,分别命名为ES7~12,发布的年份分别对应2016年到2021年 ES7 includes方法 数组中新增了includes方法, ...

  7. 缓存面试解析:穿透、击穿、雪崩,一致性、分布式锁、Redis过期,海量数据查找

    为什么使用缓存 在程序内部使用缓存,比如使用map等数据结构作为内部缓存,可以快速获取对象.通过将经常使用的数据存储在缓存中,可以减少对数据库的频繁访问,从而提高系统的响应速度和性能.缓存可以将数据保 ...

  8. 如何做一个完美的api接口?

    如何做一个api接口?:我们知道API其实就是应用程序编程接口,可以把它理解为是一种通道,用来和不同软件系统间进行通信,本质上它是预先定义的函数:-api,接口 1 我们知道API其实就是应用程序编程 ...

  9. mpi转以太网连接300PLC在气动系统中的应用

    mpi转以太网连接300PLC在气动系统中的应用 某企业装备有限公司 摘要 工业通讯迅速发展的今天,MPI转以太网通讯已经发展为成熟,稳定,高效通讯 方式,兴达易控自主研发的MPI转以太网模块MPI- ...

  10. JAVA中三种I/O框架——BIO、NIO、AIO

    一.BIO(Blocking I/O) BIO,同步阻塞IO模型,应用程序发起系统调用后会一直等待数据的请求,直至内核从磁盘获取到数据并拷贝到用户空间: 在一般的场景中,多线程模型下的BIO是成本较低 ...