vitaminwater vitaminwater - 3 months ago 52
JSON Question

Golang interface{} type misunderstanding

I got a bug in Go when using an

interface{}
as function parameter type, when given a non-pointer type, and using
json.Unmarshal
with it.

Because a piece of code is worth a thousand words, here is an example:

package main

import (
"encoding/json"
"fmt"
)

func test(i interface{}) {
j := []byte(`{ "foo": "bar" }`)
fmt.Printf("%T\n", i)
fmt.Printf("%T\n", &i)
json.Unmarshal(j, &i)
fmt.Printf("%T\n", i)
}

type Test struct {
Foo string
}

func main() {
test(Test{})
}


Which outputs:

main.Test
*interface {}
map[string]interface {}


json.Unmarshal
turns my struct to a
map[string]interface{}
oO...

Little readings later explains some of it,
interface{}
is a type in itself, not some sort of typeless container, which explains the
*interface{}
, and the fact that
json.Unmarshal
could not get the initial type, and returned a
map[string]interface{}
..

From
Unmarshal
docs:


To unmarshal JSON into an interface value, Unmarshal stores one of
these in the interface value:
[...]


And if I pass a pointer to the test function like so, it works:

func test(i interface{}) {
j := []byte(`{ "foo": "bar" }`)
fmt.Printf("%T\n", i)
fmt.Printf("%T\n", &i)
json.Unmarshal(j, i)
fmt.Printf("%T\n", i)
fmt.Println(i)
}

func main() {
test(&Test{})
}


Which outputs:

*main.Test
*interface {}
*main.Test
&{bar}


Cool, the data is unmarshalled and all, now in this second snippet I removed the
&
when calling
Unmarshal
. Because I have a
*Test
in
i
, no use for it.

So in all logic, if I put back the
&
to
i
when calling
Unmarshal
it should mess up with
i
's type again. But no.

If I run:

func test(i interface{}) {
j := []byte(`{ "foo": "bar" }`)
fmt.Printf("%T\n", i)
fmt.Printf("%T\n", &i)
json.Unmarshal(j, &i)
fmt.Printf("%T\n", i)
fmt.Println(i)
}

func main() {
test(&Test{})
}


Well it still works:

*main.Test
*interface {}
*main.Test
&{bar}


And now I'm out of google search queries.

Answer

The right scenario

interface{} is a wrapper for any value and of any type. An interfaces schematically wraps a (value; type) pair, a concrete value and its type. More details on this: The Laws of Reflection #The representation of an interface.

json.Unmarshal() already takes the value of type interface{}:

func Unmarshal(data []byte, v interface{}) error

So if you already have an interface{} value (the i interface{} parameter of the test() function), don't try to take its address, just pass it along as-is.

Also note that for any package to modify a value stored in an interface{}, you need to pass a pointer to it. So what should be in i is a pointer. So the right scenario is to pass *Test to test(), and inside test() pass i to json.Unmarshal() (without taking its address).

Explanation of other scenarios

When i contains *Test and you pass &i, it will work because the json package will simply dereference the *interface{} pointer, and finds an interface{} value, which wraps a *Test value. It's a pointer, so it's all good: unmarshals the JSON object into the pointed Test value.

When i contains Test and you pass &i, same thing goes as above: *interface{} is dereferenced, so it finds an interface{} which contains a non-pointer: Test. Since the json package can't unmarshal into a non-pointer value, it has to create a new value. And since the passed value to the json.Unmarshal() function is of type *interface{}, it tells the json package to unmarshal the data into a value of type interface{}. This means the json package is free to choose which type to use. And by default the json package unmarshals JSON objects into map[string]interface{} values, so that is what's created and used (and eventually put into the value pointed by the pointer you passed: &i).

All in all

All in all, avoid using pointers to interfaces. Instead "put" pointers into the interfaces (the interface value should wrap the pointer). When you already have an interface{} holding a pointer, just pass it along.

Comments