golang实践-小心接口与实现的动态绑定

背景

知道吗?自定义类型的实例为空,且函数方法不访问类型实例的私有属性时,可以正常运行!
这段时间,我们采用了practoractor来处理有状态对象的并发,避免锁带来的困扰。但有时候,会出现非常奇怪的问题,就是框架反复重启对象,且不受控制,即使采用supervision策略也无效。这问题在过去几个月反复出现都没找到原因,最终在修改对象读取读取数据初始化代码的时候,才明白是某些特殊条件成立,返回对象是空造成的。分析背后的原因,涉及到语言特点和框架设计,很有价值。

示例代码

type Person interface {
    Name()
}
type Male struct {
    name string
}

func (m *Male) Name() {
    fmt.Println("male no name")
    //fmt.Println("male is ",m.name) //<-在运行时,替换上一行。
}
//返回一个空对象
func NewMale() *Male {
    return nil
}
func main() {
    var m = NewMale()
    if m == nil {
        fmt.Println("Male instance is nil! ")
    }
    if reflect.ValueOf(m).IsNil() {
        fmt.Println("Male instance is nil!")
    }
    m.Name()

    var p Person = NewMale()
    if p == nil {
        fmt.Println("Person instance is nil!")
    }
    if reflect.ValueOf(p).IsNil() {
        fmt.Println("Person  implemention instance is nil!")
    }
    p.Name()
}

运行后,竟然会发现能够正常运行结果是:

Male instance is nil! 
Male instance is nil!
male no name
Person  implemention instance is nil!
male no name

再按照代码注释,替换后,就会报错:

Male instance is nil! 
Male instance is nil!
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x108703d]

goroutine 1 [running]:
main.(*Male).Name(0x0)
    /Users/apple/dev/GoglandProjects/hgzb/src/study/main.go:17 +0x8d
main.main()
    /Users/apple/dev/GoglandProjects/hgzb/src/study/main.go:33 +0x8f
exit status 2

造成这种问题的原因,其实在于go对接口实现采用了动态绑定。

类型没有实例,也可以执行

上述代码其实反映了重要的现象:go运行时,类型定义中的函数与数据是分离的
具体要从go的接口定义分析,其实材料非常多。我们很容易查到源代码runtime2.go中接口定义的数据结构:

type iface struct {
    tab *itab
    data unsafe.Pointer
}
//tab字段类似于C++的vptr,tab中包含了对应的方法数组,除此之外还保存了实现该接口的类型元数据。data是对应的实现该接口的类型的实例指针。
//itab数据结构如下:
type itab struct {
    inter     *interfacetype
    _type     *_type
    link      *itab
    bad       int32
    unused    int32
    fun       [0]unsafe.Pointer
}

如果结合gdb,我们可以看到类型信息里保存了接口与实际对象的元数据。同时,itab还用fun数组(不定长结构)保存了实际方法地址,实现在运行期对目标方法的动态调用。
当我们在代码中调用一个接口的方法时,操作类似如下: s.tab->fun[0] (s.data)。我们可以理解为每个类型的方法与数据是分离的,在需要的时候,动态把函数与具体的struct绑定。这也理解了,为什么空对象的函数竟然可以执行的情况。

进一步了解go的itab运行时绑定的过程,可以查看老外的好文:Go Data Structures: Interfaces。阅读时,对照看看网友写的c++对象模型详解,图画的不错,更系统的毫无疑问是《深度探索c++对象模型》(侯捷翻译那本)。

actor模型的对象管理

上述问题,在一般代码中其实很容易发现,只是容易忽略。但是,如果在actor模型中,会麻烦了。
熟悉akka就会知道,actor的接口定义就是一个receive方法。框架先将需要运行的业务对象放到一个管理器中,在启动、重启的时候,进行孵化。框架执行时,会向具体实现对象发送以下消息:

  • 启动时,发送Start消息,让对象启动后立即执行。
  • 正常运行时,发送业务消息,让对象调用自己的业务实现,同时拦截崩溃。
  • 遇到业务崩溃,根据策略,通过发送restart、stop(Poison pill)消息,实现重启或停止。

由于go的语言特性——没有构造函数,也没有kotlin的空判断。如果不注意,很可能向框架传入空的类型实例。业务运行时,框架传入的消息调用到私有方法或属性时,就会拦截到崩溃消息(go 采用defer,别告诉我不知道怎么拦截),必然又向该对象再发送一次消息,又崩溃。周而复始。
由于我们用到的actor模型没有空对象判定机制,因此出现了这种情况会更常见一些。
protoactor的该问题讨论

图确实不好画,我就不再写了,理解akka框架,喜欢研读源代码的,肯定明白^_^

小结

go作为面向接口的语言,一定要注意类型、方法、字段三者的关系。
- 函数跟着类型走,定义了类型就会有函数。
- 字段(属性)跟着struct走
- 函数执行时,需要的时候才会访问struct的相应字段
如果想深入了解类型、对象、方法、字段,建议看看《元素模式》,写的很好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值