generics - go reflect for metaprogramming/templates -
i have code receives protobuf messages duplicated in couple places want put library. problem exact protobuf message that's used different each set of code.
edit: , don't have flexiblity of restructuring them.
i'm not entirely sure possible solve without duplicate code in go, did make attempt (below). doing wrong, or not that's not possible? (note: stripped down code, in real code 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 error handling, or set _ resp.header = &testmsg.respheader{} //call handler function unique per service //the signature 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 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 called when receiving zmq request resp := ms.handler(reqmsgs) log.info(resp) }
when try compile errors like:
resp.header undefined (type reflect.value has no field or method header) cannot use resp (type reflect.value) type proto.message in argument proto.marshal: reflect.value not implement proto.message (missing protomessage method)
which make complete sense since resp isn't connected ms.resptype.
from point of view, protobuf definition specific. cleaned down great deal. example: there no need have different request , response header per type when differ in content. obvious eliminated specific request , reponse types, because again, differed in semantic meaning, on other hand rather obvious surrounding code. way, have eliminated lot of redundancy. in sum, different types of requests con identified headers, either presence or absence of user_id
field or evaluation of content
field. of course, can expand headers value
selection 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, see miniservice
rather odd. you'd set service things daos, maybe other services , have them handle individual requests taking in request object , returning response object. grpc services defined .proto
file (staying within example)
service miniservice { rpc userinfo(exchange) returns (exchange) }
which after compiling .proto
defines following interface
type miniservice interface { userinfo(ctx context.context, in *exchange) (*exchange, error) }
you don't have use grpc, shows how deal services, because else, daos, loggers , such needs field in struct implementing said interface. 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 dao…") dao := &daomock{ users: []string{"alice", "bob", "mallory"}, logger: logger, } logger.println("main: setting service…") service := &miniservice{ dao: dao, logger: logger, } // first, valid request req1 := &exchange{ header: []*header{ &header{ value: &header_userid{userid: 0}, }, }, } if resp1, err := service.userinfo(req1); err != nil { logger.printf("main: error returned on request: %s\n", err.error()) } else { fmt.println(">", string(resp1.getcontent())) } // missing useridheader causes error returned // header creation compacted brevity nouseridheader := &exchange{header: []*header{&header{value: &header_content{content: "foo"}}}} if resp2, err := service.userinfo(nouseridheader); err != nil { logger.printf("main: error returned 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 returned 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 magic happens: // identify different types of requests presence or absence // of headers _, hdr := range in.getheader() { v := hdr.getvalue() if i, ok := v.(*header_userid); ok { idhdr = } } 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 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, requests , responses more generic , suitable being used in various types of requests, without changing format , without need reflection. not saying golden bullet, however. others might come solutions better fit needs. hth.
Comments
Post a Comment