深入 go interface 底层原理

打个广告:欢迎关注我的微信公众号,在这里您将获取更全面、更新颖的文章!

原文链接:深入 go interface 底层原理 欢迎点赞关注

什么是 interface

在 Go 语言中,interface(接口)是一种抽象的类型定义。它主要用于定义一组方法的签名,但不包含方法的实现细节。接口提供了一种规范,任何类型只要实现了接口中定义的所有方法,就被认为是实现了该接口。这使得不同的类型可以以一种统一的方式被处理,增强了代码的灵活性、可扩展性和可维护性。

例如,如果定义一个 Shape 接口,包含一个 Area 方法:

type Shape interface {
    Area() float64
}

// 圆形结构体
type Circle struct {
    Radius float64
}

// 圆形实现面积计算方法
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

// 矩形结构体
type Rectangle struct {
    Length, Width float64
}

// 矩形实现面积计算方法
func (r Rectangle) Area() float64 {
    return r.Length * r.Width
}

// 打印形状面积的函数
func printArea(s Shape) {
    fmt.Printf("面积: %.2f\n", s.Area())
}

func main() {
    circle := Circle{Radius: 5}
    rectangle := Rectangle{Length: 4, Width: 6}

    printArea(circle)
    printArea(rectangle)
}

那么无论是 Circle 结构体还是 Rectangle 结构体,只要它们都实现了 Area 方法,就都可以被当作 Shape 类型来使用。

接口在 Go 语言中广泛应用于解耦代码、实现多态性、定义通用的行为规范等场景。它让代码更加模块化和易于管理,有助于提高代码的质量和可复用性。

底层数据结构

在 Go 语言中,有两种“interface”,一种是空接口(`interface{}`),它可以存储任意类型的值;另一种是非空接口,这种接口明确地定义了一组方法签名,只有实现了这些方法的类型才能被认为是实现了该非空接口。 下面讨论一下这两种接口的底层实现。

空接口与非空接口

在 Go 语言中,空接口的底层数据结构是 runtime.eface :

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

_type 字段指向一个 _type 结构体,该结构体包含了所存储值的详细类型信息,data 字段则是一个 unsafe.Pointer ,它直接指向实际存储的数据的内存地址。

非空接口的底层数据结构是 runtime.iface:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

同样,data 字段也是一个指向实际数据的指针。然而,这里的重点是 tab 字段,它指向一个 itab 结构体。

itab 结构体

itab 结构体包含接口的类型信息和指向数据的类型信息:

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.
}

type interfacetype struct {
    typ     _type // 接口的类型
    pkgpath name  // 接口所在路径
    mhdr    []imethod // 接口所定义的方法列表
}
  1. inter 字段描述了接口自身的类型信息,包括接口所定义的方法等。

  2. _type 字段存储了实际值的类型信息。

  3. hash 字段是对 _type 结构体中哈希值的拷贝,它在进行类型比较和转换等操作时能够提供快速的判断依据。

  4. fun 字段则是一个动态大小的函数指针数组,当fun[0]=0时,表示_type并没有实现该接口(这里指的是itab下的_type),当实现了接口时,fun存放了第一个接口方法的地址,其他方法依次往后存放。在这里fun存储的其实是接口方法对应的实际类型的方法,每次调用发方法时实行动态分派。

_type 结构体

_type是runtime对Go任意类型的内部表示。

type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    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
}
  • size描述类型的大小

  • hash数据的hash值

  • align指对齐

  • fieldAlgin是这个数据嵌入结构体时的对齐

  • kind是一个枚举值,每种类型对应了一个编号

  • alg是一个函数指针的数组,存储了hash/equal这两个函数操作。

  • gcdata存储了垃圾回收的GC类型的数据,精确的垃圾回收中,就是依赖于这里的gcdata

  • nameOff和typeOff为int32,表示类型名称和类型的指针偏移量,这两个值会在运行期间由链接器加载到runtime.moduledata结构体中,通过以下两个函数可以获取偏移量

func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}

