Reflections in Go, for Cats

Shannon Wells ·

Several months ago, I had a story requiring metaprogramming in golang. I wasn’t very familiar with reflections in Go, and the available docs and write-ups aren’t the best for me, since I’m a learn-by-example kind of person. Having come to Go (ha) as a Rubyist, the lack of generics left a little bit of a hole in my heart, but a stiff upper lip, I pressed on and learned about reflections. Since I had to write my own examples anyway, it made sense to clean this up and turn it into a blog.

My simplified use case

  1. We want to be able to deserialize and validate registered types from []byte with reflect.Type as parameters, but only if previously registered.
  2. Our interface Pet uses the Adopt([]byte)function for deserialization. We’ll need to initialize the incoming type and call Adopt, passing in the data.
  3. Last, consumers of our module should be able to register a reflect.Type and a validator (Veterinarian.Examine in the code example) for this type, with a Registrar. The type is expected to implement the Pet interface.

The Pet interface:

Pet is a domesticated animal companion for humans. It can be “adopted” via Adopt.

type Pet interface {
    Adopt([]byte) error
    IsVaccinated() bool
    IsHealthy() bool
    PetName() string
    Type() string
    Vaccinate()
}

Cat implements Pet:

type Cat struct {
	Name string `json:"name"`
	Healthy bool
	Vaccinated bool `json:vaccinated`
}
    func (f *Cat) Adopt(data []byte) error { return json.Unmarshal(data, f) }
    func (f *Cat) IsVaccinated() bool { return f.Vaccinated } 
    func (f *Cat) IsHealthy() bool { return f.Healthy }
    func (f *Cat) Adopt(data []byte) error { return json.Unmarshal(data, f) }
    func (f *Cat) PetName() string { return f.Name }
    func (f *Cat) Sound() string { return "meow!" }
    func (f *Cat) Type() string { return "Felis Catus" }
    func (f *Cat) Vaccinate() { f.Vaccinated = true }

We can ensure Cat implements the Pet interface. If it doesn’t, this doesn’t compile:

var _ Pet = &Cat{}

Our Simple Veterinarian

type Veterinarian struct {
	vtype   reflect.Type
	Examine func(Pet) bool
}

Our Simple Registrar

type Registrar struct {
	RegisteredPets map[string]Veterinarian
}

The rest of this is in the linked Gist below. I have annotated in detail the test code and implementations. If you download and run the code, particularly if you step through with a debugger, you can examine the test assertions. You can also add your own assertions to explore the answers to your own questions.

In the first set of tests, we examine the basics of reflection by playing around with initializing a Cat struct just by knowing its type, and then calling both Pet and Cat functions on it. The test assertions demonstrate the explanations.

In the next set of tests, we apply this knowledge to our deserialization task.

The final set of tests are to test the implementation of Veterinarian and Registrar. These are left for you to make pass, if you like.


