Go语言之接口(接口实现条件,使用,原理,类型断言)

一、接口的声明

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

二、实现接口条件

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

  • 在类型中添加与接口签名一致的方法即可实现该方法。签名包括方法中的名称、参数列表、返回值列表。
package main

import "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 = fm

	ew.EatMeat("dataaaaaa")
}

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

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

  • 当一个接口中有多个方法时,只有这些方法都被实现,接口才能被正确编译使用
package main

import "fmt"

//定义接口
type EatWhat interface {
	EatMeat(data interface{})error
	LikeSleep()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 = fm

	ew.EatMeat("dataaaaaa")
	fmt.Println(ew.LikeSleep())
}

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

3.类型与接口的关系

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

三、接口的使用

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

1.动态类型

  • 一个接口类型可以接受任意实现该接口的对象
package main

import "fmt"

//定义接口
type EatWhat interface {
	EatMeat(data interface{})error
	LikeSleep()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 = fm

	ew.EatMeat("dataaaaaa")
	fmt.Println(ew.LikeSleep())
}

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

2.动态调用

  • 因为接口是动态的,故调用接口方法也是动态的,取决于接口保存的类型。
package main

import "fmt"

//定义接口
type EatWhat interface {
   EatMeat(data interface{})error
   LikeSleep()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 = fm

   ew.EatMeat("dataaaaaa")
   fmt.Println(ew.LikeSleep())

   ew = fh
   ew.EatMeat("hhhhhhh")
   fmt.Println(ew.LikeSleep())

   ew = fs
   ew.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 {
	Writer
	Closer
}
package main

import (
	"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 main

import (
	"fmt"
)

func main(){
	var x interface{}
	x = 200

	value, ok := x.(int)

	fmt.Println(value, ok)
}

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

(2) 例子2:x类型不满足T类型
  • 若不满足,返回值value为T类型的默认值,bool为false,int为0等
package main

import (
	"fmt"
)

func main(){
	var x interface{}
	x = 200

	value, ok := x.(bool)

	fmt.Println(value, ok)
}

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

(3) 例子3:x类型为nil接口值
  • 断言永远失败,返回值value为T类型的默认值,bool为false,int为0等,返回值ok恒为false。
package main

import (
	"fmt"
)

func main(){
	var x interface{}
	x = nil
	value, ok := x.(int)

	fmt.Println(value, ok)
}

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

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

import "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 int
	a = 10
	getType(a)

	var b bool
	b = true
	getType(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 *_type
    	data   unsafe.Pointer
    }
    
  • 相比之下,eface结构体维护的就是比较简单了。
    (1)_type:存储了空接口所承载的具体的实体类型
    (2)data:保存了接口具体的值的数据指针

  • 具体:

    package main
    
    import "fmt"
    
    type me struct {
    	height float64
    	weight 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 = I
    	fmt.Println(I.GetWegih())
    }
    

    在这里插入图片描述

(2) 带有方法的非空接口
  • Go语言中使用iface结构体表示带方法签名的非空接口。
    type iface struct{
    	tab *itab
    	data unsafe.Pointer
    }
    type itab struct {
    	iner *interfacetype
    	_type *_type
    	hash uint32
    	_    [4]byte
    	fun  [1]uintptr
    }
    type interfacetype struct {
    	type    _type
    	pkgpath name
    	mhdr    []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 main
    
    import "fmt"
    
    type Clife interface {
    	EatMeat(data interface{})error
    	LikeSleep(data interface{})error
    }
    
    type me struct {
    	height float64
    	weight 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 Clife
    
    	I := me{
    		208.33,
    		49.99,
    	}
    
    	c = &I
    	fmt.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.接口内存逃逸

  • 由于接口中保存的是具体的实体类型的指针,所以当分配到栈上的值复制给指针时,就会发生内存逃逸。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值