Warm tip: This article is reproduced from serverfault.com, please click

How to save and load a structure of interfaces in go

发布于 2020-11-29 13:24:45

Coming from python I am used to be able to write a class with custom methods. Initiate it, save the object and load this again. I am trying to accomplish something similar in go. After reading about serialization I tried to use the marshall/unmarshall approach.

But below code deos not work as it results in method.Method = nil. I want to be able to call method.Method.Calculcate()

Below is my attempt.

import (
    "encoding/json"
    "fmt"
    "log"
)

type Calculator struct {
    Method Method `json:"Method"`
}

type method1 struct {
    Input string `json:"input"`
}

func NewMethod1(input string) Method {
    return &method1{
        Input: input,
    }
}

type method2 struct {
    Input string `json:"input"`
}

func NewMethod2(input string) Method {
    return &method2{
        Input: input,
    }
}

type Method interface {
    Calculcate()
}

func (m *method1) Calculcate() {
}
func (m *method2) Calculcate() {
    }

func main() {
    model := Calculator{
        Method: NewMethod1("inputData"),
    }
    model.Method.Calculcate()
    var jsonData []byte
    jsonData, err := json.Marshal(model)
    if err != nil {
        log.Println(err)
    }
    fmt.Println(string(jsonData))

    var method Calculator
    json.Unmarshal(jsonData, &method)
    if err != nil {
        log.Println(err)
    }
}

I want to load the struct and use the same code to initiate method1 as method 2 when calculcating. This means I don't know in advance whether I am loading method 1 or method 2. Below is a little bit of pseudocode explaining what I want.

Calculator :=Load Calculator()
model := Calculator{
    Method: NewMethod1("inputData"),
}
model.Method.Calculcate()
Questioner
MathiasRa
Viewed
0
icza 2020-11-29 22:13:23

The problem is that decoding a JSON does not know what concrete type to use when a field has an interface type (multiple types may implement an interface). It might be obvious in your example, but just think a little further: you send this JSON over to another computer trying to decode it. The JSON representation does not record the concrete type for interface values (which in this case is *main.method1), so the other end does not know what to instantiate for the interface field Calculator.Method.

If you need this kind of serialization and deserialization, use the encoding/gob package instead. That package also writes type information so the decoder part at least knows the type name. encoding/gob does not transmit type definition, so if the other part has a different version of the type, the process can also fail. This is documented in the package doc:

Interface types are not checked for compatibility; all interface types are treated, for transmission, as members of a single "interface" type, analogous to int or []byte - in effect they're all treated as interface{}. Interface values are transmitted as a string identifying the concrete type being sent (a name that must be pre-defined by calling Register), followed by a byte count of the length of the following data (so the value can be skipped if it cannot be stored), followed by the usual encoding of concrete (dynamic) value stored in the interface value. (A nil interface value is identified by the empty string and transmits no value.) Upon receipt, the decoder verifies that the unpacked concrete item satisfies the interface of the receiving variable.

Also for the encoding/gob package to be able to instantiate a type based on its name, you have to register these types, more specifically values of these types.

This is a working example using encoding/gob:

First let's improve the Calculate() methods to see that they are called:

func (m *method1) Calculcate() {
    fmt.Println("method1.Calculate() called, input:", m.Input)
}

func (m *method2) Calculcate() {
    fmt.Println("method2.Calculate() called, input:", m.Input)
}

And now the serialization / deserialization process:

// Register the values we use for the Method interface
gob.Register(&method1{})
gob.Register(&method2{})

model := Calculator{
    Method: NewMethod1("inputData"),
}
model.Method.Calculcate()

buf := &bytes.Buffer{}
enc := gob.NewEncoder(buf)
if err := enc.Encode(model); err != nil {
    log.Println(err)
    return
}
fmt.Println(buf.Bytes())

var model2 Calculator
dec := gob.NewDecoder(buf)
if err := dec.Decode(&model2); err != nil {
    log.Println(err)
    return
}
model2.Method.Calculcate()

This will output (try it on the Go Playground):

method1.Calculate() called, input: inputData
[35 255 129 3 1 1 10 67 97 108 99 117 108 97 116 111 114 1 255 130 0 1 1 1 6 77 101 116 104 111 100 1 16 0 0 0 48 255 130 1 13 42 109 97 105 110 46 109 101 116 104 111 100 49 255 131 3 1 1 7 109 101 116 104 111 100 49 1 255 132 0 1 1 1 5 73 110 112 117 116 1 12 0 0 0 16 255 132 12 1 9 105 110 112 117 116 68 97 116 97 0 0]
method1.Calculate() called, input: inputData