去反映 metaprogramming/templates

go reflect for metaprogramming/templates

我有一些代码可以接收 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。

在我看来,您的 Protobuf 定义过于具体。我把它清理干净了很多。例如:当不同的只是内容时,没有必要为每种类型设置不同的请求和响应 header。最明显的是我删除了特定的请求和响应类型,因为再次强调,它们的不同之处在于它们的语义含义,另一方面,从周围的代码中可以看出这一点。这样,我们消除了很多冗余。总之,不同类型的请求可以由 header 标识,无论是 user_id 字段的存在与否,还是 content 字段的评估。当然,您可以根据需要扩展 headers 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 之类的东西或其他服务来设置服务,并让它们处理接受请求 object 并返回响应 object 的单个请求。 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和Response更加通用,适合在各种类型的请求中使用,不需要改变格式,不需要反射。然而,我并不是说这是金弹。其他人可能会提出更适合您需求的解决方案。但是我hth.

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

我创建了一个简单的 "template",我可以将其复制并放入 protobuf 消息名称中。然后我使用 go generate 来构建我需要的消息。这让我可以在指定类型的代码中放置特殊的 go 生成注释 - 因此即使有模板,填充它并在单个 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=
REQ=
REP=
PKG="${LIB//-/}"
if [ "$#" -ne 4 ]; then
    LOG=${PKG}
else
    LOG=
fi

HANDLERS_DIR=$(dirname "[=13=]")/../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 generatebuild_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 评论,因此在某些情况下,我创建了不止 1 个处理程序。