为了账号安全,请及时绑定邮箱和手机立即绑定

去反思元编程/模板

去反思元编程/模板

Go
幕布斯7119047 2021-12-07 18:52:46
我有一些接收 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 miniserviceimport "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 mainimport "github.com/golang/protobuf/proto"import "testmsg"import "mylibs/mini-service"这是完全合理的,因为 resp 没有连接到 ms.respType。
查看完整描述

2 回答

?
MM们

TA贡献1886条经验 获得超2个赞

在我看来,您的 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 更加通用,适用于各种类型的请求,无需更改格式,无需反射。然而,我并不是说这是金子弹。其他人可能会提出更适合您需求的解决方案。但我...


查看完整回答
反对 回复 2021-12-07
?
收到一只叮咚

TA贡献1821条经验 获得超4个赞

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


我创建了一个简单的“模板”,我可以将其复制并放入 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/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库,它有一个带有PostReq和PostResp类型的请求处理程序。所以我创建了一个处理函数,将这些作为输入:


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

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


查看完整回答
反对 回复 2021-12-07
  • 2 回答
  • 0 关注
  • 157 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信