Wednesday, January 22, 2020

Dynamic Function Call using Reflection in GO





I've had a case where a GO based server had received requests over the network.
The requests include a request type in the URL, and the parameters as JSON in the request body.

I could use the echo library for this.
The echo library ask for each handler to handle the data conversion itself, for example:


func handler(c echo.Context) {
  u := new(User)
  if err = c.Bind(u); err != nil {
    return
  }
  // do thing with the User input
}


But something felt wrong...
Why should I manually convert the input to a specific type?
Instead I would prefer to send the input structure directly to the function.
Something like this:


func handler(u User) {
  // do thing with the User input
}

So, I've decided to create a reflection based caller to implement this.
The reflection API activate the related function as well as handles the type conversion.
This is very similar to Java based Spring REST controller.


The Dynamic Activator


The dynamic activator handles casting the bytes input to the function input structure, as well as activation of a the function. It holds list of registered functions:


type Dynamic struct {
   functions map[string]interface{}
}

func (d *Dynamic) Init() {
   d.functions = make(map[string]interface{})
}


To register a function, we add the pointer of the function:


func (d *Dynamic) Register(function interface{}) {
   fullName := runtime.FuncForPC(reflect.ValueOf(function).Pointer()).Name()
   sections := strings.Split(fullName, ".")
   name := sections[len(sections)-1]
   d.functions[name] = function}


And to run the function, we use GO reflection.
The reflection loads the type of the function parameter from the function itself, and then uses the type to de-serialize the bytes array from JSON into the related fields.


func (d *Dynamic) Execute(name string, data []byte) {
   function := d.functions[name]
   functionValue := reflect.ValueOf(function)
   functionType := functionValue.Type()
   parameterType := reflect.New(functionType.In(0))
   err := json.Unmarshal(data, parameterType.Interface())
   if err != nil {
      panic(err)
   }

   parameterValue := parameterType.Elem()
   transactionResult := functionValue.Call([]reflect.Value{parameterValue})

   returnValue := transactionResult[0].Interface()
   if returnValue != nil {
      err := returnValue.(error)
      panic(err)
   }
}

That's all, so simple.


Usage Example


Let see how can we use the Dynamic Activator.
First, let create 2 functions. 
Notice:

  • Each function receives different type of input structure
  • The input parameters structures must use upper case fields to enable JSON serialization


type Parameters1 struct {
   Price int `json:"price"`}

type Parameters2 struct {
   Id     string `json:"id"`   Amount int    `json:"amount"`}

func func1(p *Parameters1) error {
   fmt.Printf("the price is %v\n", p.Price)
   return nil}

func func2(p *Parameters2) error {
   if p.Id == "-1" {
      return errors.New("bad id")
   }
   fmt.Printf("the amount is %v\n", p.Amount)
   return nil}

Now let register the functions, and dynamically activate each function.


d := Dynamic{}
d.Init()
d.Register(func1)
d.Register(func2)
d.Execute("func1", []byte(`{"price":1}`))
d.Execute("func2", []byte(`{"id":"5","Amount":7}`))
d.Execute("func2", []byte(`{"id":"-1","Amount":2}`))

and the output is:


the price is 1
the amount is 7
panic: bad id


Final Thoughts


Despite the many advantages of GO over Java, GO younger than Java, and still has a way to go before reaching the maturity of Java and SPRING.
Maybe in one of the next versions, the GO echo library developers would take this step forward, and even start the next GO-SPRING...



No comments:

Post a Comment