zoukankan      html  css  js  c++  java
  • Golang---基本类型(interface)

        摘要:今天我们来学习 Golang 中的 interface 类型。

    Go 的 5 个关键点

    interface 是一种类型

    type Animal interface {
        SetName(string)
        GetName() string
    }

      首先 interface 是一种类型,从它的定义中就可以看出用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。Go 允许不带任何方法的 interface, 这种类型的 interface 叫 empty interface。如果一个类型实现了一个 interface 中所有的方法,我们说该类型实现了该 interface, 所以所有类型都实现了 empty interface, Go 没有显式的关键字用来实现 interface, 只需要实现 interface 包含的方法即可。

    interface 变量存储的是实现者的值

    package main
    
    import (
        "fmt"
    )
    
    type Animal interface {
        SetName(string)
        GetName() string
    }
    
    type Cat struct {
        Name string
    }
    
    func (c Cat) SetName(name string) {
        fmt.Println("c addr in: ", c)
        c.Name = name
        fmt.Println(c.GetName())
    }
    
    func (c Cat) GetName() string {
        return c.Name
    }
    
    func main() {
        // c := &Cat{Name: "Cat"}
        // fmt.Println("c addr out: ", c)
        // c.SetName("DogCat")
        // fmt.Println(c.GetName())
        c := Cat{}
        var i Animal
        i = &c  //把变量赋值给一个 interface
        fmt.Println(i.GetName())
        
    }
    interface

       如果有多种类型实现了某个 interface, 这些类型的值都可以直接使用 interface 的变量存储。不难看出 interface 的变量中存储的是实现了 interface 的类型的对象值,这种能力是 duck typing。在使用 interface 时,不需要显式在 struct 上声明要实现哪个 interface, 只需要实现对应的 interface 中的方法即可,Go 会在运行时执行从其它类型到 interface 的自动转换。

    如何判断 interface 变量存储的是哪种类型的值

      当一个 interface 被多个类型实现时, 有时候我们需要区分 interface 的变量究竟存储的是哪种类型的值, Go 可以使用 comma, ok 的形式做区分 value, ok := em.(T) : em 是 interface 类型的变量, T 代表要断言的类型, value 是 interface 变量存储的值, ok 是 bool 类型标识是否为该断言的类型 T。

    c := Cat{Name: "cat"}
    var i Animal i = &c //把变量赋值给一个 interface if t, ok := i.(*Cat); ok { fmt.Println("c implement i:", t) }
    //当然我们也可以用 switch 语句
    switch t := i.(*Cat) {
    case *S:
    fmt.Println("i store *S", t)
    case *R:
    fmt.Println("i store *R", t)
    }

    空 interface

      interface{} 是一个空的 interface 类型, 空的 interface 没有方法,所以可以认为所有的类型都实现了 interface{}。如果定义一个函数参数是 interface{} 类型, 这个函数应该可以接受任何类型作为它的参数。

    func doSomething(v interface{}) {
        //do something
    }

    注意:在函数内部 v 并不是任何类型,在函数参数传递的过程中,任何类型都被自动转换为 interface{}。 关于 Go 是如何转换的,可以参考这里

    另外:既然空的 interface 可以接受任何类型的参数,那么一个 interface{} 类型的 slice 是不是就可以接受任何类型的 slice?

    func printAll(vals []interface{}) { //1
        for _, val := range vals {
            fmt.Println(val)
        }
    }
    
    func main(){
        names := []string{"stanley", "david", "oscar"}
        printAll(names)
    }

    //err:cannot use names (type []string) as type []interface {} in argument to printAll

      上述示例代码中,我们将 []string 转换为 []interface{}, 但是我们编译的时候报错,这说明 Go 并没有帮助我们自动把 slice 转换为 interface{} 类型的 slice, 所以出错了。为什么不帮我们自动转换,相关说明在这里查看。但是我们可以手动进行转换来达到我们的目的:

    var dataSlice []int = foo()
    var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
    for i, v := range dataSlice {
        interfaceSlice[i] = v
    }

    选择 interface 的实现者

    package main
    
    import (
        "fmt"
    )
    
    type Animal interface {
        SetName(string)
        GetName() string
    }
    
    type Cat struct {
        Name string
    }
    
    func (c Cat) SetName(name string) {
        fmt.Println("c addr in: ", c)
        c.Name = name
        fmt.Println(c.GetName())
    }
    
    func (c Cat) GetName() string {
        return c.Name
    }
    
    
    func main() {
        c := &Cat{Name: "Cat"}  //指针调用
        //cc := Cat{Name: "Cat"}  //值调用
        fmt.Println("c addr out: ", c)
        c.SetName("DogCat")
        fmt.Println(c.GetName())
        c := Cat{Name: "cat"}
    
        fmt.Println(i.GetName())
    
    }

      上面代码中,接受者是个 value receiver。但是 interface 定义时并没有严格规定实现者的方法 receiver 是个 value receiver 还是 pointer receiver。如果接收者是 value receiver, 那么在方法内对这个接收者所做的修改都不会影响到调用者,这和 C++ 中的 “值传递” 类似,例如:

    package main
    
    import (
        "fmt"
    )
    
    type Animal interface {
        SetName(string)
        GetName() string
    }
    
    type Cat struct {
        Name string
    }
    
    func (c Cat) SetName(name string) {
        fmt.Println("c addr in: ", c)  // c addr in:  {Cat}, 内部会把指针对应的值取出来,进行值调用
        c.Name = name
        fmt.Println(c.GetName()) //print DogCat
    }
    
    func (c Cat) GetName() string {
        return c.Name
    }
    
    func main() {
        c := &Cat{Name: "Cat"} //指针调用,but receiver is value receiver
        fmt.Println("c addr out: ", c)  // c addr out:  &{Cat} 外部还是指针类型的变量
        c.SetName("DogCat")
        fmt.Println(c.GetName()) //print Cat
    
    }

      注意:如果 receiver 是 pointer receiver, 通过 value 进行调用,则会编译保持,提示该类型没有实现这个 interface, 这可以理解为:如果是 pointer 调用,go 会自动进行转换,因为有了指针总能得到指针指向的值是什么,如果是 value, go 将无法得知 value 的原始值是什么,因为 value 仅仅是份拷贝。go 会把指针进行隐式转换得到 value, 但反过来不行。

    Go interface 的底层实现

    interface 底层结构

    func foo(x interface{}) {
        if x == nil {
            fmt.Println("empty interface")
            return
        }
        fmt.Println("non-empty interface")
    }
    
    func main() {
        var x *int = nil
        foo(x)  //print non-empty interface
    }

      通过上述的代码,我们或许有些疑惑,那就带着疑惑往下看吧,了解 go 是怎么把一种类型转换为 interface 类型的。

      首先,根据 interface 是否包含 method, 底层实现上用了两种不同的 struct 来表示:iface 和 eface。eface 表示不含 method 的 interface 结构,或者叫 empty interface。对于 Golang 中的大部分数据类型都可以抽象成 _type 结构,同时针对不同的类型还会有一些其他信息。

    type eface struct {
        _type *_type  //接口指向的数据类型
        data  unsafe.Pointer  //接口指向的数据的值
    }
    
    type _type struct {
        size       uintptr  // type size
        ptrdata    uintptr // size of memory prefix holding all pointers
        hash       uint32  // hash of type
        tflag      tflag
        align      uint8
        fieldAlign uint8
        kind       uint8
        // function for comparing objects of this type
        // (ptr to object A, ptr to object B) -> ==?
        equal func(unsafe.Pointer, unsafe.Pointer) bool
        // gcdata stores the GC type data for the garbage collector.
        // If the KindGCProg bit is set in kind, gcdata is a GC program.
        // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
        gcdata    *byte
        str       nameOff
        ptrToThis typeOff
    }

      iface 表示 non-empty interface 的底层实现。包含一些 method。method 的具体实现存放在 itab.fun 变量里,如果 interface 包含多个 method, 这里只有一个 fun 变量怎么存呢?等会根据具体的例子来说明这个问题。我们先来看一下 iface 这个结构体:

    type iface struct {
        tab  *itab  //包含函数的声明和具体实现
        data unsafe.Pointer  //指向数据的指针
    }
    
    type itab struct {
        inter *interfacetype  //包含函数的声明
        _type *_type
        hash  uint32 // copy of _type.hash. Used for type switches.
        _     [4]byte
        fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.  //函数的具体实现,如果 fun[0] == 0, 移位着没有实现 interfacettpe 中声明的函数
    }
    
    type interfacetype struct {
        typ     _type
        pkgpath name
        mhdr    []imethod  //包含函数的声明
    }

    Q1: 一个 [1]uintptr 如何存多个 method ? [此处完全来自文末参考资料]

    A1: 我们通过汇编代码来看一下:

    package main
    
    type MyInterface interface {
        Print()
        Hello()
        World()
        AWK()
    }
    
    func Foo(me MyInterface) {
        me.Print()
        me.Hello()
        me.World()
        me.AWK()
    }
    
    type MyStruct struct {}
    
    func (me MyStruct) Print() {}
    func (me MyStruct) Hello() {}
    func (me MyStruct) World() {}
    func (me MyStruct) AWK() {}
    
    func main() {
        var me MyStruct
        Foo(me)
    }
    example

    通过对其反汇编可以得到:

    $ go build -gcflags '-l' -o main main.go
    $ go tool objdump -s main
    TEXT main.Foo(SB) TEXT main.Foo(SB) C:/Users/sweenzhang/learnGo/main.go
        interface8.go:10    0x104c060   65488b0c25a0080000  GS MOVQ GS:0x8a0, CX
        interface8.go:10    0x104c069   483b6110        CMPQ 0x10(CX), SP
        interface8.go:10    0x104c06d   7668            JBE 0x104c0d7
        interface8.go:10    0x104c06f   4883ec10        SUBQ $0x10, SP
        interface8.go:10    0x104c073   48896c2408      MOVQ BP, 0x8(SP)
        interface8.go:10    0x104c078   488d6c2408      LEAQ 0x8(SP), BP
        interface8.go:11    0x104c07d   488b442418      MOVQ 0x18(SP), AX
        interface8.go:11    0x104c082   488b4830        MOVQ 0x30(AX), CX //取得 Print 函数地址
        interface8.go:11    0x104c086   488b542420      MOVQ 0x20(SP), DX
        interface8.go:11    0x104c08b   48891424        MOVQ DX, 0(SP)
        interface8.go:11    0x104c08f   ffd1            CALL CX     // 调用 Print()
        interface8.go:12    0x104c091   488b442418      MOVQ 0x18(SP), AX
        interface8.go:12    0x104c096   488b4828        MOVQ 0x28(AX), CX //取得 Hello 函数地址
        interface8.go:12    0x104c09a   488b542420      MOVQ 0x20(SP), DX
        interface8.go:12    0x104c09f   48891424        MOVQ DX, 0(SP)
        interface8.go:12    0x104c0a3   ffd1            CALL CX           //调用 Hello()
        interface8.go:13    0x104c0a5   488b442418      MOVQ 0x18(SP), AX
        interface8.go:13    0x104c0aa   488b4838        MOVQ 0x38(AX), CX //取得 World 函数地址
        interface8.go:13    0x104c0ae   488b542420      MOVQ 0x20(SP), DX 
        interface8.go:13    0x104c0b3   48891424        MOVQ DX, 0(SP)
        interface8.go:13    0x104c0b7   ffd1            CALL CX           //调用 World()
        interface8.go:14    0x104c0b9   488b442418      MOVQ 0x18(SP), AX
        interface8.go:14    0x104c0be   488b4020        MOVQ 0x20(AX), AX //取得 AWK 函数地址
        interface8.go:14    0x104c0c2   488b4c2420      MOVQ 0x20(SP), CX
        interface8.go:14    0x104c0c7   48890c24        MOVQ CX, 0(SP)
        interface8.go:14    0x104c0cb   ffd0            CALL AX           //调用 AWK()
        interface8.go:15    0x104c0cd   488b6c2408      MOVQ 0x8(SP), BP
        interface8.go:15    0x104c0d2   4883c410        ADDQ $0x10, SP
        interface8.go:15    0x104c0d6   c3          RET
        interface8.go:10    0x104c0d7   e8f48bffff      CALL runtime.morestack_noctxt(SB)
        interface8.go:10    0x104c0dc   eb82            JMP main.Foo(SB)
    反汇编代码

    其中 0x18(SP) 对应的 itab 的值。fun 在 x86-64 机器上对应 itab 内的地址偏移为 8+8+8+4+4 = 32 = 0x20,也就是 0x20(AX) 对应的 fun 的值,此时存放的 AWK 函数地址。然后 0x28(AX) = &Hello,0x30(AX) = &Print,0x38(AX) = &World。对的,每次函数是按字典序排序存放的。

    我们再来看一下函数地址究竟是怎么写入的?首先 Golang 中的 uintptr 一般用来存放指针的值,这里对应的就是函数指针的值(也就是函数的调用地址)。但是这里的 fun 是一个长度为 1 的 uintptr 数组。我们看一下 runtime 包的 additab 函数

    func additab(m *itab, locked, canfail bool) {
        ...
        *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
        ...
    }

    上面的代码的意思是在 fun[0] 的地址后面依次写入其他 method 对应的函数指针。熟悉 C++ 的同学可以类比 C++ 的虚函数表指针来看。

    Type Assertion(类型断言)

     我们知道使用 interface type assertion (中文一般叫断言) 的时候需要注意,不然很容易引入 panic。

    func do(v interface{}) {
        n := v.(int)    // might panic
    }
    
    func do(v interface{}) {
        n, ok := v.(int)
        if !ok {
            // 断言失败处理
        }
    }

    这个过程体现在下面的几个函数上:

    // The assertXXX functions may fail (either panicking or returning false,
    // depending on whether they are 1-result or 2-result).
    func assertI2I(inter *interfacetype, i iface) (r iface) {
        tab := i.tab
        if tab == nil {
            // explicit conversions require non-nil interface value.
            panic(&TypeAssertionError{"", "", inter.typ.string(), ""})
        }
        if tab.inter == inter {
            r.tab = tab
            r.data = i.data
            return
        }
        r.tab = getitab(inter, tab._type, false)
        r.data = i.data
        return
    }
    func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
        tab := i.tab
        if tab == nil {
            return
        }
        if tab.inter != inter {
            tab = getitab(inter, tab._type, true)
            if tab == nil {
                return
            }
        }
        r.tab = tab
        r.data = i.data
        b = true
        return
    }
    
    // 类似
    func assertE2I(inter *interfacetype, e eface) (r iface)
    func assertE2I2(inter *interfacetype, e eface) (r iface, b bool)

    总结

     从某种意义上来说,Golang 的 interface 也是一种多态的体现。对比其他支持多态特性的语言,实现还是略有差异,很难说谁好谁坏

    参考资料

    https://research.swtch.com/interfaces

    http://legendtkl.com/2017/07/01/golang-interface-implement/

    https://sanyuesha.com/2017/07/22/how-to-understand-go-interface/

  • 相关阅读:
    C#打开SDE数据库的几种方式
    关于DataGridView
    细说Sql Server中的视图(下)转载
    细说Sql Server中的视图(上)转载
    细说Sql Server中的视图
    Ajax实现原理,代码封装
    无限级下拉菜单(树形菜单,二级菜单)
    无限循环轮播图之JS部分(原生JS)
    无限循环轮播图之结构布局(原生JS)
    无限循环轮播图之运动框架(原生JS)
  • 原文地址:https://www.cnblogs.com/zpcoding/p/13615012.html
Copyright © 2011-2022 走看看