Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to convert a string value to the correct reflect.Kind in go?

Is there a generic helper method in Go to convert a string to the correct value based on reflect.Kind?

Or do I need to implement the switch over all kinds myself?

I have a value like "143" as a string and a reflect.Value with kind "UInt16" and like to convert that string value and set it into the UInt16 value of my struct.

My current code looks like:

func setValueFromString(v reflect.Value, strVal string) error {
    switch v.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        val, err := strconv.ParseInt(strVal, 0, 64)
        if err != nil {
            return err
        }
        if v.OverflowInt(val) {
            return errors.New("Int value too big: " + strVal)
        }
        v.SetInt(val)
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
        val, err := strconv.ParseUint(strVal, 0, 64)
        if err != nil {
            return err
        }
        if v.OverflowUint(val) {
            return errors.New("UInt value too big: " + strVal)
        }
        v.SetUint(val)
    case reflect.Float32:
        val, err := strconv.ParseFloat(strVal, 32)
        if err != nil {
            return err
        }
        v.SetFloat(val)
    case reflect.Float64:
        val, err := strconv.ParseFloat(strVal, 64)
        if err != nil {
            return err
        }
        v.SetFloat(val)
    case reflect.String:
        v.SetString(strVal)
    case reflect.Bool:
        val, err := strconv.ParseBool(strVal)
        if err != nil {
            return err
        }
        v.SetBool(val)
    default:
        return errors.New("Unsupported kind: " + v.Kind().String())
    }
    return nil
}

This works already, but I wonder if this is already implemented somewhere else.

like image 687
Tarion Avatar asked Oct 06 '16 08:10

Tarion


1 Answers

Edit: Answer to the original question ("how to obtain a reflect.Kind from its string representation") is at the end. Answer to your edited question follows:


What you're doing is the fastest and "safest". If you don't want to hassle with that big switch, you may take advantage of e.g. the json package which already contains this switch to decode values from JSON string (in encoding/json/decode.go, unexported function literalStore()).

Your decoding function could look like this:

func Set(v interface{}, s string) error {
    return json.Unmarshal([]byte(s), v)
}

A simple call to json.Unmarshal(). Using / testing it:

{
    var v int
    err := Set(&v, "1")
    fmt.Println(v, err)
}
{
    var v int
    err := Set(&v, "d")
    fmt.Println(v, err)
}
{
    var v uint32
    err := Set(&v, "3")
    fmt.Println(v, err)
}
{
    var v bool
    err := Set(&v, "true")
    fmt.Println(v, err)
}
{
    var v float32
    err := Set(&v, `5.1`)
    fmt.Println(v, err)
}
{
    var v string
    err := Set(&v, strconv.Quote("abc"))
    fmt.Println(v, err)
}

One thing to note: when you want to pass a string, that must be quoted, e.g. with strconv.Quote(). Output (try it on the Go Playground):

1 <nil>
0 invalid character 'd' looking for beginning of value
3 <nil>
true <nil>
5.1 <nil>
abc <nil>

If you don't want to require quoted strings (which just complicates things), you may build it into the Set() function:

func Set(v interface{}, s string) error {
    if t := reflect.TypeOf(v); t.Kind() == reflect.Ptr &&
        t.Elem().Kind() == reflect.String {
        s = strconv.Quote(s)
    }
    return json.Unmarshal([]byte(s), v)
}

And then you may call it with the address of a string variable and a string value unquoted:

var v string
err := Set(&v, "abc")
fmt.Println(v, err)

Try this variant on the Go Playground.


Answer to the original question: how to obtain a reflect.Kind from its string representation:

Declaration of reflect.Kind:

type Kind uint

The different values of reflect.Kinds are constants:

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    // ...
    Struct
    UnsafePointer
)

And the reflect package provides only a single method for the reflect.Kind() type:

func (k Kind) String() string

So as it stands, you cannot obtain a reflect.Kind from its string representation (only the reverse direction is possible by using the Kind.String() method). But it's not that hard to provide this functionality.

What we'll do is we build a map from all the kinds:

var strKindMap = map[string]reflect.Kind{}

We init it like this:

func init() {
    for k := reflect.Invalid; k <= reflect.UnsafePointer; k++ {
        strKindMap[k.String()] = k
    }
}

This is possible and correct because constants are initialized using iota which evaluates to successive untyped integer constants, and the first value is reflect.Invalid and the last is reflect.UnsafePointer.

And now you can obtain reflect.Kind from its string representation by simply indexing this map. A helper function which does that:

func strToKind(s string) reflect.Kind {
    k, ok := strKindMap[s]
    if !ok {
        return reflect.Invalid
    }
    return k
}

And we're done. Testing / using it:

fmt.Printf("All: %#v\n", strKindMap)

for _, v := range []string{"Hey", "uint8", "ptr", "func", "chan", "interface"} {
    fmt.Printf("String: %q, Kind: %v (%#v)\n", v, strToKind(v), strToKind(v))
}

Output (try it on the Go Playground):

All: map[string]reflect.Kind{"int64":0x6, "uint8":0x8, "uint64":0xb, "slice":0x17, "uintptr":0xc, "int8":0x3, "array":0x11, "interface":0x14, "unsafe.Pointer":0x1a, "complex64":0xf, "complex128":0x10, "int":0x2, "uint":0x7, "int16":0x4, "uint16":0x9, "map":0x15, "bool":0x1, "int32":0x5, "ptr":0x16, "string":0x18, "func":0x13, "struct":0x19, "invalid":0x0, "uint32":0xa, "float32":0xd, "float64":0xe, "chan":0x12}
String: "Hey", Kind: invalid (0x0)
String: "uint8", Kind: uint8 (0x8)
String: "ptr", Kind: ptr (0x16)
String: "func", Kind: func (0x13)
String: "chan", Kind: chan (0x12)
String: "interface", Kind: interface (0x14)
like image 194
icza Avatar answered Jan 04 '23 22:01

icza