Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting unknown interface to float64 in Golang

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?

like image 649
Rhaokiel Avatar asked Dec 24 '13 23:12

Rhaokiel


2 Answers

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
*/
like image 52
twotwotwo Avatar answered Nov 09 '22 07:11

twotwotwo


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

like image 13
Dave C Avatar answered Nov 09 '22 09:11

Dave C