使用反射进行元编程/模板化。

huangapple go评论76阅读模式
英文:

go reflect for metaprogramming/templates

问题

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

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

我不确定在Go中是否可能解决这个问题而不重复代码,但我尝试了一下(如下所示)。我做错了什么,还是这不可能的事情?(注意:这是简化的代码,在实际代码中,对象有很多额外的字段)

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)
    //添加一些错误处理,或者只是设置_

    resp.Header = &testmsg.RespHeader{}
    //调用每个服务唯一的处理程序函数
    //签名将类似于:
    //handleRequest(reqType,respType)和调用:
    //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() {
    //伪造一个zmq消息
    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)
    //接收到zmq请求时将调用什么
    resp := ms.Handler(reqMsgs)
    log.Info(resp)
}

当我尝试编译时,我会得到类似以下的错误:

resp.Header未定义(类型reflect.Value没有Header字段或方法)

无法将resp(类型reflect.Value)用作proto.Marshal的proto.Message类型参数:
reflect.Value未实现proto.Message(缺少ProtoMessage方法)

这完全有道理,因为resp没有与ms.respType连接起来。

英文:

I have some code that receives protobuf messages that is basically duplicated in a couple places so I want to put it into a library. The problem is that the exact protobuf message that's used is different for each set of code.

EDIT: And I don't have the flexiblity of restructuring them.

I'm not entirely sure this is possible to solve without duplicate code in go, but I did make an attempt (below). Am I doing something wrong, or is this not something that's not possible? (Note: This is stripped down code, in the real code the objects have lots of additional fields)

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)
}

When I try to compile I get errors like:

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)

Which make complete sense since the resp isn't connected to ms.respType.

答案1

得分: 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
}

现在,你的请求和响应更加通用,适用于各种类型的请求,而无需更改格式,也无需使用反射。我并不是说这是万能的解决方案,其他人可能会提出更适合你需求的解决方案。但希望对你有所帮助。

英文:

From my point of view, your Protobuf definition is too specific. I cleaned it down a great deal. For example: There is no need to have a different request and response header per type when all they differ in is the content. The most obvious is that I eliminated the specific request and reponse types, because again, all they differed in was their semantic meaning, which on the other hand is rather obvious from the surrounding code. This way, we have eliminated a lot of redundancy. In sum, different types of requests con be identified by the headers, be it either the presence or absence of a user_id field or the evaluation of the content field. Of course, you can expand the headers value selection by what you need.

// 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;
}

Then, I see your miniservice as rather odd. You'd usually set up a service with things like DAOs, maybe other services and have them handle individual requests taking in a request object and returning a response object. For gRPC services are defined with a .proto file like this (staying within your example)

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

Which after compiling your .proto basically defines the following interface

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

You don't have to use grpc, but it shows how to deal with services, because everything else, like DAOs, loggers and such needs to be a field in the struct implementing said interface. A small example without 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
}

Now, your Requests and Responses are more generic and are suitable for being used in various types of requests, without changing the format and without the need for reflection. I am not saying that this is the golden bullet, however. Others might come up with solutions which better fit your needs. But I hth.

答案2

得分: 1

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

我创建了一个简单的“模板”,可以将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库,其中包含具有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

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

英文:

I ended up completely abandoning reflect. I could work on generic objects, but I could not pass them on to handlers. Not being able to do this made it not worth using a library, so it seemed like a bad approach.

I created a simple "template" that I could copy and drop in the protobuf message names into. I then used go generate to build the messages that I needed. This let me put special go generate comments in my code that specified the types - so even though there is templating, filling it in and using it is done in a single go file.

So I put the base template in src/mylibs/req-handlers/base.tmp.go. I wanted to keep .go as the extension for syntax highlighting. In that file, I had generic things like {{RequestProto}} that would get replaced.

This script defined a ReqHandler type using some template variables:

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

And I created an object that reference the handler function:

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

and later in the code I called the handler function where it was needed:

err = rh.handler(req, resp)

In the bin directory, I added this script, which copies the template, and uses sed to replace some keywords with words I can specify in go code:

#!/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}

Finally to make use of it, testservice.go would then have something like:

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

When I run go generate, build_handler is called, which creates the mylibs/req-handlers/testservicelib library, which has a request handler with the PostReq and PostResp types. So I create a handler function that will have those as inputs:

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

and pass that to my generated library:

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

And life is good.

To build, the Makefile needed an extra step. Both go generate and go build/install steps are needed:

go generate testservice
go install testservice

Note that the generate call will run all the //go:generate comments in testservice.go, so in some cases I have more than 1 handler created.

huangapple
  • 本文由 发表于 2016年2月6日 09:03:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/35236079.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定