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

前端之家收集整理的这篇文章主要介绍了GOLANG实现类似C++模板,返回符合类型的对象前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

原文参考: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 *Protocol

for {
    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 *Protocol

var 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 *Protocol

var res *ConnectAppResPacket
_ = protocol.ExpectPacket(&res)

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)

用起来还不错。

猜你在找的Go相关文章