package reflect_cats_test
import (
"encoding/json"
"net/url"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Blog post here: https://blog.carbonfive.com/2020/04/07/reflections-in-go-for-cats/
// As stated in my blog post, to make this more palatable,
// I've contrived a slightly different set of requirements from
// what I had to implement.
// First, spend a few minutes reading over the implementations and
// interface at the bottom of this gist.
// We will receive bytes that we expect to convert to a Pet
//(see implementations at the bottom of this file), but we don't know in
// advance what Type of Pet it is. We need to make an empty <someType>
//struct and call Pet.Adopt() on it.
//
// The deserialization function signature is:
// `func(t reflect.Type, data []byte) Pet`
//
func TestHowToUseReflections(t *testing.T) {
// First we'll look at reflections generally. Say we have an initialized Cat:
cat := Cat{}
// Let's look at what TypeOf gives us. This is the parameter
// type we must use in our deserialization function.
catPtrType := reflect.TypeOf(&cat) // returns a reflect.Type
assert.Equal(t, "*reflect_cats_test.Cat", catPtrType.String())
// Why aren't we looking at reflect.TypeOf(cat)?
// Look at the methods of Cat which fulfill the Pet interface:
// func (f *Cat) IsVaccinated() bool
// These functions take pointer receivers. We must call the Pet
// interface functions on a pointer to a Cat, and the Registrar
// expects pointer types.
// You could, if you want, not use pointer receivers, but this
// discussion could be its own blog post. Experiment with it
// in a separate test of your own.
// Continuing, if we want to instantiate a struct via its type, we should
// use reflect.New somehow. godoc says, "New returns a Value
// representing a pointer to a new zero value for the specified type."
// That is, the returned Value's Type is PtrTo(typ)
aCatPtrTypeVal := reflect.New(catPtrType)
// We now have a Value that is a pointer to a pointer. Illustrative,
// but not very useful; it's going the opposite direction of
// what we want, which is an empty struct.
assert.Equal(t, "<**reflect_cats_test.Cat Value>", aCatPtrTypeVal.String())
// Let's get back to the thing we want to initialize — a Cat.
// For pointers, Elem() and Indirect() return the same thing:
aCatPtrVal := reflect.Indirect(aCatPtrTypeVal) // this should be a *Cat
assert.Equal(t, aCatPtrVal, aCatPtrTypeVal.Elem()) // yes, it is a *Cat
// We now have a reflect.Value containing a pointer to a Cat:
assert.Equal(t, "<*reflect_cats_test.Cat Value>", aCatPtrVal.String())
// reflect.New(thingType) creates a reflect.Value, containing a pointer to
// the zero-value version of a thing of Type thingType.
//
// If thingType is a Kind of pointer, it creates a real pointer to nil, i.e.,
// &(nil)
// NOT a real pointer to a Zero value of what typeThing points to, i.e.,
// NOT &(&thing{})
assert.False(t, aCatPtrTypeVal.IsNil()) // it's a non-nil address
assert.True(t, aCatPtrTypeVal.Elem().IsNil()) // that points to a nil address for a Cat
// If what you want is &(&thing{}), you must call Set, using reflect.New on
// catPtrType.Elem(), where catPtrType.Elem points to a type
// that the pointer points to.
// Here we set aCatPtrVal to the Value of a pointer to an
// empty/zero-value Cat.
// To get the empty Cat struct from this, we call Value.Elem,
// which gives us the child Value that the pointer contains.
// So, catPtrType is a TypeOf *Cat, and catPtrType.Elem() gives us a TypeOf Cat
catType := catPtrType.Elem()
// New initializes an empty struct. Note reflect.New returns a pointer
// to a — Value — of the provided type.
aCatPtrVal2 := reflect.New(catType)
// You can do the same by calling Set on an existing pointer:
aCatPtrVal.Set(reflect.New(catType))
assert.NotEqual(t, aCatPtrVal, aCatPtrVal2) // The two addresses are different,
// but the struct values are the same. We check the Cat struct fields by
// calling aCatPtrVal.Elem() :
assert.Equal(t, "", aCatPtrVal.Elem().FieldByName("Name").String())
assert.Equal(t, "", aCatPtrVal2.Elem().FieldByName("Name").String())
assert.Equal(t, "<bool Value>", aCatPtrVal.Elem().FieldByName("Vaccinated").String())
assert.Equal(t, "<bool Value>", aCatPtrVal2.Elem().FieldByName("Vaccinated").String())
// Note we can't ask about struct fields that don't exist:
assert.Equal(t,
"<invalid Value>",
aCatPtrVal.Elem().FieldByName("Nonexistentfield").String())
// Then we call Value.Interface to give us thing itself
// as a reflect.Value. Here is what we wanted in the first place:
// an empty Cat struct. Well this certainly a roundabout way to get there.
assert.Equal(t, cat, aCatPtrVal.Elem().Interface())
// This checks the same thing as above:
assert.True(t, aCatPtrVal2.Elem().IsZero())
// Let's see if we can call Pet interface funcs.
aPet, ok := aCatPtrVal2.Interface().(Pet)
require.True(t, ok) // verify the cast worked
require.NotNil(t, aPet) // otherwise the linter warns we didn't do a nil check
// Now we are getting somewhere.
// make a JSON-serialized Cat and check that we can deserialize it.
shelterPet := `{"name":"Lily","vaccinated":true}`
require.NoError(t, aPet.Adopt([]byte(shelterPet)))
// Try calling more interface functions
assert.True(t, aPet.IsVaccinated())
assert.Equal(t, "Lily", aPet.PetName()) // woo! our deserialize worked!
// Can we play some more? Let's cast to Cat so we can directly
// examine Cat things.
aCat, ok := aCatPtrVal.Interface().(Cat)
typeOfACatPtrValInterface := reflect.TypeOf(aCat)
// ^^ What is this thing's type?
// It's a Cat.
assert.Equal(t, "reflect_cats_test.Cat", typeOfACatPtrValInterface.String())
// We can now call Adopt and prove that it worked:
require.NoError(t, aCat.Adopt([]byte(shelterPet)))
// We can also now look at Cat.Cat-specific fields and funcs:
assert.Equal(t, "Lily", aCat.Name)
assert.Equal(t, "meow!", aCat.Sound())
}
// Now that we've done some exploratory playing around,
// implement what we have learned.
func TestImplementation(t *testing.T) {
catPtrType := reflect.TypeOf(&Cat{})
petStream := `{"name":"Freddie","type":"Felis Catus"}`
// our function takes a type, and some data, and combines
// these two to return a Pet, if possible.
// if it's not possible, it returns nil.
AdoptFunc := func(incomingType reflect.Type, data []byte) Pet {
// incomingType is expected to be a pointer Kind. You can do a safety check:
if incomingType.Kind() != reflect.Ptr {
return nil
}
// From godoc: "Elem returns the value that the interface v
// contains or that the pointer v points to."
// So here reflect.New(incomingType.Elem()) is the same as saying
//
// vStructPtr := reflect.ValueOf(&Cat{})
//
// Since this function doesn't know at compile time what
// to deserialize, we use incomingType.Elem()
vStructPtr := reflect.New(incomingType.Elem())
// vStructPtr.Interface() gives us a *Cat. Check that
// it casts to a Pet interface.
pet, ok := vStructPtr.Interface().(Pet)
if !ok {
return nil
}
if err := pet.Adopt(data); err != nil {
return nil
}
return pet
}
res := AdoptFunc(catPtrType, []byte(petStream))
require.NotNil(t, res)
// Verify the Pet functions' outputs
assert.Equal(t, "Freddie", res.PetName())
assert.Equal(t, "Felis Catus", res.Species())
assert.False(t, res.IsVaccinated())
// verify that we can send AdoptFunc bad input without
// panicking.
// 1. try with the wrong type, e.g. a non-pointer type.
urlType := reflect.TypeOf(url.URL{})
res = AdoptFunc(urlType, []byte(petStream))
assert.Nil(t, res)
// 2. try with json data that will not serialize to a Cat.
res = AdoptFunc(catPtrType, []byte(`"garbage":"moregarbage"`))
assert.Nil(t, res)
// 3. try with a pointer type that doesn't implement Pet,
// but otherwise looks sort of like a Cat type struct.
type Borked struct {
Name string `json:"name"`
}
borkedPtrType := reflect.TypeOf(&Borked{})
res = AdoptFunc(borkedPtrType, []byte(petStream))
assert.Nil(t, res)
}
// There was another requirement for this story, which was to allow
// registration of types with validation functions.
//
// We have Pets which must be registered by their owners, including a
// Veterinarian that must be able to Examine the Pet and vaccinate them.
// If Examine is successful, then IsVaccinated should return true.
// Simulate getting a pet from a shelter by deserializing bites,
// I mean, bytes, into a Pet.
// The rest of this exercise is left to the reader — make the tests pass!
func TestRegistration(t *testing.T) {
cv := &CatVet{}
r := Registrar{}
t.Run("Can register but not re-register *Cat with Registrar", func(t *testing.T) {
tpf := reflect.TypeOf(&Cat{})
assert.NoError(t, r.RegisterPet(tpf, cv))
assert.Len(t, r.RegisteredPets, 1)
err := r.RegisterPet(tpf, cv)
assert.EqualError(t, err, "already registered Felis Catus")
})
t.Run("Attempting to register a non-Pet returns an error", func(t *testing.T) {
type notAPet struct{
Name string
}
err := r.RegisterPet(reflect.TypeOf(notAPet{ Name: "Chimpanzee" }), cv)
assert.EqualError(t, err, "not a Pet")
})
t.Run("returns nil, error if Pet is not a pointer", func(t *testing.T) {
tf := reflect.TypeOf(Cat{})
err := r.RegisterPet(tf, cv)
assert.EqualError(t, err, "reflect_cats_test.Cat is not a pointer")
})
t.Run("Dog is a Pet and can be 'adopted' too", func(t *testing.T){
// If you like, make the tests pass for a Dog struct of your own
// design.
type Dog struct {}
// var _ Pet = &Dog
})
}
func TestAdopt(t *testing.T) {
cv := &CatVet{}
r := &Registrar{}
cat := &Cat{}
t.Run("Registered pet is examined", func(t *testing.T) {
require.NoError(t, r.RegisterPet(reflect.TypeOf(cat), cv))
petStream := `{"name":"Bili","type":"Felis Catus"}`
t.Run("returns (Pet, nil) if examine succeeds.", func(t *testing.T){
pet, err := r.Adopt([]byte(petStream))
require.NoError(t, err)
assert.NotNil(t, pet)
assert.True(t, cat.IsVaccinated())
})
t.Run("returns error if examine fails", func(t *testing.T){
t.Fail()
})
})
t.Run("can register multiple species", func(t *testing.T){
t.Fail()
})
t.Run("returns nil, error if the type is not registered", func(t *testing.T) {
t.Fail()
})
t.Run("returns nil, error if the data cannot be unmarshaled", func(t *testing.T) {
t.Fail()
})
t.Run("returns nil, error if the data cannot be unmarshaled", func(t *testing.T) {
t.Fail()
})
}
// ======== IMPLEMENTATION =======
// — INTERFACES —
// Pet is a domesticated animal companion for humans. It can be
// "adopted" via Adopt.
type Pet interface {
IsVaccinated() bool
IsHealthy() bool
Adopt([]byte) error
PetName() string
Species() string
Vaccinate()
}
// — TYPES —
// Cat implements Pet
type Cat struct {
// Must be exported field or JSON will not marshal/unmarshal.
Name string `json:"name"`
Healthy bool `json:"healthy"`
Vaccinated bool `json:"vaccinated"`
}
// IsVaccinated returns the boolean value of f.Vaccinated
func (f *Cat) IsVaccinated() bool { return f.Vaccinated }
func (f *Cat) IsHealthy() bool { return f.Healthy }
// Adopt deserializes data into f.
func (f *Cat) Adopt(data []byte) error { return json.Unmarshal(data, f) }
// PetName returns the Pet's name.
func (f *Cat) PetName() string { return f.Name }
// Sound
func (f Cat) Sound() string { return "meow!" }
func (f Cat) Vaccinate() { f.Vaccinated = true }
// Species returns the string to use to register a type in the Registrar's
// map of allowed types.
func (f *Cat) Species() string { return "Felis Catus" }
// This construct is used to ensure Cat implements the Pet interface.
// If it doesn't, this doesn't compile.
var _ Pet = &Cat{}
// Veterinarian interface
type Veterinarian interface {
Examine(pet Pet) bool
}
type CatVet struct {}
func (cv *CatVet)Examine(pet Pet) bool {
if pet.IsHealthy() {
pet.Vaccinate()
return true
}
return false
}
// Simple registrar
type Registrar struct {
// RegisteredPets are mapped by their species. You could add a field in
// the Veterinarian struct that indicates what species the Veterinarian
// can Examine.
RegisteredPets map[string]Veterinarian
}
// RegisterPet registers Pets to Examine, with their Veterinarian.
// Registrar expects all Pet functions to have pointer receivers.
// (Not Labrador receivers) Returns error if:
// * the type is already registered
// * the type does not implement Pet
// * the type is not a pointer
func (r *Registrar) RegisterPet(t reflect.Type, vet Veterinarian) error {
panic("implement me")
return nil
}
// Adopt takes bytes and attempts to convert them into a Pet
// Returns nil + error if:
// * the Pet type is not registered
// * the pet data cannot be read
// * Examine returns false
func (r *Registrar) Adopt(petData []byte) (Pet, error) {
panic("implement me")
return nil, nil
}


Interested in more software development tips & insights? Visit the development section on our blog!

Shannon Wells
Shannon Wells

I'm a Principal Developer for Carbon Five.