0

I'm creating a basic REST service. My intent is to write the logic for the resources as abstractly as possible. What I mean is if I have already created a CRUD logic for endpoint /devices for example, then when I need a new resource endpoint like /cars, I should not be repeating myself over the CRUD procedures.

In another language like Python, classes and methods are first class objects and that can be stored in a list or dictionary (map) and then instantiated as needed. In Go it doesn't seem as easy. I tried to use the reflect package.

First I create a TypeRegistry according to this.

var TypeRegistry = make(map[string]reflect.Type)
TypeRegistry["devices"] = reflect.TypeOf(models.Device{}) // models.Device{} is the Gorm SQL table model

Then I have handler creator which is intended to handle the creation of all types of resources like this (error handling redacted):

func CreateOneHandler(typeString string) func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        defer r.Body.Close()

        jsn, _ = ioutil.ReadAll(r.Body)
        jsonBytes, _ := datamapper.CreateOne(typeString, jsn)

        w.Write(jsonBytes)
    }
}

I'm using Chi, so I bind the handlers like this:

func addRoute(r chi.Router, endpoint string, typeString string) {
    r.Route("/"+endpoint, func(r chi.Router) {
        typeString := endpoint
        r.Post("/", CreateOneHandler(typeString))
    })
}

The idea is to, after defining the Gorm models, simply add routes by calling it repeatedly, addRoute(r, "devices"); addRoute(r, "cars") for a consistent REST interface across multiple models.

Now within CreateOne() I want to insert something into the table:

func CreateOne(typeString string, json []byte) ([]byte, error) {
    modelType := typeregistry.TypeRegistry[typeString]
    value := reflect.New(modelType)
    db.Create(modelPtr.Elem()) // ==> Now this doesn't work 
}

How do I make it work? Gorm said "create failed no such table: value". Because a reflect value or reflect type isn't the same as if I were just to instantiate objects the regular way. How do I make it work?

(A side note: given the static nature of the type switch and type assertions, I am already compromising some of my designs which would probably be possible in a language like Python. It seems to me like it's unavoidable to litter code with type switches which tried to check whether it is a device, car or any number of new models explicitly. In a regular object-oriented language maybe this would be simple polymorphic method call. Any pointer to better design would be appreciated as well.)

robbieperry22
  • 1,753
  • 1
  • 18
  • 49
huggie
  • 17,587
  • 27
  • 82
  • 139
  • To get the value from a `reflect.Value` use `.Interface()`, i.e. `modelPtr.Elem().Interface()`, although I believe you should leave it as a pointer and omit the `Elem()` so that `db.Create` can modify the value if need be. – mkopriva Feb 13 '20 at 10:10
  • ... that said do you really need reflection for this? Are you doing some other stuff that you've omitted from the question for brevity, or is all this just for initializing a new instance? Because if that's the case than just do something like this: https://play.golang.com/p/Fv9CDG7WUMb – mkopriva Feb 13 '20 at 10:12
  • @mkopriva Thanks it works. Why do I need an Interface() call? I thought passing it over to an interface parameter should already be automatically wrapped in one. Also you might be right about reflect not being the right tool. It was due to my knee-jerk reaction and my aversion toward switch cases. And now it seems like reflect doesn't exactly do what I intend it to do and I've compounded the problem. – huggie Feb 13 '20 at 11:53
  • *"I thought passing it over to an interface parameter should already be automatically wrapped in one."* You're correct there, however, most methods on `reflect.Value` return another `reflect.Value` which is not something you would want to store into the db, or return through an api to your clients, what you want is the "underlying" value of the `reflect.Value` and `.Interface()` does actually *unwrap* the underlying value. So `var i = (interface{})(modelPtr)` and `var i = modelPtr.Interface()` both result in `i`s having type `interface{}` however their underlying types will be very different. – mkopriva Feb 13 '20 at 12:03
  • https://play.golang.com/p/8rj2ATTmhLZ – mkopriva Feb 13 '20 at 12:06

0 Answers0