[SOLVED] Map[string]interface{} - Best Practice

I’m hoping someone might have some input on trying to bring me app in line with Go Best Practice.

I’m creating an application which listens on the end of a rabbit message queue. The upstream is a legacy system used by other apps - changing the message format is a no-go. Messages coming off the queue are (as most of you will know) strings. These strings can be unmarshalled to the following struct (which I’ve simplified somewhat here).

type Message struct {
	Subject string `json:"subject"`
	TimeSent int64  `json:"sent"`
	Body     map[string]interface{}  `json:"body"`
	Status   string `json:"status"`
}

My issue lies in with the ‘Body’ Field and the use of the interface{} in the value. I keep my Go Proverbs (http://go-proverbs.github.io/) in mind when coding and I’m not happy that I’m ignoring the proverb ‘interface{} says nothing’ ( https://youtu.be/PAAkCSZUG1c?t=455)

The Body map has a string key and the value can be type:

  • string
  • float64
  • bool
    -[]map[string]interface{} - this represents json - I don’t need to know the internals so happy leaving this value as interface{}

I find myself creating func(body map[string]interface{}) when I need to do any transformation on the message body and I really dislike it. I also have to do things on a few of the fields (e.g. print them) and which requires different behaviours with different fields (remove quotes in the json field, print 't’or ‘f’ with the bool fields) . This results in type checking, casting and unclean code.

I’d prefer to have an interface which encompasses all the types received. I’ve toyed with creating:

type Datum interface {
	ToString() string
}

and creating some custom types e.g:

type StringDatum string
func (sd StringDatum) ToString() string {...}

type FloatDatum float64
func (fd FloatDatum ) ToString() string {...}

type CSVDatum []map[string]interface{}
func (csvd CSVDatum ) ToString() string  {...}
...
...

Meaning I can constrain my functions somewhat func(body map[string]Datum). But that feels wrong too - I feel I’m abstracting away from basic types…

Can anyone offer any advice on best practice here? Are there any patterns I could consider that might assist in with making this code better? Or is this a case where I ‘actually need’ interface{}?

1 Like

Do you need to process all the fields of the Body? Can you know from something else (e.g., the Subject) what to expect in the Body? If so, maybe you can leave it as json.RawMessage, look at the Subject, unmarshal again into a suitable struct type depending on the Subject, and go from there.

1 Like

Thanks for the reply.

It’a a good idea and one I considered.

I can infer a structure from the subject however there’s >200 subjects/message types that are passed and transformed through the app. The app is a little lightweight transformer that sits between a couple of larger applications and allows the output of one application to be used in a number of downstream app or saves to file system. There are a restricted set of field ids mapping to a type across all these message types (< 10) so it seemed better (initially at least) not to template each message but to put in a map[string]interface{} and use the ID to infer type. It’s a bad output from a legacy system we’re using go to help address.

I don’t need to process all the fields currently - only really the CSV and bool fields (used for conditional routing) however I’d like to build a solution that was extensible if the requirements change (e.g we may have to transform the string field to remove backticks at some point in the future).

https://play.golang.org/p/Yu4RBlueMr
or:
https://play.golang.org/p/7WGLj4OGRK

2 Likes

Thanks Charles,

That’s an interesting approach. Body isn’t a single type but a map of types. I’ve modified to be a halfway between your suggestion and mine:
https://play.golang.org/p/uPF5iLoFlW

This was quick implementation so there’s likely a formatting/naming issue or two in there.

I’m going to look further into this approach. It would allow me add modification code to the types which I like.

https://play.golang.org/p/7WGLj4OGRK

Thanks Charles,

Unless I’m misunderstanding the code that has the same issue as the last suggestion. I need the concrete types inside the Body map but that still assumes the Body is a map[string]interface{} which doesn’t address my initial issue.

Adding your suggestion to mine (custom types and overarching interface) will address this though - I posted an example in the earlier message. Thanks for the input - much appreciated

To be a little clearer - I need custom behaviour on the different types (e.g. ToString as described in original question). This is why the multiple types would work better than one MyType struct.

Hope that makes sense :smiley:

I think this is what you want: https://play.golang.org/p/yquZwu3XKm

That implementation reverts back to a map[string]interface{} which may seem preferable in the examples we’ve looked at here in isolation but would result in the spread of func(interface) around my code which I’d prefer to avoid.

Having bodymap as map[string]Datum constrains what I can put in the map and means I’m creating related funcs with the signature func(Datum). The custom types would have a shared API which is good for me too.

I think the one I posted based on your input is a better fit and addresses the problems I raised in the initial question.
Unless I’m misunderstanding something…

OK. I fixed your version … you were getting lots of noise in your map: https://play.golang.org/p/mTzBFUTuvc

Hi Charles,

I looked but didn’t see many changes/ ‘map noise’ fix. Just a hangover from an older post
http://www.mergely.com/iKyw33fF/

Perhaps it was the wrong link. Doesn’t matter - all solved. Thanks for the help.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.