【问题标题】:go reflect for metaprogramming/templates去反映元编程/模板
【发布时间】:2016-02-06 01:03:38
【问题描述】:

我有一些代码接收 protobuf 消息,这些消息基本上在几个地方重复,所以我想把它放到一个库中。问题是每组代码使用的确切 protobuf 消息不同。

编辑:而且我没有重组它们的灵活性。

我不完全确定这是否可以在没有重复代码的情况下解决,但我确实尝试过(如下)。我做错了什么,还是这不是不可能的事情? (注意:这是精简的代码,在实际代码中,对象有很多额外的字段)

Example.proto:

package testmsg;

enum RepStatus {
    DONE_OK = 0;
    DONE_ERROR = 1;
}

message ReqHeader {
    optional int64 user_id = 1;
}

message RespHeader {
    optional RepStatus status = 1;
    optional string error_msg = 2;
}

message PostReq {
    optional ReqHeader header = 1;
    optional bytes post_data = 2;
}

message PostResp {
    optional RespHeader header = 1;
}

message StatusReq {
    optional ReqHeader header = 1;
    optional string id = 2;
}

message StatusRep {
    optional RespHeader header = 1;
    optional string status = 2;
}

mini-service/service.go:

package miniservice

import "reflect"
import "github.com/golang/protobuf/proto"
import "testmsg"

type MiniService struct {
    name string
    reqType reflect.Type
    repType reflect.Type
}

func NewService(name string, reqPort int, reqType proto.Message, repType proto.Message) *MiniService {
    ms := new(MiniService)
    ms.name = name
    ms.reqType = reflect.TypeOf(reqType)
    ms.repType = reflect.TypeOf(repType)
    return ms
}

func (ms *MiniService) Handler(msgs []string) (string) {
    resp := reflect.New(ms.repType.Elem())

    msg := msgs[0]
    req := reflect.New(ms.reqType.Elem())
    err := proto.Unmarshal([]byte(msg), req)
    //add some error handling, or just get set _

    resp.Header = &testmsg.RespHeader{}
    //Call handler function that is unique per service
    //the signature will be something like:
    //handleRequest(reqType, respType) & called like:
    //handleRequest(req, resp)
    resp.Header.Status = testmsg.RepStatus_DONE_OK.Enum()

    respMsg, _ := proto.Marshal(resp)
    return string(respMsg)
}

testservice.go:

package main
import "github.com/golang/protobuf/proto"
import "testmsg"
import "mylibs/mini-service"

func main() {
    //fake out a zmq message
    req := &testmsg.PostReq{}
    req.Header = &testmsg.ReqHeader{}
    req.Header.MessageId = proto.Int64(10)
    reqMsg, _ := proto.Marshal(req)
    reqMsgs := []string{string(reqMsg)}

    ms := miniservice.NewService("tester", 5555, testmsg.PostReq, testmsg.PostResp)
    //What will be called when receiving a zmq request
    resp := ms.Handler(reqMsgs)
    log.Info(resp)
}

当我尝试编译时,我收到如下错误:

resp.Header undefined (type reflect.Value has no field or method Header)

cannot use resp (type reflect.Value) as type proto.Message in argument to proto.Marshal:
reflect.Value does not implement proto.Message (missing ProtoMessage method)

这很有意义,因为 resp 没有连接到 ms.respType。

【问题讨论】:

    标签: templates generics go metaprogramming


    【解决方案1】:

    在我看来,您的 Protobuf 定义过于具体。我把它清理了很多。例如:当它们的不同之处在于内容时,不需要每种类型有不同的请求和响应标头。最明显的是我去掉了特定的请求和响应类型,因为它们的区别只是它们的语义,另一方面,这从周围的代码中是相当明显的。这样,我们就消除了很多冗余。总之,不同类型的请求可以由标头标识,无论是user_id 字段的存在或不存在,还是content 字段的评估。当然,您可以根据需要扩展标题value 选择。

    // exchange.proto
    syntax = "proto2";
    package main;
    
    enum Status {
        DONE_OK = 0;
        DONE_ERROR = 1;
    }
    
    message Header {
        required string name = 1;
        oneof value {
            int32 user_id = 2;
            Status status = 3;
            string content= 4;
        }
    }
    
    message Exchange {
        repeated Header header = 1;
        optional bytes content = 2;
    }
    

    那么,我觉得你的 miniservice 很奇怪。您通常会使用诸如 DAO 之类的东西设置服务,也许还有其他服务,并让它们处理接收请求对象并返回响应对象的单个请求。对于gRPC 服务是用这样的.proto 文件定义的(保留在您的示例中)

    service Miniservice {
      rpc UserInfo(Exchange) returns (Exchange)
    }
    

    编译你的.proto之后基本上定义了如下接口

    type Miniservice interface {
        UserInfo(ctx context.Context, in *Exchange) (*Exchange, error)
    }
    

    您不必使用 grpc,但它显示了如何处理服务,因为其他所有内容,如 DAO、记录器等都需要成为实现所述接口的结构中的字段。一个没有grpc的小例子

    //go:generate protoc --go_out=. exchange.proto
    
    package main
    
    import (
        "fmt"
        "log"
        "os"
    )
    
    var (
        statusName = "Status"
        userIdName = "uid"
    )
    
    func main() {
    
        logger := log.New(os.Stderr, "SRVC ", log.Ltime|log.Lshortfile)
    
        logger.Println("Main: Setting up dao…")
        dao := &daoMock{
            Users:  []string{"Alice", "Bob", "Mallory"},
            Logger: logger,
        }
    
        logger.Println("Main: Setting up service…")
    
        service := &Miniservice{
            DAO:    dao,
            Logger: logger,
        }
    
        // First, we do a valid request
        req1 := &Exchange{
            Header: []*Header{
                &Header{
                    Value: &Header_UserId{UserId: 0},
                },
            },
        }
    
        if resp1, err := service.UserInfo(req1); err != nil {
            logger.Printf("Main: error was returned on request: %s\n", err.Error())
        } else {
            fmt.Println(">", string(resp1.GetContent()))
        }
    
    
        // A missing UserIdHeader causes an error to be returned
        // Header creation compacted for brevity
        noUserIdHeader := &Exchange{Header: []*Header{&Header{Value: &Header_Content{Content: "foo"}}}}
    
        if resp2, err := service.UserInfo(noUserIdHeader); err != nil {
            logger.Printf("Main: error was returned by service: %s\n", err.Error())
        } else {
            fmt.Println(">", string(resp2.GetContent()))
        }
    
        // Self explanatory
        outOfBounds := &Exchange{Header: []*Header{&Header{Value: &Header_UserId{UserId: 42}}}}
    
        if resp3, err := service.UserInfo(outOfBounds); err != nil {
            logger.Printf("Main: error was returned by service: %s\n", err.Error())
    
        } else {
            fmt.Println(">", string(resp3.GetContent()))
        }
    }
    
    
    type daoMock struct {
        Users  []string
        Logger *log.Logger
    }
    
    func (d *daoMock) Get(id int) (*string, error) {
    
        d.Logger.Println("DAO: Retrieving data…")
        if id > len(d.Users) {
            d.Logger.Println("DAO: User not in 'database'...")
            return nil, fmt.Errorf("id %d not in users", id)
        }
    
        d.Logger.Println("DAO: Returning data…")
        return &d.Users[id], nil
    }
    
    type Miniservice struct {
        Logger *log.Logger
        DAO    *daoMock
    }
    
    func (s *Miniservice) UserInfo(in *Exchange) (out *Exchange, err error) {
    
        var idHdr *Header_UserId
    
        s.Logger.Println("UserInfo: retrieving ID header")
    
        // Here is where the magic happens:
        // You Identify different types of requests by the presence or absence
        // of certain headers
        for _, hdr := range in.GetHeader() {
            v := hdr.GetValue()
            if i, ok := v.(*Header_UserId); ok {
                idHdr = i
            }
        }
    
        if idHdr == nil {
            s.Logger.Println("UserInfo: invalid request")
            return nil, fmt.Errorf("invalid request")
        }
    
        u, err := s.DAO.Get(int(idHdr.UserId))
    
        if err != nil {
            s.Logger.Printf("UserInfo: accessing user data: %s", err.Error())
            return nil, fmt.Errorf("error accessing user data: %s", err.Error())
        }
    
        /* ----------------- create the response ----------------- */
        statusHeader := &Header{
            Name:  &statusName,
            Value: &Header_Status{Status: Status_DONE_OK},
        }
        userHeader := &Header{
            Name:  &userIdName,
            Value: &Header_UserId{UserId: idHdr.UserId},
        }
    
        s.Logger.Println("UserInfo: sending response")
    
        return &Exchange{
            Header:  []*Header{statusHeader, userHeader},
            Content: []byte(*u),
        }, nil
    }
    

    现在,您的 Requests 和 Responses 更加通用,适用于各种类型的请求,无需更改格式,也无需进行反射。然而,我并不是说这是金子弹。其他人可能会提出更适合您需求的解决方案。但是我hth。

    【讨论】:

    • 不幸的是,我在更改消息方面没有任何灵活性:(如果我这样做了,我认为这会解决所有问题。
    • @CharlesL。然后只需为每种请求类型添加一个处理程序。
    【解决方案2】:

    我最终完全放弃了反射。我可以处理通用对象,但我无法将它们传递给处理程序。由于无法做到这一点,因此不值得使用库,因此这似乎是一种糟糕的方法。

    我创建了一个简单的“模板”,我可以将 protobuf 消息名称复制并放入其中。然后我使用go generate 来构建我需要的消息。这让我可以在我的代码中添加特殊的 go generate cmets 来指定类型 - 所以即使有模板,也可以在一个 go 文件中填写和使用它。

    所以我把基本模板放在src/mylibs/req-handlers/base.tmp.go。我想保留.go 作为语法高亮的扩展。在那个文件中,我有一些通用的东西,比如 {{RequestProto}} 会被替换。

    此脚本使用一些模板变量定义了ReqHandler 类型:

    type ReqHandlerFunc func(req *testmsg.{{RequestProto}}, resp *testmsg.{{ResponseProto}}) error
    

    我创建了一个引用处理函数的对象:

    func NewReqHandler(name string, handler ReqHandlerFunc) *ReqHandler {
        ...
        rh.handler = handler
        return rh
    }
    

    后来在代码中,我在需要的地方调用了处理函数:

    err = rh.handler(req, resp)
    

    在bin目录下,我添加了这个脚本,它复制了模板,并使用sed将一些关键字替换为我可以在go代码中指定的单词:

    #!/bin/bash
    
    if [ "$#" -ne 3 ] && [ "$#" -ne 4 ]; then
        echo "Usage: build_handler (Package Name) (Request Proto Name) (Response Proto Name) [Logger Name]"
        exit 1
    fi
    
    LIB=$1
    REQ=$2
    REP=$3
    PKG="${LIB//-/}"
    if [ "$#" -ne 4 ]; then
        LOG=${PKG}
    else
        LOG=$4
    fi
    
    HANDLERS_DIR=$(dirname "$0")/../src/mylibs/req-handlers
    
    #Generate go code
    mkdir -p ${HANDLERS_DIR}/${LIB}/
    GEN_FILE=${HANDLERS_DIR}/${LIB}/${LIB}_handler.go
    cp ${HANDLERS_DIR}/base.tmpl.go ${GEN_FILE}
    sed -i"" -e "s/{{PackageName}}/${PKG}/g" ${GEN_FILE}
    sed -i"" -e "s/{{LoggerName}}/${LOG}/g" ${GEN_FILE}
    sed -i"" -e "s/{{RequestProto}}/${REQ}/g" ${GEN_FILE}
    sed -i"" -e "s/{{ResponseProto}}/${REP}/g" ${GEN_FILE}
    

    最后要使用它,testservice.go 会有类似的东西:

    //go:generate build_handler testservicelib PostReq PostResp
    import "mylibs/req-handlers/testservicelib"
    

    当我运行go generate 时,会调用build_handler,这会创建mylibs/req-handlers/testservicelib 库,该库具有PostReqPostResp 类型的请求处理程序。所以我创建了一个处理函数,它将这些作为输入:

    func handleRequest(req *testmsg.PostReq, resp *testmsg.PostResp) error {
        ...
    }
    

    并将其传递给我生成的库:

    reqHandler := testservicelib.NewReqHandler("test", handleRequest)
    

    生活是美好的。

    要构建,Makefile 需要一个额外的步骤。 go generate 和 go build/install 步骤都需要:

    go generate testservice
    go install testservice
    

    请注意,生成调用将运行testservice.go 中的所有//go:generate cmets,因此在某些情况下,我创建了多个处理程序。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-01-25
      • 1970-01-01
      • 1970-01-01
      • 2013-05-06
      • 2018-11-11
      • 2015-12-03
      • 1970-01-01
      • 2012-08-29
      相关资源
      最近更新 更多