GOLANG实现类似C++模板,返回符合类型的对象

来源:互联网 发布:淘宝红包下载安装 编辑:程序博客网 时间:2024/05/17 03:04

原文参考:https://gocn.io/article/319

在协议解析中,C++的模板有比较大的作用,有时候我们希望丢弃所有的包,只留下特定类型的包。参考SRS的代码SrsRtmpClient::connect_app2:

类型系统的设计, SrsConnectAppResPacket继承自SrsPacket

class SrsPacket;class SrsConnectAppResPacket : public SrsPacket

协议栈提供了expect_message模板函数,接收特定类型的包:

SrsCommonMessage* msg = NULL;SrsConnectAppResPacket* pkt = NULL;if ((ret = protocol.expect_message<SrsConnectAppResPacket>(&msg, &pkt)) != ERROR_SUCCESS) {    return ret;}SrsAmf0Any* data = pkt->info->get_property("data");SrsAmf0EcmaArray* arr = data->to_ecma_array();SrsAmf0Any* prop = arr->ensure_property_string("srs_server_ip");string srs_server_ip = prop->to_str();

在向服务器发送了ConnectApp后,就等待ConnectAppRes响应包,丢弃所有的其他的。这个时候,类型SrsConnectAppResPacket就作为了一个参数,也就是C++的模板。如果是GOLANG怎么实现呢?没有直接的办法的,因为没有泛型。

在GOLANG中,也需要定义个interface,参考Packet,当然也是有ConnectAppResPacket实现了这个接口(Message是原始消息,它的Payload可以Unmarshal为Packet):

type Message struct { Payload []byte }type Packet interface {} // Message.Payload = Packet.Marshal()type ConnectAppResPacket struct { Args amf0.Amf0 }

第一种方法,协议栈只需要收取Message,然后解析Message为Packet,收到packet后使用类型转换,判断不是自己需要的包就丢弃:

func (v *Protocol) ReadMessage() (m *Message, err error)func (v *Protocol) DecodeMessage(m *Message) (pkt Packet, err error)

不过这两个基础的API,User在使用时,比较麻烦些,每次都得写一个for循环:

var protocol *Protocolfor {    var m *Message    m,_ = protocol.ReadMessage()    var p Packet    p,_ = protocol.DecodeMessage(m)    if res,ok := p.(*ConnectAppResPacket); ok {        if data, ok := res.Args.Get("data").(*amf0.EcmaArray); ok {            if data, ok := data.Get("srs_server_ip").(*amf0.String); ok {                srs_server_ip = string(*data)            }        }    }}

比较方便的做法,就是用回调函数,协议栈需要提供个ExpectPacket方法:

func (v *Protocol) ExpectPacket(filter func(m *Message, p Packet)(ok bool)) (err error)

这样可以利用回调函数可以访问上面函数的作用域,直接转换类型和设置目标类型的包:

var protocol *Protocolvar res *ConnectAppResPacket_ = protocol.ExpectPacket(func(m *Message, p Packet) (ok bool){    res,ok = p.(*ConnectAppResPacket)})if data, ok := res.Args.Get("data").(*amf0.EcmaArray); ok {    if data, ok := data.Get("srs_server_ip").(*amf0.String); ok {        srs_server_ip = string(*data)    }}

这样已经比较方便了,不过还是需要每次都给个回调函数。要是能直接这样用就好了:

var protocol *Protocolvar res *ConnectAppResPacket_ = protocol.ExpectPacket(&res)if data, ok := res.Args.Get("data").(*amf0.EcmaArray); ok {    if data, ok := data.Get("srs_server_ip").(*amf0.String); ok {        srs_server_ip = string(*data)    }}

这样也是可以做到的,不过协议栈函数要定义为:

func (v *Protocol) ExpectPacket(ppkt interface{}) (err error)

在函数内部,使用reflect判断类型是否符合要求,设置返回值。代码参考ExpectPacket,下面是一个简要说明:

func (v *Protocol) ExpectPacket(ppkt interface{}) (m *Message, err error) {    // 由于ppkt是**ptr, 所以取类型后取Elem(),就是*ptr,用来判断是否实现了Packet接口。    ppktt := reflect.TypeOf(ppkt).Elem()    // ppktv是发现匹配的包后,设置值的。    ppktv := reflect.ValueOf(ppkt)    // 要求参数必须是实现了Packet,避免传递错误的值进来。    if required := reflect.TypeOf((*Packet)(nil)).Elem(); !ppktt.Implements(required) {        return nil,fmt.Errorf("Type mismatch")    }    for {        m, err = v.ReadMessage()        pkt, err = v.DecodeMessage(m)        // 判断包是否是匹配的那个类型,如果不是就丢弃这个包。        if pktt = reflect.TypeOf(pkt); !pktt.AssignableTo(ppktt) {            continue        }        // 相当于 *ppkt = pkt,类似C++中对指针的指针赋值。        ppktv.Elem().Set(reflect.ValueOf(pkt))        break    }    return}

遗憾的就是这个参数ppkt类型不能是Packet,因为会有类型不匹配;也不能是*Packet,因为在GOLANG中传递接口的引用本身是很奇怪的;这个参数只能是interface{}。不过用法也很简单,只是需要注意参数的传递。

var res *ConnectAppResPacket// 这是正确的做法,传递res指针的地址,相当于指针的指针。_ = protocol.ExpectPacket(&res)// 这是错误的做法,会在ExpectPacket检查返回错误,没有实现Packet接口_ = protocol.ExpectPacket(res)

用起来还不错。

0 0
原创粉丝点击