Go语言之接口(接口实现条件,使用,原理,类型断言)
迪丽瓦拉
2025-05-30 09:12:03
0

一、接口的声明

  • Go语言的接口设计是侵入式的,接口编写者无需知道接口被哪些类型实现,而接口实现者只需要知道实现的是什么样子的接口,无需指明实现哪一个接口。编译器知道最终编译时,哪个类型实现哪个接口。
  • 接口声明的语法:
    type 接口类型名 interface{方法名1(参数列表1)返回值列表1方法名2(参数列表2)返回值列表2...
    }
    
  • 接口类型名:使用type将接口定义为自定义的类型名,Go语言的接口在命名时,一般会在单词后面添加er,如写操作的接口叫Writer,有字符串功能的接口叫Stringer,有读操作的接口叫Reader等。
  • 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可被接口所在包之外的代码访问
  • 参数列表、返回值列表:参数列表、返回值列表中的参数变量名可被忽略。

二、实现接口条件

1.条件一:接口的方法与实现接口的类型方法格式一致

  • 在类型中添加与接口签名一致的方法即可实现该方法。签名包括方法中的名称、参数列表、返回值列表。
package mainimport "fmt"//定义接口
type EatWhat interface {EatMeat(data interface{})error
}//定义类型结构
type me struct {
}//定义的me类型实现接口
func (I *me)EatMeat(data interface{})error{fmt.Println("I like eat meat:!!!!: data:",data)return nil
}func main(){//实例化me结构体fm := new(me)//声明一个EatWhat的接口var ew EatWhat//将接口赋值结构体的实例化,即me类型ew = fmew.EatMeat("dataaaaaa")
}

结果:
在这里插入图片描述

2.条件二:接口中所有方法均被实现

  • 当一个接口中有多个方法时,只有这些方法都被实现,接口才能被正确编译使用
package mainimport "fmt"//定义接口
type EatWhat interface {EatMeat(data interface{})errorLikeSleep()bool
}//定义类型结构
type me struct {
}//定义的me类型实现接口
func (I *me)EatMeat(data interface{})error{fmt.Println("I like eat meat:!!!!: data:",data)return nil
}
//必须得所有方法都实现
func (I *me)LikeSleep()bool{return true
}func main(){//实例化me结构体fm := new(me)//声明一个EatWhat的接口var ew EatWhat//将接口赋值结构体的实例化,即me类型ew = fmew.EatMeat("dataaaaaa")fmt.Println(ew.LikeSleep())
}

结果:
在这里插入图片描述

3.类型与接口的关系

  • 在Go中类型和接口是多对多的关系
    (1)一个类型可以实现多个接口
    (2)多个类型可以实现相同一个接口

三、接口的使用

  • 常见的接口的使用有
    (1)动态类型
    (2)动态调用
    (3)接口嵌套组合
    (4)类型断言

1.动态类型

  • 一个接口类型可以接受任意实现该接口的对象
package mainimport "fmt"//定义接口
type EatWhat interface {EatMeat(data interface{})errorLikeSleep()bool
}//定义类型结构1
type me struct {
}//定义的me类型实现接口
func (I *me)EatMeat(data interface{})error{fmt.Println("I like eat meat:!!!!: data:",data)return nil
}
func (I *me)LikeSleep()bool{return true
}func main(){//实例化me结构体fm := new(me)//声明一个EatWhat的接口var ew EatWhat//将接口赋值结构体的实例化,即me类型ew = fmew.EatMeat("dataaaaaa")fmt.Println(ew.LikeSleep())
}

结果:
在这里插入图片描述

2.动态调用

  • 因为接口是动态的,故调用接口方法也是动态的,取决于接口保存的类型。
package mainimport "fmt"//定义接口
type EatWhat interface {EatMeat(data interface{})errorLikeSleep()bool
}//定义类型结构1
type me struct {
}
//定义类型结构2
type he struct {
}
//定义类型结构3
type she struct {
}//定义的me类型实现接口
func (I *me)EatMeat(data interface{})error{fmt.Println("I like eat meat:!!!!: data:",data)return nil
}
func (I *me)LikeSleep()bool{return true
}//定义的he类型实现接口
func (H *he)EatMeat(data interface{})error{fmt.Println("he does not like meat!!!data:",data)return nil
}
func (H *he)LikeSleep()bool{return true
}//定义的she类型实现接口
func (S *she)EatMeat(data interface{})error{fmt.Println("she also likes meat!!!Data:",data)return nil
}
func (S *she)LikeSleep()bool{return false
}func main(){//实例化me结构体fm := new(me)fh := new(he)fs := new(she)//声明一个EatWhat的接口var ew EatWhat//将接口赋值结构体的实例化,即me类型ew = fmew.EatMeat("dataaaaaa")fmt.Println(ew.LikeSleep())ew = fhew.EatMeat("hhhhhhh")fmt.Println(ew.LikeSleep())ew = fsew.EatMeat("ssssssss")fmt.Println(ew.LikeSleep())
}

