I have an interesting problem.
I want to make a request to an endpoint that gives me an array of objects back in an HTTP response.
I then want to unmarshal that response into a slice struct. Easy enough.
This code works:
type Human struct {}
func (h Human) Talk() {}
type CanTalk interface{
Talk()
}
type Model struct {
List []CanTalk
}
func main() {
model := Model{}
data, err := makeRequestToEndpoint()
if err != nil {
// ...
}
err = json.Unmarshal(data, &model.List)
if err != nil {
// ...
}
for _, human := range model.List {
human.Talk()
}
}
But here's the catch: I want to do this generically.
I don't want to write this same logic for every single type in my system (since they're all in separate files), so I opted to use an interface{}
as opposed to []CanTalk
, so that I could abstract the unmarshal and logic below it to a utility function. The interface{}
is then seeded with a concrete type for json.Unmarshal.
Even though this compiles and runs, it does not work:
type Human struct {}
func (h Human) Talk() {}
type CanTalk interface{
Talk()
}
type Model struct {
List interface{} // Using interface{} here instead of []CanTalk
}
func main() {
model := Model{List:[]Human{}} // Seeding the list with the specified type to unmarshal into
data, err := makeRequestToEndpoint()
if err != nil {
// ...
}
err = json.Unmarshal(data, &model.List)
if err != nil {
// ...
}
if list, ok := model.List.([]interface{}); ok {
for i := range list {
// This line is never reached, because "ok == false"
if human, ok := list[i].(CanTalk); ok {
// Never reached
human.Talk()
}
}
}
}
As you can see, the unmarshal does not pick up on the seed that I gave to model.List
. The output of model.List
after unmarshaling is still in the format of the HTTP response payload (a map[string]interface{}
or something like that), not the type of the seed, hence the failure to cast into a CanTalk
type.
Is this even possible, or would the only way to achieve this (short of real generics) be to repeat the unmarshal logic for each separate type in my system?