• 技术文章 >后端开发 >Golang

    golang的接口有啥用

    青灯夜游青灯夜游2023-01-04 20:42:16原创226

    在golang中,接口是一种类型,是用来将对方法进行一个收束,其作用是:1、作为方法的收束器,进行面向对象设计;2、作为各种数据的承载者,可以用来接收函数参数等。接口的定义语法“type 接口类型名 interface{方法名( 参数列表1 ) 返回值列表}”;当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。

    本教程操作环境:windows7系统、GO 1.18版本、Dell G3电脑。

    一、接口(interface)是什么

    interface是一组method签名的组合,我们通过interface来定义对象的一组行为。

    (注意method 和普通func的区别)

    Interface是一种类型,和往常语言的接口不一样,它只是用来将对方法进行一个收束。然而正是这种收束,使GO语言拥有了基于功能的面向对象。

    接口的主要功能:

    1.作为方法的收束器,进行面向对象设计。

    2.作为各种数据的承载者,可以用来接收函数参数等。

    这也是,GO语言提倡面向接口编程

    二、接口的定义使用

    2.1定义

    类似结构体

    type 接口类型名 interface{
        方法名1( 参数列表1 ) 返回值列表1
        方法名2( 参数列表2 ) 返回值列表2
        …
    }

    当然这只是有方法的接口定义,面向数据的接口不用。

    2.2使用

    一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表

    //定义接口
    type FastfoodStore interface{
        MakeHamberger()
        MakeFriedChips()
        MakeSoftDrink()
    }
    //定义结构体
    type KFC struct{}
    type HambergerKing struct{}
    
    //实现了接口中所有的方法
    func (kfc KFC) MakeHamberger(){
        fmt.println("肯德基的汉堡")
    }
    func (kfc KFC) MakeFriedChips(){
        fmt.println("肯德基的薯条")
    }
    func (kfc KFC) MakeSoftDrink(){
        fmt.println("肯德基的饮料")
    }
    
    func (K *HambergerKing) MakeHameberger(){
        fmt.println("汉堡王的汉堡")
    }
    func (K *HambergerKing) MakeFriedChips(){
        fmt.println("汉堡王的薯条")
    }
    func (K *HambergerKing) MakeSoftDrink(){
        fmt.println("汉堡王的饮料")
    }

    我们可以看到不同于Java的接口显式实现,Go的语言是隐式实现的。

    那么GO语言是如何检查该类型是否是接口呢?

    答:Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查。从类型检查的过程来看,编译器仅在需要时才检查类型,类型实现接口时只需要实现接口中的全部方法,不需要像 Java 等编程语言中一样显式声明。

    我们可以看到在上面实现接口的时候,KFC是用结构体对象实现的,而Hamberger king是通过指针实现的两者有什么不同呢?

    答:区别在于我们初始化接口的时候

    //结构体初始化和指针初始化
    var f faststore = KFC{}             //可以通过编译
    var f faststore = &KFC{}            //可以通过编译
    
    var f faststore = HambergerKing{}    //无法通过编译
    var f faststore = &HambergerKing{}    //可以通过编译

    所以在我们使用指针进行实现,结构体初始化时,为啥不行呢?

    答:Go 语言在传递参数时都是传值的。

    1.png

    如上图所示,无论上述代码中初始化的变量指针还是结构体,使用 调用方法时都会发生值拷贝:

    如上图左侧,对于 &HambergerKing{} 来说,这意味着拷贝一个新的 &HambergerKing{} 指针,这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体;
    如上图右侧,对于 HambergerKing{} 来说,这意味着方法会接受一个全新的 HambergerKing{},因为方法的参数是*HambergerKing,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体;
    上面的分析解释了指针类型的现象,当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。当然这并不意味着我们应该一律使用结构体实现接口,这个问题在实际工程中也没那么重要,在这里我们只想解释现象背后的原因。

    在上面我们说过,interface有两种用法,现在介绍了其中一种就是作为方法的收束器。那么第二种就是作为数据的承载者

    2.3 数据承载者

    作为数据容器时,接口就是一个“空”接口,这个空来形容没有Method。空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

    需要注意的是,与 C 语言中的 void * 不同,interface{} 类型不是任意类型。如果我们将类型转换成了 interface{} 类型,变量在运行期间的类型也会发生变化,获取变量类型时会得到 interface{}。

    我们尝试从底层实现来解释两种用法的不同,你会好理解一些。Go 语言使用 runtime.iface 表示第一种接口,使用 runtime.eface 表示第二种不包含任何方法的接口 interface{},两种接口虽然都使用 interface 声明,但是由于后者在 Go 语言中很常见,所以在实现时使用了特殊的类型。

    2.png

    空接口作为函数的参数

    使用空接口实现可以接收任意类型的函数参数。

    // 空接口作为函数参数
    func show(a interface{}) {
        fmt.Printf("type:%T value:%v\n", a, a)
    }

    空接口作为map的值

    使用空接口实现可以保存任意值的字典。

    // 空接口作为map值
        var studentInfo = make(map[string]interface{})
        studentInfo["name"] = "Wilen"
        studentInfo["age"] = 18
        studentInfo["married"] = false
        fmt.Println(studentInfo)
    //gin框架的gin.H{}

    三、关于接口类型转换

    interface 可以存储所有的值,那么自然会涉及到类型转换这个话题。与此同时,我们也将在这节细说类型转换中,因为结构体实现和结构体指针实现的接口的异同。

    3.1结构体指针实现接口

    //我们仍然运用上面快餐店的例子
    type Store interface{
        MakeHamberger()
    }
    type KFC struct{
        name string
    }
    func (k *KFC) MakeHamberger(){
        fmt.println(k.name+"制作了一个汉堡")
    }
    func main(){
        var s store = &KFC{name:"东街店"}
        store.MakeHamberger()
    }

    这里将上述代码生成的汇编指令拆分成三部分分析:

    1.结构体 KFC 的初始化;

    KFC的初始化又可以分为下面几步:

    3.png

    2.赋值触发的类型转换过程;

    因为 KFC 结构体的定义中只包含一个字符串,而字符串在 Go 语言中总共占 16 字节,所以每一个 KFC 结构体的大小都是 16 字节。初始化 KFC 结构体之后就进入了将 *KFC 转换成 Store 类型的过程了:

    类型转换的过程比较简单,Store 作为一个包含方法的接口,它在底层使用 [runtime.iface] 结构体表示。runtime.iface 结构体包含两个字段,其中一个是指向数据的指针,另一个是表示接口和结构体关系的 tab 字段,我们已经通过上一段代码 SP+8 初始化了 KFC 结构体指针,这段代码只是将编译期间生成的 runtime.itab 结构体指针复制到 SP 上:

    4.png

    到这里,我们会发现 SP ~ SP+16 共同组成了 runtime.iface 结构体。

    3.调用接口的方法 Quack();

    栈上的这个 runtime.iface 也是 MakeHamberger() 方法的第一个入参。通过CALL()完成方法的调用。

    3.2 结构体实现接口

    //我们仍然运用上面快餐店的例子
    type Store interface{
        MakeHamberger()
    }
    type KFC struct{
        name string
    }
    func (k KFC) MakeHamberger(){
        fmt.println(k.name+"制作了一个汉堡")
    }
    func main(){
        var s store = KFC{name:"东街店"}
        store.MakeHamberger()
    }

    如果我们在初始化变量时使用指针类型 &KFC{Name: "东街店"} 也能够通过编译,不过生成的汇编代码和上一节中的几乎完全相同,所以这里也就不分析这个情况了。

    初始化 KFC 结构体;

    在栈上初始化 KFC 结构体,而上一节的代码在堆上申请了 16 字节的内存空间,栈上只有一个指向 KFC 的指针。

    完成从 KFC 到 Store 接口的类型转换;

    初始化结构体后会进入类型转换的阶段,编译器会将 go.itab."".KFC,"".Store 的地址和指向 KFC 结构体的指针作为参数一并传入 runtime.convT2I 函数:这个函数会获取 runtime.itab 中存储的类型,根据类型的大小申请一片内存空间并将 elem 指针中的内容拷贝到目标的内存中:

    func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
        t := tab._type
        x := mallocgc(t.size, t, true)
        typedmemmove(t, x, elem)
        i.tab = tab
        i.data = x
        return
    }

    runtime.convT2I 会返回一个 runtime.iface,其中包含 runtime.itab 指针和 KFC 变量。当前函数返回之后,main 函数的栈上会包含以下数据:

    5.png

    SP 和 SP+8 中存储的 runtime.itab 和 KFC 指针是 runtime.convT2I 函数的入参,这个函数的返回值位于 SP+16,是一个占 16 字节内存空间的 runtime.iface 结构体,SP+32 存储的是在栈上的 KFC 结构体,它会在 runtime.convT2I 执行的过程中拷贝到堆上。

    3.3类型断言

    如何将一个接口类型转换成具体类型?

    x.(T)

    非空接口

    func main() {
        var c Store = &KFC{Name: "东街店"}
        switch c.(type) {
        case *KFC:
            kfc := c.(*KFC)
            kfc.MakeHamberger()
        }
    }

    因为 Go 语言的编译器做了一些优化,所以代码中没有runtime.iface 的构建过程,不过对于这一节要介绍的类型断言和转换没有太多的影响。

    switch语句生成的汇编指令会将目标类型的 hash 与接口变量中的 itab.hash 进行比较

    空接口

    func main() {
        var c interface{} = &KFC{Name: "东街店"}
        switch c.(type) {
        case *KFC:
            kfc := c.(*KFC)
            kfc.MakeHamberger()
        }
    }

    上述代码会在类型断言时就不是直接获取变量中具体类型的 runtime._type,而是从 eface._type 中获取,汇编指令仍然会使用目标类型的 hash 与变量的类型比较.

    【相关推荐:Go视频教程编程教学

    以上就是golang的接口有啥用的详细内容,更多请关注php中文网其它相关文章!

    声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn核实处理。
    专题推荐:go语言 Golang 接口
    上一篇:go语言必须有一个什么包 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • go语言的形参占用内存吗• golang流程控制语句有哪些• go语言中方法和函数的区别是什么• golang文件要关闭吗• 聊聊GO初始化数据结构的方法(附代码示例)
    1/1

    PHP中文网