关于golang:Golang-interface的类型断言是如何实现

6次阅读

共计 10569 个字符,预计需要花费 27 分钟才能阅读完成。

前言

哈喽,everyBody,我是 asong,明天咱们一起来摸索一下interface 的类型断言是如何实现的。咱们通常应用 interface 有两种形式,一种是带办法的 interface,一种是空的interface。因为Go 中是没有泛型,所以咱们能够用空的 interface{} 来作为一种伪泛型应用,当咱们应用到空的 interface{} 作为入参或返回值时,就会应用到类型断言,来获取咱们所须要的类型,所以平时咱们会在代码中看到大量的类型断言应用,你就不好奇它是怎么实现的嘛?你就不好奇它的性能损耗是多少嘛?反正我很好奇,略~。

类型断言的根本应用

Type Assertion(断言)是用于 interface value 的一种操作,语法是 x.(T)xinterface type的表达式,而 Tasserted type,被断言的类型。举个例子看一下根本应用:

func main() {var demo interface{} = "Golang 梦工厂"
    str := demo.(string)
    fmt.Printf("value: %v", str)
}

下面咱们申明了一个接口对象 demo,通过类型断言的形式断言一个接口对象demo 是不是 nil,并判断接口对象demo 存储的值的类型是T,如果断言胜利,就会返回值给str,如果断言失败,就会触发panic。这段代码加上如果这样写,就会触发panic

number := demo.(int64)
fmt.Printf("value:%v\n", number)

所以为了平安起见,咱们还能够这样应用:

func main() {var demo interface{} = "Golang 梦工厂"
    number, ok := demo.(int64)
    if !ok {fmt.Printf("assert failed")
        return
    }
    fmt.Printf("value:%v\n", number)
}
运行后果:assert failed

这里应用的表达式是 t,ok:=i.(T),这个表达式也是能够断言一个接口对象(i) 里不是 nil,并且接口对象(i) 存储的值的类型是 T,如果断言胜利,就会返回其类型给 t,并且此时 ok 的值 为 true,示意断言胜利。如果接口值的类型,并不是咱们所断言的 T,就会断言失败,但和第一种表达式不同的是这个不会触发 panic,而是将 ok 的值设为 false ,示意断言失败,此时t T 的零值。所以举荐应用这种形式,能够保障代码的健壮性。

如果咱们想要辨别多种类型,能够应用 type switch 断言,应用这种办法就不须要咱们按下面的形式去一个一个的进行类型断言了,更简略,更高效。下面的代码咱们能够改成这样:

func main() {var demo interface{} = "Golang 梦工厂"

    switch demo.(type) {
    case nil:
        fmt.Printf("demo type is nil\n")
    case int64:
        fmt.Printf("demo type is int64\n")
    case bool:
        fmt.Printf("demo type is bool\n")
    case string:
        fmt.Printf("demo type is string\n")
    default:
        fmt.Printf("demo type unkonwn\n")
    }
}

type switch的一个典型利用是在 go.uber.org/zap 库中的 zap.Any() 办法,外面就用到了类型断言,把所有的类型的 case 都列举进去了,default分支应用的是Reflect,也就是当所有类型都不匹配时应用反射获取相应的值,具体大家能够去看一下源码。

类型断言实现源码分析

非空接口和空接口都能够应用类型断言,咱们分两种进行分析。

空接口

咱们先来写一段测试代码:

type User struct {Name string}

func main() {var u interface{} = &User{Name: "asong"}
    val, ok := u.(int)
    if !ok {fmt.Printf("%v\n", val)
    }
}

老样子,咱们将上述代码转换成汇编代码看一下:

go tool compile -S -N -l main.go > main.s4 2>&1

截取局部重要汇编代码如下:

    0x002f 00047 (main.go:12)    XORPS    X0, X0
    0x0032 00050 (main.go:12)    MOVUPS    X0, ""..autotmp_8+136(SP)
    0x003a 00058 (main.go:12)    PCDATA    $2, $1
    0x003a 00058 (main.go:12)    PCDATA    $0, $0
    0x003a 00058 (main.go:12)    LEAQ    ""..autotmp_8+136(SP), AX
    0x0042 00066 (main.go:12)    MOVQ    AX, ""..autotmp_7+96(SP)
    0x0047 00071 (main.go:12)    TESTB    AL, (AX)
    0x0049 00073 (main.go:12)    MOVQ    $5, ""..autotmp_8+144(SP)
    0x0055 00085 (main.go:12)    PCDATA    $2, $2
    0x0055 00085 (main.go:12)    LEAQ    go.string."asong"(SB), CX
    0x005c 00092 (main.go:12)    PCDATA    $2, $1
    0x005c 00092 (main.go:12)    MOVQ    CX, ""..autotmp_8+136(SP)
    0x0064 00100 (main.go:12)    MOVQ    AX, ""..autotmp_3+104(SP)
    0x0069 00105 (main.go:12)    PCDATA    $2, $2
    0x0069 00105 (main.go:12)    PCDATA    $0, $2
    0x0069 00105 (main.go:12)    LEAQ    type.*"".User(SB), CX
    0x0070 00112 (main.go:12)    PCDATA    $2, $1
    0x0070 00112 (main.go:12)    MOVQ    CX, "".u+120(SP)
    0x0075 00117 (main.go:12)    PCDATA    $2, $0
    0x0075 00117 (main.go:12)    MOVQ    AX, "".u+128(SP)

下面这段汇编代码的作用就是赋值给空接口,数据都存在栈上,因为空 interface{} 的构造是 eface,所以就是组装了一个eface 在内存中,内存布局如下:

咱们晓得空接口的数据结构中只有两个字段,一个 _type 字段,一个 data 字段,从上图中,咱们能够看进去,eface_type 存储在内存的 +120(SP) 处,unsafe.Pointer存在了 +128(SP) 处,当初咱们晓得了他是怎么存的了,接下来咱们看一下空接口的类型断言汇编是怎么实现的:

    0x007d 00125 (main.go:13)    PCDATA    $2, $1
    0x007d 00125 (main.go:13)    MOVQ    "".u+128(SP), AX
    0x0085 00133 (main.go:13)    PCDATA    $0, $0
    0x0085 00133 (main.go:13)    MOVQ    "".u+120(SP), CX
    0x008a 00138 (main.go:13)    PCDATA    $2, $3
    0x008a 00138 (main.go:13)    LEAQ    type.int(SB), DX
    0x0091 00145 (main.go:13)    PCDATA    $2, $1
    0x0091 00145 (main.go:13)    CMPQ    CX, DX
    0x0094 00148 (main.go:13)    JEQ    155
    0x0096 00150 (main.go:13)    JMP    395
    0x009b 00155 (main.go:13)    PCDATA    $2, $0
    0x009b 00155 (main.go:13)    MOVQ    (AX), AX
    0x009e 00158 (main.go:13)    MOVL    $1, CX
    0x00a3 00163 (main.go:13)    JMP    165
    0x00a5 00165 (main.go:13)    MOVQ    AX, ""..autotmp_4+80(SP)
    0x00aa 00170 (main.go:13)    MOVB    CL, ""..autotmp_5+71(SP)
    0x00ae 00174 (main.go:13)    MOVQ    ""..autotmp_4+80(SP), AX
    0x00b3 00179 (main.go:13)    MOVQ    AX, "".val+72(SP)
    0x00b8 00184 (main.go:13)    MOVBLZX    ""..autotmp_5+71(SP), AX
    0x00bd 00189 (main.go:13)    MOVB    AL, "".ok+70(SP)
    0x00c1 00193 (main.go:14)    CMPB    "".ok+70(SP), $0

从下面这段汇编咱们能够看进去,空接口的类型断言是通过判断 eface 中的 _type 字段和比拟的类型进行比照,雷同就会去筹备接下来的返回值,如果类型断言正确,通过两头长期变量的传递,最终 val 保留在内存中 +72(SP) 处。ok保留在内存 +70(SP) 处。

    0x018b 00395 (main.go:15)    XORL    AX, AX
    0x018d 00397 (main.go:15)    XORL    CX, CX
    0x018f 00399 (main.go:13)    JMP    165
    0x0194 00404 (main.go:13)    NOP

如果断言失败,就会清空 AXCX寄存器,因为 AXCX中存的是 eface 构造体外面的字段。

最初总结一下空接口类型断言实现流程:空接口类型断言本质是将 eface_type与要匹配的类型进行比照,匹配胜利在内存中组装返回值,匹配失败间接清空寄存器,返回默认值。

非空接口

老样子,还是先写一个例子,而后咱们在看他的汇编实现:

type Basic interface {GetName() string
    SetName(name string) error
}

type User struct {Name string}

func (u *User) GetName() string {return u.Name}

func (u *User) SetName(name string) error {
    u.Name = name
    return nil
}

func main() {var u Basic = &User{Name: "asong"}
    switch u.(type) {
    case *User:
        u1 := u.(*User)
        fmt.Println(u1.Name)
    default:
        fmt.Println("failed to match")
    }
}

应用汇编指令看一下他的汇编代码如下:

    0x002f 00047 (main.go:26)    PCDATA    $2, $0
    0x002f 00047 (main.go:26)    PCDATA    $0, $1
    0x002f 00047 (main.go:26)    XORPS    X0, X0
    0x0032 00050 (main.go:26)    MOVUPS    X0, ""..autotmp_5+152(SP)
    0x003a 00058 (main.go:26)    PCDATA    $2, $1
    0x003a 00058 (main.go:26)    PCDATA    $0, $0
    0x003a 00058 (main.go:26)    LEAQ    ""..autotmp_5+152(SP), AX
    0x0042 00066 (main.go:26)    MOVQ    AX, ""..autotmp_4+64(SP)
    0x0047 00071 (main.go:26)    TESTB    AL, (AX)
    0x0049 00073 (main.go:26)    MOVQ    $5, ""..autotmp_5+160(SP)
    0x0055 00085 (main.go:26)    PCDATA    $2, $2
    0x0055 00085 (main.go:26)    LEAQ    go.string."asong"(SB), CX
    0x005c 00092 (main.go:26)    PCDATA    $2, $1
    0x005c 00092 (main.go:26)    MOVQ    CX, ""..autotmp_5+152(SP)
    0x0064 00100 (main.go:26)    MOVQ    AX, ""..autotmp_2+72(SP)
    0x0069 00105 (main.go:26)    PCDATA    $2, $2
    0x0069 00105 (main.go:26)    PCDATA    $0, $2
    0x0069 00105 (main.go:26)    LEAQ    go.itab.*"".User,"".Basic(SB), CX
    0x0070 00112 (main.go:26)    PCDATA    $2, $1
    0x0070 00112 (main.go:26)    MOVQ    CX, "".u+104(SP)
    0x0075 00117 (main.go:26)    PCDATA    $2, $0
    0x0075 00117 (main.go:26)    MOVQ    AX, "".u+112(SP)

下面这段汇编代码作用就是赋值给非空接口的 iface 构造,组装了 iface 的内存布局,因为下面剖析了非空接口的,这里就不细讲了,了解他的意思就好。接下来咱们看一下他是如何进行类型断言的。

    0x00df 00223 (main.go:29)    PCDATA    $2, $1
    0x00df 00223 (main.go:29)    PCDATA    $0, $2
    0x00df 00223 (main.go:29)    MOVQ    "".u+112(SP), AX
    0x00e4 00228 (main.go:29)    PCDATA    $0, $0
    0x00e4 00228 (main.go:29)    MOVQ    "".u+104(SP), CX
    0x00e9 00233 (main.go:29)    PCDATA    $2, $3
    0x00e9 00233 (main.go:29)    LEAQ    go.itab.*"".User,"".Basic(SB), DX
    0x00f0 00240 (main.go:29)    PCDATA    $2, $1
    0x00f0 00240 (main.go:29)    CMPQ    CX, DX
    0x00f3 00243 (main.go:29)    JEQ    250
    0x00f5 00245 (main.go:29)    JMP    583
    0x00fa 00250 (main.go:29)    MOVQ    AX, "".u1+56(SP)

下面代码咱们能够看到调用 iface 构造中的 itab 字段,这里为什么这么调用呢?因为咱们类型推断的是一个具体的类型,编译器会间接结构出 iface,不会去调用曾经在runtime/iface.go 实现好的断言办法。上述代码中,先结构出 iface,其中 *itab 存在内存 +104(SP) 中,unsafe.Pointer 存在 +112(SP) 中。而后在类型推断的时候又从新结构了一遍 *itab,最初将新的 *itab 和前一次 +104(SP) 里的 *itab 进行比照。

前面的赋值操作也就不再细说了,没有什么特地的。

这里还有一个要留神的问题,如果咱们类型断言的是接口类型,那么咱们在就会看到这样的汇编代码:

