Converting unknown interface to float64 in Golang
Asked Answered
O

3

19

So I am receiving an interface{}, but I want to in any way possible convert it to a float64 or return an error if not possible.

Here's what I'm doing:

func getFloat(unk interface{}) (float64, error) {
    if v_flt, ok := unk.(float64); ok {
        return v_flt, nil
    } else if v_int, ok := unk.(int); ok {
        return float64(v_int), nil
    } else if v_int, ok := unk.(int16); ok {
        return float64(v_int), nil
    } else ... // other integer types
    } else if v_str, ok := unk.(string); ok {
        v_flt, err := strconv.ParseFloat(v_str, 64)
        if err == nil {
            return v_flt, nil
        }
        return math.NaN(), err
    } else if unk == nil {
        return math.NaN(), errors.New("getFloat: unknown value is nil")
    } else {
        return math.NaN(), errors.New("getFloat: unknown value is of incompatible type")
    }
}

But I feel like I'm going about it the wrong way, is there a better way to do this?

Oecology answered 24/12, 2013 at 23:23 Comment(0)
C
29

Dave C has a good answer using reflect, and I'll compare that to type-by-type code below. First, to do what you were already doing more concisely, you can use the type switch:

    switch i := unk.(type) {
    case float64:
            return i, nil
    case float32:
            return float64(i), nil
    case int64:
            return float64(i), nil
    // ...other cases...
    default:
            return math.NaN(), errors.New("getFloat: unknown value is of incompatible type")
    }

The case float64: is like your if i, ok := unk.(float64); ok { ... }. Code for that case can access the float64 as i. Despite the lack of braces the cases act like blocks: i's type is different under each case and there is no C-style fallthrough.

Also, note large int64s (over 253) will be rounded when converted to float64, so if you're thinking of float64 as a "universal" number type, take its limitations into account.

An example of that is in the Playground at http://play.golang.org/p/EVmv2ibI_j.


Dave C mentions you can avoid writing out individual cases if you use reflect; his answer has code, and even handles named types and pointers to suitable types. He also mentions handling strings and types convertible to them. After doing a naïve test comparing options:

  • Passing the reflect version an int gets me about 13 million conversions a second; the overhead isn't noticeable unless you're converting millions of items.
  • You can write a switch to handle some common types then fall back to reflect; at least in my simple test below it goes at ~50M conversion/s and allocates less, presumably only the interface{} value without a reflect.Value.
  • A switch only over number types loses some flexibility, but can avoid allocation since the compiler can prove through escape analysis that nothing needs to remain allocated after.

That said, if you need to tune enough that you care about these differences, you should probably run your own test in the context of your code. For example, the allocations can have varying costs depending on your app's total live data size, GC settings like GOGC, and how long each collection takes, and your code might allow/prevent different optimizations (inlining, etc.) than my sample.

The code is on the Playground and below:

package main

/* To actually run the timings, you need to run this from your machine, not the Playground */

import (
    "errors"
    "fmt"
    "math"
    "reflect"
    "runtime"
    "strconv"
    "time"
)

var floatType = reflect.TypeOf(float64(0))
var stringType = reflect.TypeOf("")

func getFloat(unk interface{}) (float64, error) {
    switch i := unk.(type) {
    case float64:
        return i, nil
    case float32:
        return float64(i), nil
    case int64:
        return float64(i), nil
    case int32:
        return float64(i), nil
    case int:
        return float64(i), nil
    case uint64:
        return float64(i), nil
    case uint32:
        return float64(i), nil
    case uint:
        return float64(i), nil
    case string:
        return strconv.ParseFloat(i, 64)
    default:
        v := reflect.ValueOf(unk)
        v = reflect.Indirect(v)
        if v.Type().ConvertibleTo(floatType) {
            fv := v.Convert(floatType)
            return fv.Float(), nil
        } else if v.Type().ConvertibleTo(stringType) {
            sv := v.Convert(stringType)
            s := sv.String()
            return strconv.ParseFloat(s, 64)
        } else {
            return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type())
        }
    }
}

func getFloatReflectOnly(unk interface{}) (float64, error) {
    v := reflect.ValueOf(unk)
    v = reflect.Indirect(v)
    if !v.Type().ConvertibleTo(floatType) {
        return math.NaN(), fmt.Errorf("cannot convert %v to float64", v.Type())
    }
    fv := v.Convert(floatType)
    return fv.Float(), nil
}

