Skip to content

Instantly share code, notes, and snippets.

@shannonwells
Last active May 19, 2021 17:27
Show Gist options
  • Save shannonwells/8a933210213c1ea260a90f456513e9c3 to your computer and use it in GitHub Desktop.
Save shannonwells/8a933210213c1ea260a90f456513e9c3 to your computer and use it in GitHub Desktop.
Reflections in Go, For Cats
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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment