4

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.

icza
  • 389,944
  • 63
  • 907
  • 827
vitaminwater
  • 6,934
  • 3
  • 19
  • 23
  • 3
    A good rule of thumb is to never use a pointer to an interface. There's almost never a need for one. – JimB Aug 08 '16 at 13:42

1 Answers1

5

The right scenario

interface{} is a wrapper for any value and of any type. An interface 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.

icza
  • 389,944
  • 63
  • 907
  • 827
  • Thanks @icza for the good read, this will surely be useful to a bunch of us. – vitaminwater Aug 09 '16 at 07:06
  • 1
    Just went through the docs you pointed to, and especially the passage aboute settability, I guess that's what prevents Unmarshal to use the value in the interface{} when given a non-pointer. – vitaminwater Aug 09 '16 at 07:32
  • 1
    @vitaminwater Yes. The value in the interface is a copy. If you want the `json` package to unmarshal into a variable you pass, you have to pass its address - a pointer (which will also be passed as a value), but the `json` package will not modify the pointer but the _pointed_ value - which is your variable. – icza Aug 09 '16 at 07:35
  • For anyone seeking full explanations: https://blog.golang.org/laws-of-reflection#TOC_9. thanks @icza – vitaminwater Aug 09 '16 at 07:40