结果:
在这里插入图片描述

  • 上述me、he、she三个不同的类型调用接口中的方法,对应各自类型的实现的方法,故结果不同。

3.接口嵌套组合

  • 在Go中,不仅结构体和接口之间可以嵌套组合,接口与接口之间也可以通过嵌套组合创建新的接口**。一个接口可以包含一个或多个其他接口,相当于直接将这些内嵌接口方法列举在外层接口中一样。只要接口中所有方法被实现,则这个接口中所有嵌套接口的方法都可以被调用**。
//系统中io包中定义了写入器,关闭器和写入关闭器
type Writer interface {Write(p []byte)(n int, err error)
}
type Closer interface {Closer() error
}
type WriteCloser interface {WriterCloser
}
package mainimport ("fmt""io"
)type mystr struct {
}func (ms *mystr)Write(p []byte)(n int, err error){fmt.Println("!!!!write!!!!!")return n, nil
}
func (ms *mystr)Close()error{fmt.Println("!!!!close!!!!!")return nil
}
func main(){//将自己的类型赋值给io包中的WriteCloser接口var ms io.WriteCloser = new(mystr)ms.Write(nil)ms.Close()var writeOnly io.Writer = new(mystr)writeOnly.Write(nil)}

结果:
在这里插入图片描述

4.类型断言

  • 类型断言是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。
  • 类型断言的语法:value, ok := x.(T)(x表示一个接口的类型,T表示一个具体的类型(也可以为接口类型))。
  • 该断言表达式会返回x的值(也就是value),和一个布尔值(即ok)
    ,可根据该布尔值判断x是否为T类型:
    (1)若T是具体某个类型,类型断言会检查x的动态类型,是否等于具体类型T。若相等,则返回x的动态值,其类型是T。
    (2)若T是接口类型,类型断言会检查x的动态类型是否满足T,若满足,x的动态值不会被提取,返回值是一个类型为T的接口值 。
    (3)无论T是什么类型,若x是nil接口值,类型断言都会失败

(1) 例子1:x类型满足T类型

package mainimport ("fmt"
)func main(){var x interface{}x = 200value, ok := x.(int)fmt.Println(value, ok)
}

结果:
在这里插入图片描述

(2) 例子2:x类型不满足T类型

  • 若不满足,返回值value为T类型的默认值,bool为false,int为0等
package mainimport ("fmt"
)func main(){var x interface{}x = 200value, ok := x.(bool)fmt.Println(value, ok)
}

结果:
在这里插入图片描述

(3) 例子3:x类型为nil接口值

  • 断言永远失败,返回值value为T类型的默认值,bool为false,int为0等,返回值ok恒为false。
package mainimport ("fmt"
)func main(){var x interface{}x = nilvalue, ok := x.(int)fmt.Println(value, ok)
}

结果:
在这里插入图片描述

(4) 例子4:断言配合switch使用

package mainimport "fmt"func getType(i interface{})error{switch i.(type) {case int:fmt.Println("the type of int")case bool:fmt.Println("ther type of bool")case string:fmt.Println("the type of string")case float64:fmt.Println("the type of float64")default:fmt.Println("the type of an other type")}return nil
}
func main(){var a inta = 10getType(a)var b boolb = truegetType(b)
}

结果:在这里插入图片描述

四、接口的原理

1.编译检查

  • 当把具体的类型赋值给接口的时候,如果该类型没有实现接口的所有方法时,就会编译报错。这个在编译的时候是如何进行检查的呢?
  • Go在编译的时候,将接口和类型的方法进行排序,排序的规则:根据函数名+包排序。(这些方法是按照函数名称的字典序进行排列的)这样就可以在每次进行判断类型是否已经实现方法时,不需要再次比较类型中已经比较过的方法。
    在这里插入图片描述
  • 如上个图所示,上图接口I、类型T和接口O都是已经排好序了的。对于接口I和类型T:对类型T中的FuncA,它先跟接口I中的FuncA进行比较,找到了FuncA;对类型T中的FuncB,因为FunA已经被找到了,所有它就不会跟接口I中的FuncA进行比较,它会先跟接口I中的FuncB进行比较,找到了接口I中的FuncB;对类型T中的FuncC,它不需要跟接口I中的FunA、FuncB进行比较,直接找到了接口I中的FuncC;对类型T中的FuncD,它不需要跟接口I中的FuncA、FuncB、FuncC进行比较,直接找到了接口I中的FuncD;对类型T中的FuncE,会和接口I中的FuncF进行比较,不匹配;对于类型T中的FuncF,会和接口I中的FuncF进行比较找到了。故总共比较了6次。
  • 上图的类型T同时实现了接口I和接口O。类型T与接口I比较的次数为7次。

2.接口实现

  • 在Go中有两种接口形式:一种是带方法签名的非空接口,和另一种不带方法签名的空接口。

(1) 不带方法的空接口

  • Go语言中,空接口类型可以接收任意类型的数据,它只需要记录这个数据在哪,是什么类型的数据即可。使用eface结构体表示不带方法签名的空接口。

    type eface struct {_type *_typedata   unsafe.Pointer
    }
    
  • 相比之下,eface结构体维护的就是比较简单了。
    (1)_type:存储了空接口所承载的具体的实体类型
    (2)data:保存了接口具体的值的数据指针

  • 具体:

    package mainimport "fmt"type me struct {height float64weight float64
    }func (I *me)GetHegiht()float64{return I.height
    }
    func (I *me)GetWegih()float64{return I.weight
    }
    func main(){var c interface{}I := me{208.33,49.99,}c = Ifmt.Println(I.GetWegih())
    }
    

    在这里插入图片描述

(2) 带有方法的非空接口

  • Go语言中使用iface结构体表示带方法签名的非空接口。
    type iface struct{tab *itabdata unsafe.Pointer
    }
    type itab struct {iner *interfacetype_type *_typehash uint32_    [4]bytefun  [1]uintptr
    }
    type interfacetype struct {type    _typepkgpath namemhdr    []imethod
    }
    
  • iface是接口的具体实现,其中包含一个tab指针指向itab实体和unsafe.Pointer
    *(1)tab itab:存储了接口的类型,以及这个接口的实体类型
    (2)data unsafe.Pointer:保存了接口具体的值的数据指针
  • itab:
    (1)iner:表示接口的具体类型,包含包名pkgpath和方法的偏移量mhdr;通过偏移量mhdr可以快速的定位到方法的类型和方法名。
    (2)_type:存储接口的动态数据类型,在切片、map中常见到
    (3)hash:从_type中拷贝出来hash值,可以用来快速判断接口的动态类型和具体类型是否一致。
    (4)—:空的四字节用于内存对齐
    (5)fun:代表接口的函数指针列表,用于运行时动态调用类型实现接口里对应方法的函数。为什么fun数组大小是1呢?----因为这里存储的是接口中第一个方法的函数指针,如果有多个方法(如果有更多的方法,在他之后的内存空间里继续存储,上述也说到,这些方法是按照函数名称的字典序进行排列的),通过增加地址就可以获取到这些函数指针
  • 具体:
    package mainimport "fmt"type Clife interface {EatMeat(data interface{})errorLikeSleep(data interface{})error
    }type me struct {height float64weight float64
    }func (I *me)GetHegiht()float64{return I.height
    }
    func (I *me)GetWegih()float64{return I.weight
    }func (I *me)EatMeat(data interface{})error{fmt.Println("I like eating meat!!!!!data:", data)return nil
    }
    func (I *me)LikeSleep(data interface{})error{fmt.Println("!!!I like sleeping!!!!!data:", data)return nil
    }
    func main(){var c ClifeI := me{208.33,49.99,}c = &Ifmt.Println(c.EatMeat("aaaaaaaa"))fmt.Println(c.LikeSleep("bbbbbbb"))
    }
    
    在这里插入图片描述
  • 当我们把* me的类型的变量I赋值给接口c,此时c的动态值data就会变成I,tab会指向一个itab结构体,它的接口类型为*Clife,动态类型(即实体类型)为 *me,同时itab结构体中的fun会从动态类型(即实体类型)元数据中拷贝接口要求的那些方法的地址,以便通过c快速定位方法,而无需再去类型元数据那里查找。
  • 一旦接口类型确定了,动态类型也确定了,那么itab的内容就不会改变,故这个itab结构体是可复用的。
  • 实际,在Go中会把用到的itab结构体缓存起来,并且以<接口类型,动态类型>为key,以itab结构体指针为value构造一个哈希表,用于存储和查询itab中缓存的信息。需要一个itab时,会首先到这个哈希表中查找,如果已经有这个 itab指针,会直接拿来使用。如果哈希表中没有这个itab指针,会创建一个itab结构体,然后添加到这个哈希表中。

3.接口内存逃逸

  • 由于接口中保存的是具体的实体类型的指针,所以当分配到栈上的值复制给指针时,就会发生内存逃逸。

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...