我可以将JSON编组为接口的实现者吗?

I have an interface that declares a method and some structs that implement that interface. Now I want to unmarshal some JSON into instances of these structs. To wit:

package main

import (
    "encoding/json"
    "fmt"
)

type Animal interface {
    makeNoise() string
}

type Dog struct {
    Name string
}

func (d Dog) makeNoise() string {
    return "woof"
}

type Fish struct {
    NumScales int
}

func (f Fish) makeNoise() string {
    return "glub glub glub"
}

type Zoo struct {
    Animals []Animal
}

func main() {
    animals := `{"Animals": [{"Name": "Fido"}, {"NumScales": 123}]}`
    animalBytes := []byte(animals)
    var zoo Zoo
    er := json.Unmarshal(animalBytes, &zoo)
    if er != nil {
        panic(er)
    } else {
        fmt.Println(zoo)
    }
}

But when I run that, I get "panic: json: cannot unmarshal object into Go value of type main.Animal". Can I instead get a Zoo whose Animals are a Dog named Fido and a Fish with 123 scales?

There is no straight forward way to achieve what you want based on the current condition you gave us. @eduncan911 provided a very general method, but however, if you are able to tweak the JSON input a bit, you are able to achieve it using the following method.

The core idea is to use json.RawMessage as a buffer to delay the unmarshal until it knows the type it's gonna unmarshal to.

Firstly, tweak the JSON input to something like below:

{
    "Animals": [{
        "Type": "dog",
        "Property": {
            "Name": "Fido"
        }
    },{
        "Type": "fish",
        "Property": {
            "NumScales": 123
        }
    }]
}

From what I can see, this tweak does not make the JSON worse, but actually make it better in terms of readability.

Then, create a new struct, say AnimalCard:

type AnimalCard struct {
    Type string
    Property json.RawMessage
    Animal Animal
}

And modify your Zoo to

type Zoo struct {
    Animals []*AnimalCard
}

Now unmarshal your json to zoo, you will get an array of *AnimalCard. Now you could iterate through zoo array and unmarshal it according to type:

for _, card := range zoo.Animals {
    if card.Type == "dog" {
        dog := Dog{}
        _ = json.Unmarshal(card.Property, &dog)
        card.Animal = dog
    } else if card.Type == "fish" {
        fish := Fish{}
        _ = json.Unmarshal(card.Property, &fish)
        card.Animal = fish
    }
}

Playground Exmaple is here.

What if I got more and more Animals in the Zoo?

Good question :) The problem the above solution gave won't be that scalable. What if we have 20 animals, not only 2? What if 200? 2000? We need a more general way to do it.

The core idea this time is to use reflect.

First, we could maintain a map, which maps a type name to an interface implementation:

mapper map[string]Animal{}

Then we put in our animals pointers:

mapper["dog"] = &Dog{}
mapper["fish"] = &Fish{}

Now, after we unmarshalled the JSON into AnimalCard and start iterating, we use reflection to initialize a new instance pointer and unmarshal into it:

for _, card := range zoo.Animals {
    // get the animal type pointer
    animal := mapper[card.Type]
    // get the pointer's type
    animalType := reflect.TypeOf(animal)
    // create a new instance pointer of the same type
    newInstancePtr := reflect.New(animalType.Elem()).Interface().(Animal)
    // unmarshal to the pointer
    _ = json.Unmarshal(card.Property, newInstancePtr)
    // assign the pointer back
    card.Animal = newInstancePtr
}

Playground Example is here.

Use the json.Unmarshaler interface to create a custom UnmarshalJSON methods. Then within the method, test the type casting to see which type works, assign it, and return it.

Good summary at the end of this post:

http://attilaolah.eu/2013/11/29/json-decoding-in-go/