0

I'm new to Go so please bear with me if this is a trivial problem. I am using a home grown "type registry" to map type names to their type, so as to generate them dynamically based on use cases that point to the various type names (I'm basically trying for a simple solution to polymorphic Aggregation JSON response structures in Elasticsearch, but of course this could apply to many other dynamic/polymorphic situations). I'm using the solution provided by dolmen in this question: is there a way to create an instance of a struct from a string? :

var typeRegistry = make(map[string]reflect.Type)

func registerType(typedNil interface{}) {
    t := reflect.TypeOf(typedNil).Elem()
    typeRegistry[t.Name()] = t
}

func init() {
    registerType((*playlistIDAggregation)(nil))
    registerType((*srcIDAggregation)(nil))
    registerType((*assetIDAggregation)(nil))
}

func makeInstance(name string) interface{} {
    return reflect.New(typeRegistry[name]).Elem().Interface()
}

I then want to use my dynamically generated struct as the target for the JSON unmarshalling of the Aggregations node in my ES response:

playlistIDAgg := makeInstance("playlistIDAggregation")
err = json.Unmarshal(esResponse.Aggregations, &playlistIDAgg)

This isn't working like I want it to, as the Unmarshal is trying to unmarshall into an empty interface instead of the underlying struct type. it's putting the data under "data" nodes in the playlistIDAgg variable, and those data fields are of course map[string]interface{}. Am I just missing the way to type assert my playlistIDAgg interface or is there a better way to do this?

EDIT--- The questions in the comments made me realize an edit to this question was long overdue. In my particular case, the structs I defined, to bind to my Bucket aggregations returned by Elasticsearch, have a similar structure and vary only by their root JSON tag, which ES uses to name the aggregation and strongly type it. E.g.

type <name>Aggregation struct {
    Agg BucketAggregationWithCamIDCardinality `json:"<name>"`
}

So, rather than a type registry, my particular problem could be solved by dynamically setting the JSON tag on the struct based on the particular use case.

In addition, a heavier but more robust option would be to leverage Oliver Eilhard's Elasticsearch Go client lib, called Elastic, which has built-in support for all the ES aggregation response structures: https://github.com/olivere/elastic/

Fabien Coppens
  • 273
  • 4
  • 12
  • playlistIDAgg already holds a pointer to your desired type, try removing “&” when you’re passing it to unmarshal and see what happens. – mkopriva Jan 31 '20 at 22:08
  • Removing the & doesn't work, as ```json.Unmarshal``` expects a pointer. I get this error at runtime: json: Unmarshal(non-pointer elasticsearch.playlistIDAggregation) – Fabien Coppens Jan 31 '20 at 22:22
  • 1
    I missed that you’re using Elem when registering, in that case remove the “&” but also remove the Elem from makeInstance, the one right after reflect.New – mkopriva Jan 31 '20 at 22:24
  • That works, but then I have to explicitly type assert the interface to access the struct's fields, i.e. ```buckets := playlistIDAgg.(*playlistIDAggregation).Agg.Buckets```, which is what I was trying to avoid all along, as I'd have to do a switch. There must another small piece that eludes me? – Fabien Coppens Jan 31 '20 at 22:59
  • 1
    @FabienCoppens A type assertion to a concrete type is always required to access fields on the concrete type. – Charlie Tumahai Feb 01 '20 at 02:28
  • @CeriseLimón I get that. But what I'm trying to accomplish is to have a way to dynamically determine the concrete type to type assert to. Otherwise my "type registry" is useless and I might as well have a switch statement with everything explicit for every struct type. That's what I'm trying to generify, because you can imagine how tedious and unmaintainable this can be if in the future I am manipulating hundreds of different types of aggregations (i.e. structs) – Fabien Coppens Feb 01 '20 at 05:40
  • @FabienCoppens you're saying you want to dynamically do a type assertion to then access *the fields* of a concrete type... do all of your types have the same fields? What do you imagine should happen if this dynamic assertion, asserts a type, that does not have the fields you're trying to manipulate? – mkopriva Feb 01 '20 at 06:22
  • 1
    @FabienCoppens What is the higher-level problem that you are trying to solve? There may be solutions to that problem that don't involve a type registry. – Charlie Tumahai Feb 01 '20 at 08:26
  • Great questions, and I should have been more explicit. In my case, the different aggregations have a shared structure, they only differ by the key of the root aggregation. In ES, you give a specific name to each aggregation, and that name becomes the root JSON node under the Aggregations node. There is a quick and dirty way to solve this by giving all of the aggregations the same name, such as "rootAgg", but then we lose the strong typing of the aggregation in the ES response. – Fabien Coppens Feb 03 '20 at 23:34
  • Here's what the aggregation structs look like: ```type playlistIDAggregation struct { Agg BucketAggregationWithCamIDCardinality `json:"playlistId"` } ``` Maybe the problem to slve here is to make the json annotation value dynamic... – Fabien Coppens Feb 03 '20 at 23:39

1 Answers1

1

I changed makeInstance function by getting address of element and add target structs with exposed fields.

func makeInstance(name string) interface{} {
    return reflect.New(typeRegistry[name]).Elem().Addr().Interface()
}

Here is the working code

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type playlistIDAggregation struct {
    PlaylistID string
}

type srcIDAggregation struct {
    SrcID string
}

type assetIDAggregation struct {
    AssetID string
}

var typeRegistry = make(map[string]reflect.Type)

func registerType(typedNil interface{}) {
    t := reflect.TypeOf(typedNil).Elem()
    typeRegistry[t.Name()] = t
}

func init() {
    registerType((*playlistIDAggregation)(nil))
    registerType((*srcIDAggregation)(nil))
    registerType((*assetIDAggregation)(nil))
}

func makeInstance(name string) interface{} {
    return reflect.New(typeRegistry[name]).Elem().Addr().Interface()
}

func main() {
    playlistIDAgg := makeInstance("playlistIDAggregation")
    fmt.Printf("Type = %[1]T => %#[1]v\n", playlistIDAgg)
    err := json.Unmarshal([]byte(`{"PlayListID": "dummy-id"}`), &playlistIDAgg)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Type = %[1]T => %#[1]v\n", playlistIDAgg)
}

https://play.golang.org/p/dn19_iG5Xjz

Ozan
  • 1,044
  • 1
  • 9
  • 23