去反思元编程/模板

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


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


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


示例.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;

}

迷你服务/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"


这是完全合理的,因为 resp 没有连接到 ms.respType。


幕布斯7119047
浏览 160回答 2
2回答

MM们

在我看来,您的 Protobuf 定义太具体了。我把它清理了很多。例如:当它们的不同之处在于内容时,不需要为每种类型设置不同的请求和响应标头。最明显的是我删除了特定的请求和响应类型,因为同样,它们的不同之处在于它们的语义,另一方面,从周围的代码中可以很明显地看出这一点。这样,我们就消除了很多冗余。总之,不同类型的请求可以通过标头识别,无论是user_id字段的存在或不存在,还是字段的评估content。当然,您可以value根据需要扩展标题选择。// exchange.protosyntax = "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.protopackage mainimport (    "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 更加通用,适用于各种类型的请求,无需更改格式,无需反射。然而,我并不是说这是金子弹。其他人可能会提出更适合您需求的解决方案。但我...

收到一只叮咚

我最终完全放弃了反射。我可以处理通用对象,但我无法将它们传递给处理程序。无法做到这一点使得使用库变得不值得,所以这似乎是一种糟糕的方法。我创建了一个简单的“模板”,我可以将其复制并放入 protobuf 消息名称中。然后我用来go generate构建我需要的消息。这让我可以在我的代码中放置特殊的 go generate 注释来指定类型 - 所以即使有模板,填写和使用它也是在一个 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/bashif [ "$#" -ne 3 ] && [ "$#" -ne 4 ]; then    echo "Usage: build_handler (Package Name) (Request Proto Name) (Response Proto Name) [Logger Name]"    exit 1fiLIB=$1REQ=$2REP=$3PKG="${LIB//-/}"if [ "$#" -ne 4 ]; then    LOG=${PKG}else    LOG=$4fiHANDLERS_DIR=$(dirname "$0")/../src/mylibs/req-handlers#Generate go codemkdir -p ${HANDLERS_DIR}/${LIB}/GEN_FILE=${HANDLERS_DIR}/${LIB}/${LIB}_handler.gocp ${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 PostRespimport "mylibs/req-handlers/testservicelib"当我运行时go generate,build_handler被调用,它创建了一个mylibs/req-handlers/testservicelib库,它有一个带有PostReq和PostResp类型的请求处理程序。所以我创建了一个处理函数,将这些作为输入:func handleRequest(req *testmsg.PostReq, resp *testmsg.PostResp) error {    ...}并将其传递给我生成的库:reqHandler := testservicelib.NewReqHandler("test", handleRequest)而且生活还不错。为了构建,Makefile 需要一个额外的步骤。需要 go generate 和 go build/install 步骤:go generate testservicego install testservice请注意,generate 调用将运行 中的所有//go:generate注释testservice.go,因此在某些情况下,我创建了 1 个以上的处理程序。
打开App,查看更多内容
随时随地看视频慕课网APP