Go语言之接口(接口实现条件,使用,原理,类型断言)
创始人
2025-05-30 08:58:11
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.接口内存逃逸

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

相关内容

热门资讯

“月入几万,加盟就赚”的快递驿... 很长一段时间里,最受普通人欢迎的创业方式,开奶茶店是一种,加盟快递驿站也是一种。驿站的盛行虽是事实,...
去拉美掘金,月入5万只是及格线... 文 | 严贝贝 陈梓洁编辑 | 曹宾玲数据支持 | 洞见数据研究院半年外派生活结束,大海拿着到手的2...
专访贺铿:既要经济增长,又要低... 本文来源:时代周报 作者:阿力米热2025年是“十四五”规划收官之年,同时也是中国提出“双碳”目标的...
V观财报|若羽臣:朗姿股份计划... 【V观财报|若羽臣:朗姿股份计划减持不超3%公司股份】若羽臣公告,近日收到朗姿股份出具的《关于拟减持...
震惊!宁波某企业公开动员员工当... “一方面,多名员工离职后因涉职务犯罪被立案或受到刑事处罚;另一方面,公司内部却发邮件鼓励员工报名当‘...
周鸿祎:准备干掉360整个市场... “ 周鸿祎曾表示自己也想做网红,未来我能够替360节省上10亿元的广告费。从2022年到2024年,...
避雷!A股6月超200家上市公... 大家知道A股有老话:一大涨就减持,很多上市公司敢拉就敢把公司卖给你。最近大家应该能发现每天都有大量上...
汇川技术分拆联合动力上市,关联... 苏州汇川联合动力系统股份有限公司(以下简称“联合动力”)向深交所递交了创业板IPO招股说明书,并且在...
“苏超”热度引爆足球概念,最牛... 过去一周(6月3日—6月6日),三大股指集体上涨。截至6月6日收盘,上证指数周涨幅1.13%,报33...
北交所进入“双指数”投资时代,... 成立近四年的北交所,即将进入“双指数”投资时代。北交所官网最新披露,北交所和中证指数公司将于本月底发...
河南首富换人!泡泡玛特创始人王... 红星资本局6月8日消息,今天上午,根据福布斯实时富豪榜,泡泡玛特(09992.HK)创始人王宁目前身...
罕见一幕发生,银行存款利率跌破... 作者|碎叶冬青近日罕见一幕发生,我们又一次见证了历史。5月20日,六大行宣布下调人民币存款利率,最大...
原则上支持LP冒险|Findm... 各位好,今天录一期。本来是我同事杨博宇写了一篇关于某地政府削减GP管理费的稿子,拿来我发现我有些话想...
今年新股0破发!打新人数创2年... 今年以来,打新人数持续回暖。3月上市的新亚电缆,打新人数超过1400万,是2023年以来的最高值。今...
券商营业部员工卖了1200万基... 红星资本局6月8日消息,一名券商营业部员工在不到一年时间卖出总规模超1200万元的基金产品后,认为公...
利空突袭!集体大跌! 利空突袭... 预期被打得太满,不是好事。据最新消息,标准普尔道琼斯指数公司宣布在最新一轮季度调整中维持标普500指...
一条视频涨粉2000万,韦神凭... 韦东奕为何要开抖音号?01 三句话,涨粉2000万好家伙,还得是韦神。韦东奕最近在抖音上再次创造了一...
美利云初露峥嵘 美利云初露峥嵘... 富凯摘要:数据中心业务已经取得了显著增长。作者|欧文6月6日,3次打开涨停的美利云最终以涨停收盘,股...
V观财报|牧原股份:当前公司国... 【V观财报|牧原股份:当前公司国际化处于起步阶段】牧原股份披露的活动记录表显示,公司近日接受机构调研...
千亿A股创新药公司,现重大研发... 本周(6月2日—6月6日)机构调研公司数量仍在百家以上,截至6月6日18时,共有141家上市公司披露...