Once Upon a Reflection: Looking Deeper into Golang reflection
I often reflect upon my code…
One of the coolest features of the Golang programming language is the reflect package. As the package documentation states at the onset of the package:
Package reflect implements run-time reflection, allowing a program to manipulate objects with arbitrary types. The typical use is to take a value with static type interface{} and extract its dynamic type information by calling TypeOf, which returns a Type.
On the surface, that all sounds pretty straight-forward. But when you start to think about the types of objects (literally) that you might reflect upon, things can get interesting pretty quickly. It’s one thing to dynamically get the type of an object like an Int
, and then extract it’s value. But what about dynamically getting access to Methods inside a struct
and then calling it with values created dynamically from an a-priori signature, returning values and extracting those values programatically? Rather quickly, the power of reflection can become overwhelming and even confusion. Moreover, appropriate use-cases of reflection should be well thought out so that we aren’t simply
getting too clever for our own good.
I always find it best to motive real uses-cases by first understanding the mechanics of the underlying constructs. So before we motivate a real-life use-case, we need to see how the reflect
package works on different objects. I’m going to assume that you have a basic understanding of reflection in general and jump right in to manipulating methods defined inside struct
.
Let’s start by defining a pathologically simple type
definition. Recall that we must first declare a type
in order to add methods.
type T struct{}
The most simple method we can define will take no arguments, and return no values:
func(t *T)foo() {
// be forever foo'ish
}
Now, let’s see reflection allows us to manipulate type T
to access, call, and return values. A simple main()
function will demonstrate some basics of this approach.
func main() {
var t T
reflect.ValueOf(&t).MethodByName("foo").Call([]reflect.Value{})
}
That’s it! We use the reflect.ValueOf()
to get a reflect.Value
of our T-typed
variable. With that Value
, we call the reflect MethodByName
where we pass in a string value, dynamically for example if we received it at runtime, and receive the method function associated with the string name. Next we call it passing an empty array of reflect.Value{}
s.
Running the code does nothing as we didn’t provide anything (aside from a snarky comment) in the function body of foo()
.
Let’s make things more interesting. Suppose we want to define an arbitrary number of methods and dynamically call them at runtime (if they exist in the struct definition), passing in arguments as defined by the method, and returning values from the call.
Although it’s not more more complicated, an example further illustrates how we can do it. This time, we’ll define type with a method that takes a value and returns values.
type T struct{}
func (t *T) Foo(i int, s string) (int, *T) {
fmt.Printf("T Foo(%d, %s)\n", i, s)
return 42, nil
}
How does the reflection look to access this method and it’s return values? Finding our method is similar to the code above. However, we need to do a little maintenance before we can call our method. We need to build an array of reflect.Value
s to pass to our call. So, let’s build a helper-function that does it for us. Here we define a function that is variadic, meaning that it takes an arbitrary number of parameters, args
, along with a type, any
and the name
of the method that we’ll invoke. Using the len(args)
, we can make an array of that length and call reflect.ValueOf
on each arg, placing that value into our calling inputs
array.
func Invoke(any interface{}, name string, args ...interface{}) []reflect.Value {
inputs := make([]reflect.Value, len(args))
for i, _ := range args {
inputs[i] = reflect.ValueOf(args[i])
}
return reflect.ValueOf(any).MethodByName(name).Call(inputs)
}
Also, notice that we have defined our helper-function to return an array of reflect.Value
. When we pass our inputs
to the Value.Call()
method, we will return an array of reflect.Value
from Call()
. We can assign the value returned from Invoke()
to a variable, and see what our method returned. As golang is a language that can return multiple values, we need to inspect the array of returns values by ranging over it. Again, another helper-function might be of value here for illustration.
func inspect(v []reflect.Value) {
for i, _ := range v {
fmt.Printf("v[%d] => %q, ", i, v[i].Kind())
switch v[i].Kind() {
case reflect.Int:
fmt.Printf("%d", v[i].Int())
case reflect.Interface:
fmt.Printf("%v", v[i].Interface())
case reflect.Invalid, reflect.Bool,
reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64, reflect.Uint, reflect.Uint8,
reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr, reflect.Float32, reflect.Float64,
reflect.Complex64, reflect.Complex128, reflect.Array,
reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr,
reflect.Slice, reflect.String, reflect.Struct,
reflect.UnsafePointer:
}
fmt.Println()
}
}
Here, I’ve defined inspect(v []reflect.Value)
. We pass in our array of return values and switch
over the array by inspecting the Kind()
of value that we returned in each element of the array. In order to access the underlying value for each kind, we call the respective function that extracts the typed Value. For example, if the return value Kind()
was an Int
, we would call the Value.Int()
method to get the actual integer value.
So how would we call this in a main()
function?
...
retVal := Invoke(&T{}, "Foo", 10, "abc")
inspect(retVal)
...
We can go one step further. Why not turn the Invoke
function into a method on a struct. Then we can create a non-method function that we’ll use inside our method function to call any method by name within our struct.
package main
type Thing struct{
}
func (tp *Thing) Goodbye (greeting string) {
fmt.Printf("adios %s\n", greeting)
}
func (tp *Thing) Hello(answer int) string {
fmt.Printf("Hi There!\n")
return "rob"
}
func (tp *Thing) Invoke(name string, args ...interface{}) []reflect.Value {
return invoke(tp, name, args...)
}
func inspect(v []reflect.Value) {
for i, _ := range v {
fmt.Printf("v[%d] => %q, ", i, v[i].Kind())
switch v[i].Kind() {
case reflect.Int:
fmt.Printf("%d", v[i].Int())
case reflect.Interface:
fmt.Printf("%v", v[i].Interface())
case reflect.Invalid, reflect.Bool,
reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64, reflect.Uint, reflect.Uint8,
reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr, reflect.Float32, reflect.Float64,
reflect.Complex64, reflect.Complex128, reflect.Array,
reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr,
reflect.Slice, reflect.String, reflect.Struct,
reflect.UnsafePointer:
}
fmt.Println()
}
}
func invoke(any interface{}, name string, args ...interface{}) []reflect.Value {
if len(args) > 0 {
fmt.Printf("\tthere are %d args\n", len(args))
inputs := make([]reflect.Value, len(args))
for i, _ := range args {
inputs[i] = reflect.ValueOf(args[i])
}
return reflect.ValueOf(any).MethodByName(name).Call(inputs)
} else {
return reflect.ValueOf(any).MethodByName(name).Call([]reflect.Value{})
}
}
func main() {
var a Thing
inspect(a.Invoke("Hello", 42))
a.Invoke("Goodbye", "Rob")
}
Don’t Panic!
So what happens if we attempt to call a method that
- doesn’t exist
- has the wrong calling parameters
Give it a try. Add another parameter to a.Invoke("Goodbye", "Rob", "DEAD")
.
panic: reflect: Call with too many input arguments
goroutine 1 [running]:
reflect.Value.call(0x10d0380, 0x1194dc0, 0x213, 0x10e87c7, 0x4, 0xc420078390, 0x2, 0x2, 0xc42000c098, 0x13, ...)
/usr/local/Cellar/go/1.9.2/libexec/src/reflect/value.go:361 +0x1357
reflect.Value.Call(0x10d0380, 0x1194dc0, 0x213, 0xc420078390, 0x2, 0x2, 0x1194dc0, 0x213, 0x0)
/usr/local/Cellar/go/1.9.2/libexec/src/reflect/value.go:302 +0xa4
main.invoke(0x10d0380, 0x1194dc0, 0x10e8be4, 0x7, 0xc420045f50, 0x2, 0x2, 0x0, 0x0, 0x0)
/Users/robert/src/go/nillish/tst4.go:54 +0x285
main.(*Thing).Invoke(0x1194dc0, 0x10e8be4, 0x7, 0xc420045f50, 0x2, 0x2, 0x0, 0x0, 0x0)
/Users/robert/src/go/nillish/tst4.go:22 +0x73
main.main()
/Users/robert/src/go/nillish/tst4.go:74 +0x1d0
So that isn’t too friendly, but we can actually use defer()/recover()
to make this experience less painful. By adding a deferred recovery function, we can recover when we fail to call the appropriate method, thereby inhibiting the program from panicing.
defer func() {
if r := recover(); r != nil {
fmt.Printf("failed to find method: %s\n", r)
}
}()
By adding this deferred function, we’ll recover from the panic and we could actually determine that the panic was caused by the failed Value.Call()
. We’d capture the value:
failed to find method: reflect: Call with too many input arguments
Upon Final Reflection
Using the reflect
package along with defer/panic
idioms can be very powerful tools. We’ve successfully called two separate methods using our Invoke()
method, and caught a panic when we failed to call a third method with invalid arguments.
How might you use these techniques dynamically? Perhaps we could specify a json-file that describes objects, their methods, and their calling and returning parameters…
… The possibilities are literally endless! Go get it!