// 代码批改
func main() {var u Basic = &User{Name: "asong"}
    v, ok := u.(Basic)
    if !ok {fmt.Printf("%v\n", v)
    }
}
    // 局部汇编代码
    0x008c 00140 (main.go:27)    MOVUPS    X0, ""..autotmp_4+168(SP)
    0x0094 00148 (main.go:27)    PCDATA    $2, $1
    0x0094 00148 (main.go:27)    MOVQ    "".u+128(SP), AX
    0x009c 00156 (main.go:27)    PCDATA    $0, $0
    0x009c 00156 (main.go:27)    MOVQ    "".u+120(SP), CX
    0x00a1 00161 (main.go:27)    PCDATA    $2, $4
    0x00a1 00161 (main.go:27)    LEAQ    type."".Basic(SB), DX
    0x00a8 00168 (main.go:27)    PCDATA    $2, $1
    0x00a8 00168 (main.go:27)    MOVQ    DX, (SP)
    0x00ac 00172 (main.go:27)    MOVQ    CX, 8(SP)
    0x00b1 00177 (main.go:27)    PCDATA    $2, $0
    0x00b1 00177 (main.go:27)    MOVQ    AX, 16(SP)
    0x00b6 00182 (main.go:27)    CALL    runtime.assertI2I2(SB)

咱们能够看到,间接调用的是 runtime.assertI2I2() 办法进行类型断言,这个办法的实现代码如下:

func assertI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil {
        // explicit conversions require non-nil interface value.
        panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
    }
    if tab.inter == inter {
        r.tab = tab
        r.data = i.data
        return
    }
    r.tab = getitab(inter, tab._type, false)
    r.data = i.data
    return
}

上述代码逻辑很简略,如果 iface 中的 itab.inter 和第一个入参 *interfacetype 雷同,阐明类型雷同,间接返回入参 iface 的雷同类型,布尔值为 true;如果 iface 中的 itab.inter 和第一个入参 *interfacetype 不雷同,则从新依据 *interfacetypeiface.tab 去结构 tab。结构的过程会查找 itabTable。如果类型不匹配,或者不是属于同一个 interface 类型,都会失败。getitab() 办法第三个参数是 canfail,这里传入了 true,示意构建 *itab 容许失败,失败当前返回 nil

差别 :如果咱们断言的类型是具体类型,编译器会间接结构出iface,不会去调用曾经在runtime/iface.go 实现好的断言办法。如果咱们断言的类型是接口类型,将会去调用相应的断言办法进行判断。

小结 非空接口类型断言的本质是 iface 中 *itab 的比照。*itab 匹配胜利会在内存中组装返回值。匹配失败间接清空寄存器,返回默认值。

类型断言的性能损耗

后面咱们曾经剖析了断言的底层原理,上面咱们来看一下不同场景下进行断言的代价。

针对不同的场景能够写出测试文件如下(截取了局部代码,全副代码获取戳这里):

var dst int64

// 空接口类型间接类型断言具体的类型
func Benchmark_efaceToType(b *testing.B) {b.Run("efaceToType", func(b *testing.B) {var ebread interface{} = int64(666)
        for i := 0; i < b.N; i++ {dst = ebread.(int64)
        }
    })
}

// 空接口类型应用 TypeSwitch 只有局部类型
func Benchmark_efaceWithSwitchOnlyIntType(b *testing.B) {b.Run("efaceWithSwitchOnlyIntType", func(b *testing.B) {var ebread interface{} = 666
        for i := 0; i < b.N; i++ {OnlyInt(ebread)
        }
    })
}

// 空接口类型应用 TypeSwitch 所有类型
func Benchmark_efaceWithSwitchAllType(b *testing.B) {b.Run("efaceWithSwitchAllType", func(b *testing.B) {var ebread interface{} = 666
        for i := 0; i < b.N; i++ {Any(ebread)
        }
    })
}

// 间接应用类型转换
func Benchmark_TypeConversion(b *testing.B) {b.Run("typeConversion", func(b *testing.B) {
        var ebread int32 = 666

        for i := 0; i < b.N; i++ {dst = int64(ebread)
        }
    })
}

// 非空接口类型判断一个类型是否实现了该接口 两个办法
func Benchmark_ifaceToType(b *testing.B) {b.Run("ifaceToType", func(b *testing.B) {var iface Basic = &User{}
        for i := 0; i < b.N; i++ {iface.GetName()
            iface.SetName("1")
        }
    })
}

