Trying to create a microservice within Go, I have a package network to take care of getting the bytes and converting to a particular request:
package network
type Request interface {
}
type RequestA struct {
a int
}
type RequestB struct {
b string
}
func GetRequestFromBytes(conn net.Conn) Request {
buf := make([]byte, 100)
_, _ := conn.Read(buf)
switch buf[0] {
case 0:
// convert bytes into RequestA
requestA = RequestAFromBytes(buf[1:])
return requestA
case 1:
requestB = RequestBFromBytes(buf[1:])
return requestB
}}
Now in the main, I want to handle the request.
package main
import (
"network"
)
(ra *RequestA) Handle() {
// handle here
}
(rb *RequestB) Handle() {
// handle
}
func main() {
// get conn here
req = GetRequestFromBytes(conn)
req.Handle()
}
However, Golang does not allow RequestA/RequestB classes to be extended in the main package. Two solutions I found online were type aliasing/embedding, but in both it looks like we'll have to handle by finding the specific type of the request at runtime. This necessitates a switch/if statement in both network and main packages, which duplicates code in both.
The simplest solution would be to move the switch statement into package main, where we determine the type of the request and then handle it, but then package main would have to deal with raw bytes, which I'd like to avoid. The responsibility of finding the request type and sending it 'upwards' should rest with the network package.
Is there a simple idiomatic Go way to solve this?
Use a type switch in the main package:
switch req := network.GetRequestFromBytes(conn).(type) {
case *network.RequestA:
// handle here, req has type *RequestA in this branch
case *network.RequestB:
// handle here, req has type *RequestB in this branch
default:
// handle unknown request type
}
This avoids encoding knowledge of the protocol bytes in the main package.
Because the question specifies that parsing code is in the network package and handler code is in the main package, it's not possible to avoid updating the code in two packages when a new request type is added. Even if the language supported the feature proposed in the question, it would still be necessary to update two packages when a new request type is added.
If you wrap a return value in an interface you will always have to either type switch on it to process it based on its type in this kind of scenario.
The alternative for the way you are approaching would be to insert the processing function into the Request objects dynamically by adding a method to the Request
interface which allowed you register a handler and another method Handle()
which you could call to process each message by calling the registered handler. Experience suggests that you would end up with a bunch of boilerplate in each Request type definition though.
In go I would probably take a different approach and not use an OO kind of design where you make each request type it's own "class". I would probably define the network package more along the lines of the code below. (Caveat: this isn't tested, there might be a few typos and theres some error stuff that needs filled in)
type RequestType byte
const (
ARequest RequestType = iota
BRequest
CRequest
EndRequestRange
)
type HandlerFunc func([]byte) error
type MessageInterface struct {
handlers map[RequestType]HandlerFunc
// other stuff
}
func NewMessageInterface() *MessageInterface {
m := &MessageInterface {
handlers: make(map[RequestType]HandlerFunc),
}
// Do other stuff
return m
}
func (m *MessageInterface) AddHandler(rt RequestType, h HandlerFunc) error {
if rt >= EndRequestRange {
// return an error
}
m.handlers[rt]=h
return nil
}
func (m *MessageInterface) ProcessNextMessage() error {
buf := make([]byte, 100)
_, _ := conn.Read(buf)
h, ok := m.handlers[ReqestType(buff[0])]
if !ok {
// probably return error
}
return h(buff[1:])
}
In your main package you would then initialize MessageInterface
, call AddHandler()
to add each handler during initialization you want and repeatedly call ProcessNextMessage()
during your main processing.
It's a different approach from the design direction you were going in, but for me a bit more of a go approach.
(Note as well - I didn't do anything to make sure the code was safe for concurrency and this kind of approach is normally used in a way that each handler is called in its own goroutine. If you made that change you might want some mutex protection around the map as well as checking a few other aspects)