var errUnexpectedType = errors.New("Non-numeric type could not be converted to float")

func getFloatSwitchOnly(unk interface{}) (float64, error) {
    switch i := unk.(type) {
    case float64:
        return i, nil
    case float32:
        return float64(i), nil
    case int64:
        return float64(i), nil
    case int32:
        return float64(i), nil
    case int:
        return float64(i), nil
    case uint64:
        return float64(i), nil
    case uint32:
        return float64(i), nil
    case uint:
        return float64(i), nil
    default:
        return math.NaN(), errUnexpectedType
    }
}

func main() {
    var m1, m2 runtime.MemStats

    runtime.ReadMemStats(&m1)
    start := time.Now()
    for i := 0; i < 1e6; i++ {
        getFloatReflectOnly(i)
    }
    fmt.Println("Reflect-only, 1e6 runs:")
    fmt.Println("Wall time:", time.Now().Sub(start))
    runtime.ReadMemStats(&m2)
    fmt.Println("Bytes allocated:", m2.TotalAlloc-m1.TotalAlloc)

    runtime.ReadMemStats(&m1)
    start = time.Now()
    for i := 0; i < 1e6; i++ {
        getFloat(i)
    }
    fmt.Println("\nReflect-and-switch, 1e6 runs:")
    fmt.Println("Wall time:", time.Since(start))
    runtime.ReadMemStats(&m2)
    fmt.Println("Bytes allocated:", m2.TotalAlloc-m1.TotalAlloc)

    runtime.ReadMemStats(&m1)
    start = time.Now()
    for i := 0; i < 1e6; i++ {
        getFloatSwitchOnly(i)
    }
    fmt.Println("\nSwitch only, 1e6 runs:")
    fmt.Println("Wall time:", time.Since(start))
    runtime.ReadMemStats(&m2)
    fmt.Println("Bytes allocated:", m2.TotalAlloc-m1.TotalAlloc)
}

/*
Reflect-only, 1e6 runs:
Wall time: 79.853582ms
Bytes allocated: 16002696

Reflect-and-switch, 1e6 runs:
Wall time: 20.921548ms
Bytes allocated: 8000776

Switch only, 1e6 runs:
Wall time: 3.766178ms
Bytes allocated: 32
*/
Cerous answered 24/12, 2013 at 23:49 Comment(5)
Thanks, I was not aware that you could switch with typesOecology
Shame one cannot just have a single case for the int variants case int, int32, int64: return float64(t)Hornwort
Different types would potentially compile to different machine code, so I can see why they did it this way.Cerous
Note that not only does this get long and annoying to ennumerate all the built in number like types, this also fails for even the most simple custom types such as type myFloat float64. You should use reflect's ConvertibleTo, (possibly with an additional attempt to parse anything convertable-to a string).Delouse
@DaveC Updated the answer. Reflection costs enough in relative terms I figure it's still worth describing the options, but I tried to be clear it still won't be noticeable unless you do millions of calls.Cerous
D
13

You can use the reflect package for this:

import "reflect"

var floatType = reflect.TypeOf(float64(0))

func getFloat(unk interface{}) (float64, error) {
    v := reflect.ValueOf(unk)
    v = reflect.Indirect(v)
    if !v.Type().ConvertibleTo(floatType) {
        return 0, fmt.Errorf("cannot convert %v to float64", v.Type())
    }
    fv := v.Convert(floatType)
    return fv.Float(), nil
}

Runnable in the Go Playground: http://play.golang.org/p/FRM21HRq4o

Delouse answered 4/8, 2014 at 18:5 Comment(0)
F
1

i am using from this package

Cast is a library to convert between different go types in a consistent and easy way. Cast provides simple functions to easily convert a number to a string, an interface into a bool, etc. Cast does this intelligently when an obvious conversion is possible. It doesn’t make any attempts to guess what you meant, for example you can only convert a string to an int when it is a string representation of an int such as “8”. Cast was developed for use in Hugo, a website engine which uses YAML, TOML or JSON for meta data

https://github.com/spf13/cast

Fourwheeler answered 6/2, 2023 at 6:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.