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.接口内存逃逸

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

相关内容

热门资讯

688076上市首年就进行业绩... 2025.12.17本文字数:1193,阅读时长大约2分钟作者 |第一财经 林志吟在科创板上市首年,...
沐曦股份首日涨幅超摩尔线程,“... 出品|达摩财经2025年末,国产GPU企业跑出了资本化加速度。被业界称为“GPU四小龙”的摩尔线程(...
真维斯回应“卫衣疑似抄袭肖战新... 11月11日消息,真维斯发布声明称,接到消费者反馈及市场监测信息显示,一款未经真维斯品牌授权的商店,...
陈吉宁龚正调研上汽集团:聚焦主... 11月11日消息,据上海发布,上海市委书记陈吉宁,市委副书记、市长龚正结合贯彻落实党的二十届三中全会...
FTX起诉币安及赵长鹏寻求追回... 11月11日消息,据报道,FTX起诉币安及其前首席执行官赵长鹏,寻求追回其所谓的萨姆·班克曼-弗里德...
中新赛克:多名股东拟合计减持公... 11月11日消息,中新赛克公告,公司股东广东红土、南京红土、昆山红土、郑州百瑞、南京创芸拟合计减持公...
2连板百傲化学:目前芯慧联新不... 11月11日消息,百傲化学发布异动公告,公司全资子公司芯傲华拟增资并控股芯慧联事项存在经营风险、并购...
华谊兄弟:阿里创投及其一致行动... 阿里创投及其一致行动人马云退出华谊兄弟5%以上股东行列。12月17日晚间,华谊兄弟传媒股份有限公司(...
中国中免:全资子公司中标上海两... 净利润增长持续下降的中国中免全权取得了上海机场多个免税店项目的经营权。12月17日,中国旅游集团中免...
新年会改变什么? 核心观点:1.不只是时间。即将过去的2025年,对于世界经济和全球市场而言,或是值得回味的一年,宏观...