2 回答
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 更加通用,适用于各种类型的请求,无需更改格式,无需反射。然而,我并不是说这是金子弹。其他人可能会提出更适合您需求的解决方案。但我...
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 个以上的处理程序。
- 2 回答
- 0 关注
- 166 浏览
添加回答
举报