去反映 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 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
请注意,生成调用将 运行 testservice.go
中的所有 //go:generate
评论,因此在某些情况下,我创建了不止 1 个处理程序。
我有一些代码可以接收 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 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
请注意,生成调用将 运行 testservice.go
中的所有 //go:generate
评论,因此在某些情况下,我创建了不止 1 个处理程序。