Go: Dynamic type cast/assertion of struct's with interface (to call methods and use struct commons)
Asked Answered
H

4

7

I cracked my brain trying to make my code shorter and cleaner. The problem is in one function, that is working with different structs, that implements one interface.

In some cases I need the model variable to implement the structure (slice of rowModel's) ([]rowModel) and some times I need to use methods from interface. The code is not short, sorry for that. So I put main comments in the code below.

Here is interface:

type StatModel interface {
    FilterData(Filter)
    ClusterData(Filter)
    CountDataForChart(string)[]ChartElement
    GroupByTreeGroups(Filter)[]OrgPack
}

type StatRow interface {
    Count( name string) float64
}

This interfaces are created for methods calls, and to make code shorter. But Interface cannot have fields or structure as Abstruct class in OOP. One of the models is here:

 type NoaggModel []NoaggRow

 type NoaggRow struct {
    Date             string
    Hour             int
    Id_user          int
    Id_line          float64
    Id_region        int
    Id_tree_devision int
    N_inb            float64
    N_out            float64
    N_hold           float64
    N_abandon        float64
    N_transfer       float64
    T_inb            float64
    T_out           float64
    T_hold           float64
    T_ring           float64
    T_acw            float64
    T_wait           float64
}

type FcrModel  []FcrRow

type FcrRow struct {
    Date             string
    Hour             int
    Id_user          int
    Id_line          float64
    Id_region        int
    Id_tree_devision int
    N_irr            float64
    N_inb            float64
}

So , I'm reading from channel, and getting different structures, and trying to calculate everything correctly. How to make type assertion and method calls correctly in this case?

func receiveLightWork(org <-chan models.OrgPack, request ChartOptions) interface{} {

    modelClusters := make(map[string][]models.OrgPack)

    // here  I fill data into modelClusters

    output := make(map[string][]OrgStat)


    // here I begin loop over clusters of different model types

    for modelName, slice := range modelClusters {

        //here I can't choose what to write
        // model must be convertable to NoaggModel, that is []NoaggRow{}
        // as others AcsiModel, FcrModel ...etc. 
        // Also model.ClusterData(customFilter) must be callable as it is in interface of common model

        var model []interface{} 

        var rowModel interface{}

        switch modelName {

        case "noagg":
            model = model.(models.NoaggModel)
            rowModel = rowModel.(models.NoaggRow{})
        case "acsi":
            model = model.(models.AcsiModel)
            rowModel = rowModel.(models.AcsiRow)
        case "fcr24":
            model = model.(models.FcrModel)
            rowModel = rowModel.(models.FcrRow)
        case "aic":
            model = model.(models.AicModel)
            rowModel = rowModel.(models.AicRow)
        }

        for _, el := range slice {


            modelFields := reflect.ValueOf(&rowModel).Elem()
            sliceFields := reflect.ValueOf(&el.SummorisedData).Elem()

            fieldsTypes := modelFields.Type()

            for i := 6; i < modelFields.NumField(); i++ {
                fmt.Println(" model_field ", fieldsTypes.Field(i).Name )
                modelField := modelFields.Field(i);
                sliceField := sliceFields.Index(i-6) ;

                modelField.Set(reflect.Value(sliceField));
            }

            id_line := sliceFields.Index(len(el.SummorisedData) - 1) ;
            date := sliceFields.FieldByName("PackName");

            modelFields.FieldByName("Id_line").Set(id_line)
            modelFields.FieldByName("Date").Set(date)

     // here append not works, because model is []interface{} and not []NoaggRow or others.
     // Writes [non-interface type []interface {} on left]
            model = append(model, rowModel)
        }


 // here I need to call interface method for model     
        model.ClusterData(customFilter) // now here is unresolved Reference 'ClusterData'

        for _, mod := range model {
          // here some common logick for creating data for chart output
         }    
    }

    return output
}

All help is very highly appreciated. I'll answer to each question on this topic if necessary.

Update 1:

Have modified few things for generating struct's on the fly. Now all is compiling correctly until the place, where I need to get instance of struct. It sees only interface.. The comments and code update is here:

func typeSwitch(model string) (interface{}, interface{}){

    switch model{
        case "noagg":
            fmt.Println("Model type:", model)
            return &models.NoaggModel{}, &models.NoaggRow{}
        case "acsi":
            fmt.Println("Model type:", model)
            return &models.AcsiModel{}, &models.AcsiRow{}
        case "fcr24":
            fmt.Println("Model type:", model)
            return &models.FcrModel{}, &models.FcrRow{}
        case "aic":
            fmt.Println("Model type:", model)
            return &models.AicModel{}, &models.AicRow{}
        default:
            fmt.Println("Unknown")
            return false,false
    }
}


func receiveLightWork(org <-chan models.OrgPack, request ChartOptions) interface{} {

    modelClusters := make(map[string][]models.OrgPack)

    for orgPack := range org {
        // here I fill data into clusters
    }

    output := make(map[string][]OrgStat)

   // here I need common code to put data from clusters in correct structures and call interface methods

    for modelName, slice := range modelClusters {

        model, rowModel := typeSwitch(modelName)

        var data_slice []interface{}

        for _, el := range slice {

            modelFields := reflect.ValueOf(rowModel).Elem()
            fieldsCounter := modelFields.NumField()

            sliceFields := reflect.ValueOf(&el.SummorisedData).Elem()
            sliceObjFields := reflect.ValueOf(&el).Elem()

            fieldsTypes := modelFields.Type()

            for i := 6; i < fieldsCounter; i++ {
                fmt.Println(" model_field ", fieldsTypes.Field(i).Name )
                modelField := modelFields.Field(i);
                sliceField := sliceFields.Index(i-6) ;

                modelField.Set(reflect.Value(sliceField));
            }

            id_line := sliceFields.Index(len(el.SummorisedData) - 1) ;
            date := sliceObjFields.FieldByName("PackName");


            modelFields.FieldByName("Id_line").Set(id_line)
            modelFields.FieldByName("Date").Set(date)

            fmt.Println("row_data : ", rowModel)
            data_slice = append(data_slice, rowModel)
        }

    // here comes : invalid type assertion: data_slice.(model) (non-interface type []interface {} on left           
        dataModel := data_slice.(model)
    // here I need correctly created instance of model 
    // (NoaggModel or FcrModel) with data inside its struct 
    // to work with it and call interface methods that are shown in interface above

    }

    return output
}
Heteropolar answered 19/8, 2016 at 16:53 Comment(9)
you want golang.org/doc/effective_go.html#type_switch - and, just a design philosophy note, sometimes "shorter" and "cleaner" are mutually exclusivePanhellenism
You are not correct. I know about type switches. I don't need the code that is divided/separated by the type of model. I need to switch the model type and save the single logick for all the model structures that can came in, if they implements the main interface. Also , I want to remove 200 + rows of duplicated logick. This code above is already in use with duplicated logick for every model inside switch{ case "noagg" ... case "acsi" ... etc. } I want now to make it simpler to use because the list of models is growing very fastHeteropolar
If you have an interface, you either use a type assertion/switch to get a more specific type, or you use reflection. There's aren't other options, so I'm note sure what you're looking for.Hardfeatured
I have Main interface for all data models, to have all logick of calculation in 1 place. But all models have different structures. So this method exports all data from chanel, and convert it to exact structure, calculate it and sends to the caller clientHeteropolar
You can't make an []interface{} into NoaggModel or FcrModel. You have to create a slice of the correct type then use type assertion/switch on each element to get the correct type and append it to your resulting slice.Sumner
Show the way, how can I create an instance of an exact type (NoaggModel or FcrModel) having only name as a string. I founded a solution how to work with rows, ... But how to create a model variable of a correct Model Type , no ((Heteropolar
Have you check my answer below?Minter
Yes. but returning interfaces was not my option at the end... I'll place here my final variant of the method code tomorrow.Heteropolar
Well, []interface{} is a concrete type. It's a slice; you can't do type assertions directly on it. But if you were to loop over range data_slice, you would get an interface{} value in each loop iteration, which you could attempt type assertions on. This should at least address the compile-time error you're seeing.Nighttime
N
3

Based on how you're skipping over the first six fields in your newItem function, it seems like these properties:

type BaseModel struct {
    Date             string
    Hour             int
    Id_user          int
    Id_line          float64
    Id_region        int
    Id_tree_devision int
}

are common to all models. Why not embed these values?


Is there some reason your OrgPack struct can't just hold a nextIdLine int value or something along those lines? I think that might result in cleaner code than using reflection and slice lengths to compute row id values.


If you did the above two things, you could easily also replace

func newItem(modelName string, el models.OrgPack) interface{}

with

func (el OrgPack) NewNoagg() Noagg
func (el OrgPack) NewFcr() Fcr

or perhaps

type RowFactory interface { New(el OrgPack) StatRow }
type NoaggFactory struct{}
func (_ NoaggFactory) New(el OrgPack) StatRow

In the latter case, you could attach RowFactory properties to your OrgPacks instead of, or in addition to, ModelNames, which would allow you to produce the correct StatRow values without needing to switch over string values.


As you've noted, every case of your switch in receiveLightWork is essentially the same: you create a slice of new elements, "cluster" them somehow, format the output, and return it.

The creation of the slice can be done through a Factory-like interface, as described above. ClusterData is already an interface method. FormatOutput probably should be.

If you move logic that depends on the type of data you're working with into methods for those types, I think it should be possible to achieve a receiveLightWork that looks like this:

    func receiveLightWork(org <-chan models.OrgPack, request ChartOptions) map[string][]OrgStat {
        modelClusters := make(map[string][]models.OrgPack)

        for orgPack := range org {
            if model, ok := modelClusters[orgPack.ModelName]; ok {
                modelClusters[orgPack.ModelName] = append(model, orgPack)
            } else {
                modelClusters[orgPack.ModelName] = []models.OrgPack{orgPack}
            }
        }

        customFilter := request.Filters
        customFilter.Cluster = "clusterDay"

        output := make(map[string][]OrgStat)
        for modelName, slice := range modelClusters {
            if len(slice) == 0 {
                continue
            }
            model := slice[0].ModelFactory.New()
            for _, el := range slice {
                model.Add(el.RowFactory.New(el))
            }
            model.ClusterData(customFilter)
            for sourceName, charts := range request.Charts {
                output = model.FormatOutput(output, sourceName, charts)
            }
        }
        return output
    }
Nighttime answered 25/8, 2016 at 18:6 Comment(9)
Your variant looks very very clean and nice to use.. but, in row model := slice[0].ModelFactory.New() what type would have slice[0]. If it implements the interface, than yes, compiler can say all is ok... but there are no type assertions here... where is the magick, that would say from what model to call this ...ModelFactory.New() methods and same for ...RowFactory.New() ?Heteropolar
It would be very efficient conversation if you would make small working example in golang playground with this your concept. Can you ? The main style and idea of your suggession is exactly what I was trying to achieve in the end. There are 2 days left for bounty reward and your answer is the closestHeteropolar
I'm beginning to realize this somewhat hinges on what an OrgPack is. Is that just an interface{} too?Nighttime
It is : OrgPack struct { ModelName string slice []int} if i remeber well.. can't remember exact right now. Can tell you tomorrow morning. I read data sets from channel org. The sets are from 3 to 25-30 int 's and 1 string ModelName with the name of model. So data comming to the chanel has type association to the models type by string name but doesn't belongs to it...Heteropolar
type OrgPack struct { SummorisedData []float64 ModelName string PackName string } and type OrgStat struct { Name string Value float64 }Heteropolar
Oh, nice. So they're concrete types. I got confused by the newItem function you wrote—reflect.valueOf(&el).Elem() seemed like it would just get you a reflect-y wrapper around var el OrgPack, and then sliceObjFields.FieldByName("PackName") would always just get you the same thing as reflect.ValueOf(el.PackName) so I couldn't tell why you needed to use reflection that much in the first place.Nighttime
With this new information I am pretty sure I can get you a working example shortly after I get home, maybe 6-8 hours from now. In case I can't, I'll be very sorry.Nighttime
I would be very be grateful if you will. I'll give a bounty to your answer now, because it is the closest to my "desire" and the time counter of bounty i gues is broken here. But I'll be waiting for your help and examples. If you wouldn't mind, we can move into chat here, or we can chat in some other app like telegram, gitter, slack or any other, but which is more "realtime". I would greatly appreciate that. Also I can provide code of method that generates data into chanel.. That can help to resolve this issue in complex.. but that is harder of courceHeteropolar
Let us continue this discussion in chat.Nighttime
M
2

Maybe this is the stupid way, because I don't know the better way to do it.
Here the sample code you can use (this only draft).

First make new function to convert []interface{} to model :

func GetModel(modelName string, data []interface{}) interface{} {
    switch modelName {
        case "noagg" :
            m := make(NoaggModel, len(data))
            for i, v := range data {
                m[i] = v.(NoaggRow)
            }
            return m
        case .....
        //and case so on
    }
}

And your code "dataModel := data_slice.(model)" replace like below :

dataModel := GetModel(modelName, data_slice)
//now your dataModel is ready to convert to StatModel
if statModel, ok := dataModel.(StatModel); ok {
    statModel.FilterData(?) //just example
}

Maybe this can give you some idea.

Minter answered 22/8, 2016 at 5:32 Comment(0)
N
2

I'm sure you're already aware, but using this many interface{} entities makes the code kind of hard to read and loses a lot of the advantages of working with a type-safe language.

If there's time to slightly re-design things, I'd question the assumption that a channel of interface types is really necessary. Maybe you could use multiple channels instead.

Then, when you would read from your channel(s), you would write:

for !done {
  select {
    case model := <- noaggModelChan:
      doThingsWithNoaggModel(model)
    case model := <- fcrModelChan:
      doThingsWithFcrModel(model)
    case done = <- doneChan:
      continue
  }
}
Nighttime answered 24/8, 2016 at 23:26 Comment(2)
Or you could write a separate handler function (goroutine) per channel, as receiveLightNoaggWork. sync.WaitGroup would help you block until all functions finish, as needed.Nighttime
For logic that is common to all structs that implement (some interface), I would say just write a function that performs that logic, and then call it in each of your N handlers. You're "repeating" code N times, but at least it's not 100*N, and reducing your need for reflection probably saves both lines of code and headache.Nighttime
H
1

I received a lot of support: 2 answers on this Stackoverflow site and many help and support on Slack chat from Gophers.

As I realized from my night talks with more skilled developers, I was trying to create "generics" that are commonly used in OOP languages(as example latest versions of php).

I wanted (and still want) to create one compact method, that would work nicely with any model structure by name of its type. But even with the use of reflection I didn't find a solution, when it is possible not to type exact struct type for making assertion to.

So.. My code became shorter, but I didn't reached the main goal:

"The code of the method should not depend of the amount of model structures, that can appear."

That is my own goals, I'm not making somebodies tasks. Only my own optimization. But the fact of the fail is sad. So my, not final, but last version is this :

func newItem(modelName string, el models.OrgPack) interface{} {

    var item models.StatRow

    switch modelName {
    case "noagg":
        item = &models.NoaggRow{}
    case "fcr24":
        item = &models.FcrRow{}
    case "acsi":
        item = &models.AcsiRow{}
    case "aic":
        item = &models.AicRow{}
    case "aux":
        item = &models.AuxRow{}
    case "cti":
        item = &models.CtiRow{}
    case "srv":
        item = &models.SrvRow{}
    case "sale":
        item = &models.SaleRow{}
    case "pds":
        item = &models.PdsRow{}
    case "wfm":
        item = &models.WfmRow{}
    }

    modelFields := reflect.ValueOf(item).Elem()

    fieldsCounter := modelFields.NumField()

    sliceFields := reflect.ValueOf(&el.SummorisedData).Elem()
    sliceObjFields := reflect.ValueOf(&el).Elem()

    fieldsTypes := modelFields.Type()

    for i := 6; i < fieldsCounter; i++ {
        fmt.Println(" model_field ", fieldsTypes.Field(i).Name)
        modelField := modelFields.Field(i);
        sliceField := sliceFields.Index(i - 6);

        modelField.Set(reflect.Value(sliceField));
    }

    id_line := sliceFields.Index(len(el.SummorisedData) - 1);
    date := sliceObjFields.FieldByName("PackName");

    modelFields.FieldByName("Id_line").Set(id_line)
    modelFields.FieldByName("Date").Set(date)

    return item

}

func formatOutput(output map[string][]OrgStat, sourceName string, modelName string, charts []Chart, mod models.StatRow, cluster string) map[string][]OrgStat {

    if sourceName == modelName {
        var stats []OrgStat
        for _, chart := range charts {
            stats = append(stats, OrgStat{Name:chart.Name, Value: mod.Count(chart.Name)})
        }
        _, group_exist := output[cluster]
        if group_exist {
            inserted_stat := output[cluster]
            output[cluster] = append(stats, inserted_stat...)
        }else {
            output[cluster] = stats
        }

    }
    return output
}

func receiveLightWork(org <-chan models.OrgPack, request ChartOptions) interface{} {

    modelClusters := make(map[string][]models.OrgPack)

    for orgPack := range org {
        _, ok := modelClusters[orgPack.ModelName]
        if ok {
            model := modelClusters[orgPack.ModelName]
            model = append(model, orgPack)
            modelClusters[orgPack.ModelName] = model

        }else {
            var modelSlice []models.OrgPack
            modelSlice = append(modelSlice, orgPack)
            modelClusters[orgPack.ModelName] = modelSlice
        }
    }

    output := make(map[string][]OrgStat)

    for modelName, slice := range modelClusters {

        switch modelName {
        case "noagg":
            model := models.NoaggModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.NoaggRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)


            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "acsi":
            model := models.AcsiModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.AcsiRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "fcr24":
            model := models.FcrModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.FcrRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "aic":
            model := models.AicModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.AicRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "aux":
            model := models.AuxModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.AuxRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "cti":
            model := models.CtiModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.CtiRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "srv":
            model := models.SrvModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.SrvRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "sale":
            model := models.SaleModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.SaleRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "pds":
            model := models.PdsModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.PdsRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        case "wfm":
            model := models.WfmModel{}

            for _, el := range slice {
                newElement := newItem(modelName, el)
                model = append(model, *(newElement.(*models.WfmRow)))
            }
            customFilter := request.Filters
            customFilter.Cluster = "clusterDay"

            model.ClusterData(customFilter)

            for _, mod := range model {
                for sourceName, charts := range request.Charts {
                    output = formatOutput(output, sourceName, modelName, charts, mod, mod.Date)
                }
            }
        }
    }

    return output
}

As you can see, the length of this methods depends directly on the amount of models, and I cannot land this code into the models, because it is made for type casting to create correct structures for later math.

If anyone have any idea, how to reach this last goal(code of receiveLightWork method not depend on models amount), I'll be happy to hear it!

Heteropolar answered 25/8, 2016 at 12:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.