ANisus
ANisus

Reputation: 78055

Dynamically create variables of certain type based on string in Go

Simple version
How can you create a variable of a certain type based upon the value of a string?

type ta struct { a int }
type tb struct { b float }
type tc struct { c string }

t := "tb"
v := MagicVarFunc(t) // Returns a new allocated var of type interface{}
v.(tb).b = 8.3

The true example
In my, surprisingly enough, working example below, I am dynamically creating variables based on a string. This is done by registering each struct type in a map with the string being the key and a nil-pointer of the type being the value.
Each type implements an interface with the method New() which returns a new variable of that specific type.

The example below is very close to what I wish to do, where each action has a set of JSON encoded data which will populate the corresponding struct. The way I've structured it is also because I wish to be able to create new stand alone actions that I register to the map.

I am not sure if am abusing the language now.
May anyone give me any pointers if I am completely out of my mind? Is there an obviously easier way?

package main

import (
    "fmt"
    "encoding/json"
)

// All I require of an action is that it may be executed
type ActionHandler interface {
    Exec()
    New() ActionHandler
}

// My list of actions
var mActions = make(map[string]ActionHandler)

// Action Exit (leaving the program)
type aExit struct {}
func (s *aExit) Exec() { fmt.Println("Good bye") }
func (s *aExit) New() ActionHandler { return new(aExit) }
func init() {
    var a *aExit
    mActions[`exit`] = a
}

// Action Say (say a message to someone)
type aSay struct {
    To  string
    Msg string
}
func (s *aSay) Exec() { fmt.Println(`You say, "` + s.Msg + `" to ` + s.To) }
func (s *aSay) New() ActionHandler { return new(aSay) }
func init() {
    var a *aSay
    mActions[`say`] = a
}

func inHandler(action string, data []byte) {
    a := mActions[action].New()
    json.Unmarshal(data, &a)
    a.Exec()
}

func main(){
    inHandler(`say`, []byte(`{"to":"Sonia","msg":"Please help me!"}`))
    inHandler(`exit`, []byte(`{}`))
}

Upvotes: 3

Views: 7552

Answers (3)

jorelli
jorelli

Reputation: 8368

start by defining a function type that does the thing you want:

type Producer func([]byte) interface{}

make a few of them:

func FooProducer(raw []byte) interface{} {
    foo := new(Foo)
    ... // do something to foo
    return foo
}

func BarProducter(raw []byte) interface{} {
    bar := new(Bar)
    ... // do something to bar
    return bar
}

stick them in a map:

likeThis := map[string]Producer{
    "foo": FooProducer,
    "bar": BarProducer,
}

and then just do one of these:

myVal := likeThis[someString](raw)

but you probably want to define some interface and make your producer something more like:

type Producer func([]byte) MyAwesomeInterface

since there's probably some common stuff you want to do with those things you're decoding. You also probably want to handle the case of a bad string input, like-a-this:

f, ok := likeThis[someString]
if !ok {
    // return, break, panic... something, just get the hell away from here.
}
myVal := f(raw)

The whole notion of inspecting types is kinda cumbersome in Go. It's generally less work to just add new types than it is to try to do reflection gymnastics with the type system.

Upvotes: 3

Sonia
Sonia

Reputation: 28375

jorelli's answer is very good. I'm just going to show a few options. Your "true example" looks essentially like command dispatch, with command parameters specified with JSON. To start with simple code that does this,

package main

import (
    "encoding/json"
    "fmt"
)

func inHandler(action string, data []byte) {
    arg := make(map[string]interface{})
    json.Unmarshal(data, &arg)
    switch action {
    case "say":
        fmt.Printf("You say, %q to %s\n", arg["msg"], arg["to"])
    case "exit":
        fmt.Println("Good bye")
    }
}

func main() {
    inHandler(`say`, []byte(`{"to":"Sonia","msg":"Please help me!"}`))
    inHandler(`exit`, []byte(`{}`))
}

Your register new commands by adding cases to the switch statement. Yeah, didn't think you'd like that. So, adding your map and init() idea,

package main

import (
    "encoding/json"
    "fmt"
)

type jmap map[string]interface{}

var mActions = map[string]func(jmap){}

func init() {
    mActions["say"] = func(arg jmap) {
        fmt.Printf("You say, %q to %s\n", arg["msg"], arg["to"])
    }
}

func init() {
    mActions["exit"] = func(jmap) { fmt.Println("Good bye") }
}

func inHandler(action string, data []byte) {
    args := make(jmap)
    json.Unmarshal(data, &args)
    mActions[action](args)
}

func main() {
    inHandler(`say`, []byte(`{"to":"Sonia","msg":"Please help me!"}`))
    inHandler(`exit`, []byte(`{}`))
}

Now if you wanted, you could put each of those init functions in a separate source file and new commands could be registered by creating a new source file with a new init function.

The rest of the program is simplified with some assumptions that the commands have flat argument lists that the JSON will always encode as an object. This allows you to dispense with separate Go struct definitions for each command. inHandler just creates the same type of object (a map) for all commands, unmarshals into it, and passes it to the command. If you wanted to handle a little more arbitrary JSON, you could unmarshal into an empty interface, and the functions would have to do some extra work to dig out the arguments. If that was too much work and you really wanted to unmarshal directly into a struct, then you arrive near jorelli's solution of making each command function unmarshal its own JSON.

Upvotes: 3

newacct
newacct

Reputation: 122509

You can use reflection to get the zero value of, or to allocate a new value (like new) of a type using reflection, if you can get the Type value at runtime. However, I don't think there is a way to get the Type from a string. You would need to have a value of that type to get the type itself.

I adopted your idea, of using a map. I map the string to the type itself, which you can get using reflect.TypeOf, which gets the type out of an interface value. Then I used reflect.Zero to get the zero value of that type (a convenient value that exists for every type). Then I got the value out as an interface.

package main
import "reflect"

type ta struct { a int }
type tb struct { b float64 }
type tc struct { c string }

var mActions map[string]reflect.Type = make(map[string]reflect.Type)
func init() {
  var a ta
  mActions[`ta`] = reflect.TypeOf(a)
  var b tb
  mActions[`tb`] = reflect.TypeOf(b)
  var c ta
  mActions[`tc`] = reflect.TypeOf(c)
}

func MagicVarFunc(action string) interface{} {
  return reflect.Zero(mActions[action]).Interface()
}

func main() {
  t := "tb"
  v := MagicVarFunc(t) // Returns a new allocated var of type interface{}
  x := v.(tb)
  x.b = 8.3
}

Upvotes: 8

Related Questions