若t为_type类型,那么调用resolveTypeOff(t,t.ptrToThis)可以获得t的一份拷贝t’。

Go 语言各种数据类型都是在 _type 字段的基础上,增加一些额外的字段来进行管理的:

type arraytype struct {
    typ   _type
    elem  *_type
    slice *_type
    len   uintptr
}

type chantype struct {
    typ  _type
    elem *_type
    dir  uintptr
}

type slicetype struct {
    typ  _type
    elem *_type
}

type structtype struct {
    typ     _type
    pkgPath name
    fields  []structfield
}

这些数据类型的结构体定义,是反射实现的基础。

类型转换

接口转接口

path:/usr/local/go/src/runtime/iface.go

func convI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil {
        return
    }
        // 接口类型相同直接赋值
    if tab.inter == inter {
        r.tab = tab
        r.data = i.data
        return
    }
        // 否则生成新的itab
    r.tab = getitab(inter, tab._type, false)
    r.data = i.data
    return
}

数据类型转空接口

path:/usr/local/go/src/runtime/iface.go

func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
    if raceenabled {
        raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
    }
    if msanenabled {
        msanread(elem, t.size)
    }
        // mallockgc一块新内存
    x := mallocgc(t.size, t, true)
    // TODO: We allocate a zeroed object only to overwrite it with actual data.
    // 值赋值进去
    typedmemmove(t, x, elem)
        // _type直接复制
    e._type = t
        // data指向新分配的内存
    e.data = x
    return
}

数据类型转非空接口

path:/usr/local/go/src/runtime/iface.go

func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
        // 如果开启了竞争检测,通过竞争检测读取数据
    if raceenabled {
        raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
    }
        // 如果开启了 The memory sanitizer (msan)
    if msanenabled {
        msanread(elem, t.size)
    }
        // 重新分配内存
    x := mallocgc(t.size, t, true)
        // 对内存做优化
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}

可以看到从接口转接口是没有发生内存的重新分配的,如果类型相同直接进行了赋值;而从数据类型转接口重新分配了内存,这是因为Go里面接口转换时接口本身的数据不能被改变,所以接口可以使用同一块内存;而数据类型转换的接口可能数据发生改变,为了避免改变接口数据,所以重新分配了内存并拷贝原来的数据。这也是反射非指针变量时无法直接改变变量数据的原因,因为反射会先将变量转换为空接口类型。以上只是举例了itab相关的部分函数,所有类型转换相关的函数都在iface.go文件下。

类型断言

接口-接口断言

path:/usr/local/go/src/runtime/iface.go

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
}

有两种形式,一个返回值或两个返回值。一个返回值时,断言失败会panic(getitab失败会panic),两个则会返回是否断言成功(此时getitab的canfail参数传true,失败是会return nil,从而不会panic)。可以看到接口断言直接比较了inter,一样则直接赋值,否则调用getitab再做判断。

接口-类型断言

接口断言是否转换为具体类型,是编译器直接生成好的代码去做的。代码示例:

package main
import "fmt"
var EBread interface{}
var a int
var EVALUE = 666
func main() {
    EBread = EVALUE
    a = EBread.(int)
    fmt.Print(a)
}

执行go tool compile -S main.go >> main.S进行反汇编,查看汇编代码。将变量定义为全局变量,这样汇编代码中会以"".的方式引用变量,可以直接看到变量名。

由于汇编代码较长,这里只摘取主要部分做分析。

0x0058 00088 (main.go:9)    MOVQ    AX, "".EBread+8(SB) // AX存放EBread的数据值
0x005f 00095 (main.go:10)   MOVQ    "".EBread(SB), CX   // CX=
0x0066 00102 (main.go:10)   LEAQ    type.int(SB), DX    // DX=type.int的偏移地址
0x006d 00109 (main.go:10)   CMPQ    CX, DX              // 比较CX和DX,即比较type.int与EBread的数据类型是否相同
0x0070 00112 (main.go:10)   JNE 232                 // 不等则跳转至232
0x0072 00114 (main.go:10)   MOVQ    (AX), AX            // 解引用
0x0075 00117 (main.go:10)   MOVQ    AX, "".a(SB)        // 赋值

可以看到empty interface转类型是编译器直接生成代码进行的对比,而非运行时调用函数进行动态的对比。

Interface 的陷阱

nil 判断

我们先看看下面例子:

func main() {
    var a *int
    var b interface{}
    fmt.Println("a == nil", a == nil)
    fmt.Println("b == nil", b == nil)
    
    b = nil 
    fmt.Println("b == nil", b == nil)
    
    b = a 
    fmt.Println("b == nil", b == nil)
}

$ go run main.go
a == nil true
b == nil true
b == nil true
b == nil false

对于接口判断 == nil 时,只有接口所指向的类型和值都为 nil 时接口才为 nil,如果想比较准确的判断接口类型是否是 nil 可以使用反射实现,但是有一定性能开销。

package main

import (
    "fmt"
    "reflect"
)

func isInterfaceNil(i interface{}) bool {
    if i == nil {
        return true
    }
    value := reflect.ValueOf(i)
    switch value.Kind() {
    case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
        return value.IsNil()
    default:
        return false
    }
}
func main() {
    var myInterface interface{}
    var myType *MyType = nil
    myInterface = myType

    fmt.Println(isInterfaceNil(myInterface))
}

值与指针接收者

先看下面几个例子:

情况一:使用指针作为接收者实现接口,使用结构体值类型调用接口方法,编译不通过。

type Duck interface {
    Quack()
}

type Cat struct{}

// 使用指针作为接收者实现接口
func (c *Cat) Quack() {
    fmt.Println("meow")
}

func main() 
    var c Duck = Cat{}
    // 使用结构体值类型调用方法
    c.Quack()
}

$ go build interface.go
./interface.go:20:6: cannot use Cat literal (type Cat) as type Duck in assignment:
        Cat does not implement Duck (Quack method has pointer receiver)

情况二:使用值类型作为接收者实现接口,使用结构体值类型调用接口方法,编译通过。

type Duck interface {
    Quack()
}

type Cat struct{}

// 使用值类型作为接收者实现接口
func (c Cat) Quack() {
    fmt.Println("meow")
}

func main() 
    var c Duck = Cat{}
    // 使用结构体值类型调用方法
    c.Quack()
}

$ go run interface.go
meow

情况三:使用指针类型作为接收者实现接口,使用结构体指针类型调用方法,编译通过。

type Duck interface {
    Quack()
}

type Cat struct{}

// 使用指针类型作为接收者实现接口
func (c *Cat) Quack() {
    fmt.Println("meow")
}

func main() 
    var c Duck = &Cat{}
    // 使用结构体指针类型调用方法
    c.Quack()
}

$ go run interface.go
meow

情况四:使用值类型作为接收者实现接口,使用结构体指针类型调用方法,编译通过。

type Duck interface {
    Quack()
}

type Cat struct{}

// 使用值类型作为接收者实现接口
func (c Cat) Quack() {
    fmt.Println("meow")
}

func main() 
    var c Duck = &Cat{}
    // 使用结构体指针类型调用方法
    c.Quack()
}

$ go run interface.go
meow
使用结构体指针类型调用方法使用结构体值类型调用接口方法
使用指针作为接收者实现接口通过不通过
使用值类型作为接收者实现接口通过通过

Interface 的应用

依赖倒置

依赖倒置(Dependency Inversion Principle,DIP) 是软件设计中的一个重要原则。

其核心思想是:

  1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象(接口或抽象类)。

  2. 抽象不应该依赖于细节,细节应该依赖于抽象。

通过遵循依赖倒置原则,可以带来以下好处:

  1. 提高代码的灵活性和可维护性:当低层模块的实现发生变化时,高层模块不需要进行大量的修改,只需要更改依赖的抽象的实现即可。

  2. 促进模块之间的解耦:使得各个模块之间的依赖关系更加清晰和松散,降低了模块之间的耦合度。

下面以数据库调用为例:

package main

import "fmt"

