go has some tricks for handling dynamically typed values, and how it handles json makes for a good example.
in other static languages, you might write a foo.toJSON() method for each and every type, along with a foo.fromJson() function to return a new object. the effort involved in doing so means this is often done with codegen or macros.
meanwhile, in golang? there's just two functions, bytes, err := json.Marshall(object) and err := json.Unmarshal(bytes, &output), which use reflection to avoid having to create special functions for every type.
for encoding, Marshall reflects over the structs passed to get the field names and values, and converts them to json. for decoding Unmarshall reflects over the empty struct passed in to do something very similar.
type Output struct {
FieldA int
FieldB string
}
var output Output
err := json.Unmarshal(bytes, &output)
it's a trick used all over golang, anywhere you'd want to pass in a type for an output function, one example of it is a database library. for one go ORM, you call .Scan(&results) at the end of a big chain of methods, which lets you unpack a row of results into a given struct.
using the &output trick means not having to return map[string]any arguments around, and it's one of the nicer things go lets you do to handle json.
it isn't always nice, though. let's get onto the crimes.
let's say we have some json objects that we want to handle in go: a request message, and a response message.
{"MessageKind": "Request", "Verb": "GET", "Address":"/"}
{"MessageKind": "Response", "Code": 200, "Status": "OK", "Payload": "Hello, World!"}
and let's say we do this with an interface, Message, and several structures that implement it. you might write something like this:
type Message interface {
Properties() map[string]any
}
struct Request {MessageKind string, Verb string; Address string }
struct Response {MessageKind string, Code int; Status string; Payload string}
func (*Request) Properties() { ....}
func (*Response) Properties() { ....}
... and let's say we have some other struct, with some field M Message in it, which we want to turn to and from json.
the good news? encoding works: the field M points to some struct, the encoder reflects upon it to find out the fields, and spits out json with the right fields.
the bad news? decoding doesn't exactly work. you can't tell golang "hey, if you see a interface, and you need to unpack json into it, use this struct". you can't tell golang, "here's a method for this interface to unmarshall it" because structs, not interfaces, have methods.
so? what's a coder to do? you wrap the interface in another struct, and then tell go how to turn that struct to and from json
type Envelope struct {
M Message
}
func (e *Envelope) UnmarshalJSON(bytes []byte) error {
var header struct {MessageKind string}
err := json.Unmarshal(bytes, &header)
if err != nil { return err }
if header.MessageKind == "Request" {
e.M = &Request{MessageKind: "Request"}
} else if header.MessageKind == "Response" {
e.M = &Response{MessageKind: "Response"}
} else {
return errors.New("Unknown message: "+ header.MessageKind)
}
return json.Unmarshal(bytes, e.M)
}
func (e Envelope) MarshalJSON() ([]byte, error) {
return json.Marshal(e.M)
}
et voila
Wherever you want to use Message inside a serialized struct, you now use Envelope. When go loads json into the structure, it works out which struct to use, and then returns it. When go creates json from the structure, it's as if the envelope wasn't there.
crimes.