// 非空接口类型判断一个类型是否实现了该接口 12 个办法
func Benchmark_ifaceToTypeWithMoreMethod(b *testing.B) {b.Run("ifaceToTypeWithMoreMethod", func(b *testing.B) {var iface MoreMethod = &More{}
        for i := 0; i < b.N; i++ {iface.Get()
            iface.Set()
            iface.One()
            iface.Two()
            iface.Three()
            iface.Four()
            iface.Five()
            iface.Six()
            iface.Seven()
            iface.Eight()
            iface.Nine()
            iface.Ten()}
    })
}

// 间接调用办法
func Benchmark_DirectlyUseMethod(b *testing.B) {b.Run("directlyUseMethod", func(b *testing.B) {
        m := &More{Name: "asong",}
        m.Get()})
}

运行后果:

goos: darwin
goarch: amd64
pkg: asong.cloud/Golang_Dream/code_demo/assert_test
Benchmark_efaceToType/efaceToType-16            1000000000               0.507 ns/op
Benchmark_efaceWithSwitchOnlyIntType/efaceWithSwitchOnlyIntType-16              384958000                3.00 ns/op
Benchmark_efaceWithSwitchAllType/efaceWithSwitchAllType-16                      351172759                3.33 ns/op
Benchmark_TypeConversion/typeConversion-16                                      1000000000               0.473 ns/op
Benchmark_ifaceToType/ifaceToType-16                                            355683139                3.38 ns/op
Benchmark_ifaceToTypeWithMoreMethod/ifaceToTypeWithMoreMethod-16                85421563                12.8 ns/op
Benchmark_DirectlyUseMethod/directlyUseMethod-16                                1000000000               0.000000 ns/op
PASS
ok      asong.cloud/Golang_Dream/code_demo/assert_test  7.797s

从后果咱们能够剖析一下:

  • 空接口类型的类型断言代价并不高,与间接类型转换简直没有性能差别
  • 空接口类型应用 type switch 进行类型断言时,随着 case 的增多性能会直线降落
  • 非空接口类型进行类型断言时,随着接口中办法的增多,性能会直线降落
  • 间接进行办法调用要比非接口类型进行类型断言要高效很多

好啦,当初咱们也晓得怎么应用类型断言能进步性能啦,又能够和共事吹水一手啦。

总结

好啦,本文到这里就曾经靠近序幕了,在最初做一个小小的总结:

  • 空接口类型断言实现流程:空接口类型断言本质是将 eface_type与要匹配的类型进行比照,匹配胜利在内存中组装返回值,匹配失败间接清空寄存器,返回默认值。
  • 非空接口类型断言的本质是 iface 中 *itab 的比照。*itab 匹配胜利会在内存中组装返回值。匹配失败间接清空寄存器,返回默认值
  • 泛型是在编译期做的事件,应用类型断言会耗费一点性能,类型断言应用形式不同,带来的性能损耗也不同,具体请看下面的章节。

文中代码已上传github:https://github.com/asong2020/…star

好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者继续创作更多优质内容的能源!

创立了一个 Golang 学习交换群,欢送各位大佬们踊跃入群,咱们一起学习交换。入群形式:加我 vx 拉你入群,或者公众号获取入群二维码

结尾给大家发一个小福利吧,最近我在看 [微服务架构设计模式] 这一本书,讲的很好,本人也收集了一本 PDF,有须要的小伙能够到自行下载。获取形式:关注公众号:[Golang 梦工厂],后盾回复:[微服务],即可获取。

我翻译了一份 GIN 中文文档,会定期进行保护,有须要的小伙伴后盾回复 [gin] 即可下载。

翻译了一份 Machinery 中文文档,会定期进行保护,有须要的小伙伴们后盾回复 [machinery] 即可获取。

我是 asong,一名普普通通的程序猿,让咱们一起缓缓变强吧。欢送各位的关注,咱们下期见~~~

举荐往期文章:

  • Go 看源码必会常识之 unsafe 包
  • 源码分析 panic 与 recover,看不懂你打我好了!
  • 详解并发编程根底之原子操作(atomic 包)
  • 详解 defer 实现机制
  • 空构造体引发的大型打脸现场
  • Leaf—Segment 分布式 ID 生成零碎(Golang 实现版本)
  • 十张动图带你搞懂排序算法(附 go 实现代码)
  • go 参数传递类型
  • 手把手教姐姐写音讯队列
  • 常见面试题之缓存雪崩、缓存穿透、缓存击穿
  • 详解 Context 包,看这一篇就够了!!!
  • 面试官:你能用 Go 写段代码判断以后零碎的存储形式吗?
  • 如何平滑切换线上 Elasticsearch 索引
正文完
 0