// 定义数据库操作接口
type DBOperation interface {
        QueryData(key string) string
}

// 实现 MySQL 数据库操作
type MySQLDB struct{}

func (m MySQLDB) QueryData(key string) string {
        return fmt.Sprintf("Querying from MySQL with key: %s", key)
}

// 实现 PostgreSQL 数据库操作
type PostgreSQLDB struct{}

func (p PostgreSQLDB) QueryData(key string) string {
        return fmt.Sprintf("Querying from PostgreSQL with key: %s", key)
}

// 业务服务结构体,依赖于数据库操作接口
type BusinessService struct {
        db DBOperation
}

// 业务方法
func (b BusinessService) ProcessData(key string) {
        result := b.db.QueryData(key)
        fmt.Println(result)
}

func main() {
        // 使用 MySQL 数据库
        mysql := MySQLDB{}
        serviceWithMySQL := BusinessService{mysql}
        serviceWithMySQL.ProcessData("user1")

        // 使用 PostgreSQL 数据库
        postgres := PostgreSQLDB{}
        serviceWithPostgres := BusinessService{postgres}
        serviceWithPostgres.ProcessData("user2")
}

在上述示例中:

  • 定义了 DBOperation 接口,包含 QueryData 方法。
  • 有 MySQLDB 和 PostgreSQLDB 两个不同的数据库实现了该接口。
  • BusinessService 结构体依赖于 DBOperation 接口,而不是具体的数据库实现。

这样,BusinessService 的代码不与具体的数据库实现紧密耦合,实现了依赖倒置。当需要切换数据库时,只需要注入不同的数据库实现对象,无需修改 BusinessService 的核心逻辑。

比如,后续如果要支持新的数据库,如 MongoDB,只需要创建新的结构体实现 DBOperation 接口,并在使用处注入即可。

策略模式

策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。以支付为例,用户可以选择微信或者支付宝进行支付:

// 定义接口
type Payer interface {
    CreateOrder()
    PayRpc()
    UpdateOrder()
}

// 支付宝支付实现
type Alipay struct {}
func (a *Alipay)CreateOrder(){
    // ...
}
func (a *Alipay)PayRpc(){
    // ...
}
func (a *Alipay)UpdateOrder(){
    // ...
}

// 微信支付实现
type Wxpay struct {}
func (w *Wxpay)CreateOrder(){
// ...
}
func (w *Wxpay)PayRpc(){
// ...
}
func (w *Wxpay)UpdateOrder(){/
/ ...
}

// 工厂+策略模式
func NewPayer(PayType string) Payer {
    switch PayType {
    case "alipay":return &Alipay{}
    case "weixin":return &Wxpay{}
    // case "other":
            // retrun &OtherPay{}
    }
}
    
func Pay(arg) {
    payer := NewPayer(arg.type)
    
    payer.CreateOrder()
    payer.PayRpc()
    payer.UpdateOrder()
}

实现多态

多态是一种运行期的行为,它有以下几个特点:

  1. 一种类型具有多种类型的能力

  2. 允许不同的对象对同一消息做出灵活的反应

  3. 以一种通用的方式对待个使用的对象

  4. 非动态语言必须通过继承和接口的方式来实现

package main

import "fmt"

// 定义形状接口
type Shape interface {
    Area() float64
}

// 圆形结构体
type Circle struct {
    Radius float64
}

// 圆形实现面积计算方法
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

// 矩形结构体
type Rectangle struct {
    Length, Width float64
}

// 矩形实现面积计算方法
func (r Rectangle) Area() float64 {
    return r.Length * r.Width
}

// 打印形状面积的函数
func printArea(s Shape) {
    fmt.Printf("面积: %.2f\n", s.Area())
}

func main() {
    circle := Circle{Radius: 5}
    rectangle := Rectangle{Length: 4, Width: 6}

    printArea(circle)
    printArea(rectangle)
}

参考

https://mp.weixin.qq.com/s/Wadii1L9-fg6bJBfYJV0mQ

https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/

https://qcrao91.gitbook.io/go/interface/

本文由mdnice多平台发布

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值