Skip to content

Instantly share code, notes, and snippets.

@antigremlin
Last active February 11, 2020 12:12
Show Gist options
  • Save antigremlin/7acee4d7017349ec70b1e7b596996504 to your computer and use it in GitHub Desktop.
Save antigremlin/7acee4d7017349ec70b1e7b596996504 to your computer and use it in GitHub Desktop.
Go value dereferencing by string path
package spike
import (
"errors"
"reflect"
"strconv"
)
var stringType = reflect.TypeOf("")
func Deref(dst interface{}, src interface{}, keys []string) (found bool, err error) {
defer func() {
// Having a recursive algorithm with exceptions is much easier than
// maintaining the error return values. I'm not much in love
// with the manual error monad Go imposes on us.
if msg := recover(); msg != nil {
err = errors.New(msg.(string))
}
}()
result := getValue(reflect.ValueOf(src), keys[0], keys[1:])
if !result.IsValid() {
return false, nil
}
dest := reflect.ValueOf(dst)
if dest.Kind() == reflect.Ptr {
dval := dest.Elem()
// TODO also consider dereferencing the value we've found
dval.Set(result.Convert(dval.Type()))
} else {
return false, errors.New("dst must be a pointer")
}
return true, nil
}
func getValue(v reflect.Value, key string, rest []string) reflect.Value {
next := index(v, key)
if !next.IsValid() || len(rest) == 0 {
return next
}
return getValue(next, rest[0], rest[1:])
}
func index(v reflect.Value, key string) reflect.Value {
switch v.Kind() {
case reflect.Ptr:
return index(v.Elem(), key) // invalid if nil
case reflect.Slice:
idx, err := strconv.Atoi(key)
if err != nil {
panic("slice index: " + err.Error())
}
// TODO check index out of range?
return v.Index(idx) // panics if out of range
case reflect.Map:
if v.Type().Key() != reflect.TypeOf("") {
panic("deref.index: map key should be string")
}
return v.MapIndex(reflect.ValueOf(key)) // invalid if not found
case reflect.Struct:
// TODO use the cache of names to field indices, built from structu tags, etc.
return v.FieldByName(key) // invalid if not found
}
panic("can't index: " + v.Kind().String())
}
package spike
import "testing"
func TestSlice0(t *testing.T) {
src := []int{1, 2, 3}
var dst float64
found, err := Deref(&dst, src, []string{"0"})
if found {
t.Log("Returned:", dst)
} else {
t.Error(err)
}
}
func TestMap(t *testing.T) {
src := map[string]int{"foo": 1, "bar": 2}
// src := map[int]int{1: 1, 2: 2}
var dst int
found, err := Deref(&dst, src, []string{"foo"})
if found {
t.Log("Returned:", dst)
} else {
t.Error(err)
}
}
func TestMapStr(t *testing.T) {
src := map[string]string{"foo": "one", "bar": "two"}
var dst []byte
found, err := Deref(&dst, src, []string{"foo"})
if found {
t.Log("Returned:", dst)
} else {
t.Error(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment