Modifying an interface{}-ed struct in Go via reflection
Despite all my love for Go, there’s one thing that I totally hate – its implementation of generics. I could go into the details of how awesome it would be with just a little bit of type classes (speaking in Haskell’s terms), but Will Yager has already laid it out perfectly in his Why Go Is Not Good article – so go read that one, if you’re interested. But with that put aside, let’s see how we can solve a problem of accepting and modifying a struct of unknown type in Go.
Prerequisites
Go read The Go Blog – The Laws of Reflection. Seriously. Like right now. This page is not going anywhere anyway.
I consider that article a prerequisite to the following discussion, so I assume that you’re familiar with that, to keep things short. And you should be, if you’re try to wring any reflection based code in Go.
Problem
Ok, now to the actual problem. Let’s say we have three different types of structures, like that:
package main
type Jaeger struct {
Name string
Country string
Status string
}
type Kaiju struct {
Alias string
Origin string
Status string
}
type Shatterdome struct {
Location string
}
And let’s say we want to write Destroy
function, that takes any structure, and
if that structure has “Status” field – changes this field to “Destroyed”.
If there’s no “Status” field – nothing happens. So the func should look
like this:
func Destroy(subj interface{}) {
// something goes here
}
We’re dropping the parameter type down to interface{}
, as the lowest common
denominator for all possible structs, and now we need to find a way to
detect the presense of that “Status” field, and to modify its value.
And here is a simple test to verify our solution:
package main
import "testing"
func TestDestroy(t *testing.T) {
// Initialize data
jaeger := Jaeger{Name: "Cherno Alpha", Country: "RU", Status: "Active"}
kaiju := Kaiju{Alias: "Scissure", Origin: "Sydney", Status: "Unknown"}
shatterdome := Shatterdome{Location: "Lima"}
// Destroy everything
Destroy(&jaeger)
Destroy(&kaiju)
Destroy(&shatterdome)
// Check the result
if jaeger.Status != "Destroy" {
t.Error("jaeger was not destroyed")
}
if kaiju.Status != "Destroy" {
t.Error("kaiju was not destroyed")
}
}
Run it to see it failing:
% go test
--- FAIL: TestDestroy (0.00 seconds)
destroy_test.go:18: jaeger was not destroyed
destroy_test.go:21: kaiju was not destroyed
FAIL
exit status 1
Now that we have the problem set up, and the test ready to verify the solution, let’s work on one.
Solution
As it turns out, this is actually fairly simple. The key thing here is that
Go stored the original type of the struct in the memory together with the object
data itself, so even when it is casted to an interface{}
– it doesn’t lose
the information about its original type, we just need to work a bit harder
to get to it.
And the way to get to it is to use reflect package. More specifically, we need to do the following steps to get to the value of the field we need:
- Get the actual struct value behind the
interface{}
, to reflect on. - Get a typed representation of that struct.
- With that type in hand, check the existence of the field and modify it.
Step 1 is required for any reflection – and it is as simple as a call to
reflect.ValueOf(subj)
to retrieve the original Value.
Next, to interrogate the type itself to find out which fields exist in it,
we need to go from a generic Value
, to the one with the right Type
- and it is also as simple as adding .Elem()
call to the value representation.
And from here on we have a full power of reflect
at our disposal. As it is
possible that the field is missing,
Value.IsValid()
call allows to find if this is the case, and if it isn’t -
Value.SetString(string)
to actually change the field.
Combining it all together, this is the final function:
func Destroy(subj interface{}) {
stype := reflect.ValueOf(subj).Elem()
field := stype.FieldByName("Status")
if field.IsValid() {
field.SetString("Destroyed")
}
}
Full solution with an example runner is available on Go playground
Importance of pointers
If you were paying attention earlier, you might have noticed that it my
test I used pointers in my calls to Destroy()
function:
// Destroy everything
Destroy(&jaeger)
Destroy(&kaiju)
Destroy(&shatterdome)
Why is that? Well, if you think of what we’re trying to achieve (modify the original data structure) – it makes sense that it has to be passed in by reference, rather than copied by value, as the latter (even if it worked, which it doesn’t) would result in the original struct being unchanged.
But even more than that – reflect
will actually refuse to modify your data,
if it is not passed in by reference. Consider the following simple example
(playground link):
package main
import "fmt"
import "reflect"
type Jaeger struct {
Status string
}
func Check(subj interface{}) {
stype := reflect.ValueOf(subj)
field := stype.FieldByName("Status")
fmt.Printf("can be set? %+v\n", field.CanSet())
}
func main() {
jaeger := Jaeger{Status: "Active"}
Check(jaeger)
}
If you run it, you’ll get "can be set? false"
. Why is that? Well, exactly
because it was passed in by value as an interface{}
type – and modifying
by-value copy of the original struct doesn’t make much sense.
Summary
Hopefull this clears some confusion around this, as I’ve seen people struggling
with reflect
package in Go, and coming up with some overly complicated
solutions to simple problems.
Do you still have questions, or is there anything wrong in my post? Feel free to ask or correct me in the comments.
Comments