0

Consider the following struct and interface definitions.

type Foo interface {
    Operate()
}

type Bar struct {
    A int
}

func (b Bar) Operate() {
    //...
}

Now, if we attempt to perform the following (playground):

var x Foo = Bar{}
err := json.Unmarshal([]byte("{\"a\": 5}"), &x)
fmt.Printf("x: %+v\nerr: %s\n", x, err) 

we get the following output:

x: {A:0}
err: json: cannot unmarshal object into Go value of type main.Foo

However, by replacing the underlying data with the struct type, it goes without a hitch (playground):

var x Foo = &Bar{}
err := json.Unmarshal([]byte("{\"a\": 5}"), &x)
fmt.Printf("x: %+v\nerr: %s\n", x, err)

Output:

x: &{A:5}    
err: %!s(<nil>)

However, this is quite confusing to me. In our call to Unmarshall, we're still passing a pointer to x, which to my understanding, should be sufficient enough to allow us to modify the Bar underneath. After all, pointers are simply addresses in memory. If we're passing that address, we should be able to modify it, no? Why does the second example work, but not the first? Why does the struct being a pointer make all the difference?

icza
  • 389,944
  • 63
  • 907
  • 827
ollien
  • 4,418
  • 9
  • 35
  • 58

1 Answers1

3

The root of the different behavior lies in the fact that if the json package has to create a new value, that value must be a concrete type and cannot be a (non-concrete) interface type.

Let's elaborate this.

First let's examine your 2nd, working example. Your variable x of type Foo wraps a pointer value of type *Bar. When you pass &x to json.Unmarshal(), the json package will get a *Foo pointer value. Pointer to interface! Should not be used, but there it is. The json package will dereference the pointer, and get the wrapped value of type *Bar. Since this is a non-nil pointer, the json package can–and will–go ahead and use it for unmarshaling. All good! The json package will modify the pointed value.

What happens in your first example?

In your first example your x variable of type Foo wraps a non-pointer struct value. Values wrapped in an interface cannot be modified.

What does this mean? The json package will again receive a value of type *Foo. Then it goes ahead and dereferences it, and gets a Bar value wrapped in an interface. The Bar value inside the Foo interface cannot be modified. The only way for the json package to "deliver" the results would be to create a new value that implements Foo, and store this value where the initially passed *Foo points to. But values of Foo cannot be created, it's a non-concrete interface type. So unmarshal returns with an error.

Some addendum. Your 2nd working example:

var x Foo = &Bar{}
err := json.Unmarshal([]byte("{\"a\": 5}"), &x)
fmt.Printf("x: %+v\nerr: %s\n", x, err)

This works, because we "prepare" a non-nil *Bar value to unmarshal into, so the json package doesn't have to create a value itself. We already prepared and passed a value for the Foo interface type (being a value of concrete type *Bar).

If we would store a "typed" nil pointed in x like this:

var x Foo = &Bar{}
x = (*Bar)(nil)
err := json.Unmarshal([]byte("{\"a\": 5}"), &x)
fmt.Printf("x: %+v\nerr: %s\n", x, err)

We will get back the same error (try it on the Go Playground):

x: <nil>
err: json: cannot unmarshal object into Go value of type main.Foo

The explanation is the same: the json package cannot use the nil pointer to unmarshal the value into. It would again need to create a value of non-concrete type Foo, but that is not possible. Only values of concrete types can be created.

icza
  • 389,944
  • 63
  • 907
  • 827
  • I can understand why passing a straight `Bar` in would leave us with something unmodifiable. However, why does making the pointer to the struct, rather than the interface, make all the difference? We have its address, so we have still pointed to the same data (the `Bar` itself) in memory, no? If we have its address, why can't we modify it just as we would a `*Bar`? – ollien May 28 '18 at 11:15
  • @ollien We don't have its address, that's the point. Its address would be of type `*Bar`. But what we have is an address of type `*Foo`. You can dereference the `*Foo` pointer which gives you the wrapped value `Bar`. But this is not a pointer type, so we still don't have the address of this `Bar` value. – icza May 28 '18 at 11:16
  • Does the `*Foo` not contain the `Bar`? I think that is the source of my confusion. In my mind, they would be at the same location in memory, but I could very well be off-base. – ollien May 28 '18 at 11:19
  • @ollien The `*Foo` pointer points to an interface value, which is schemantically a (value; type) pair. This address is not the same as the address of the wrapped value. Having a `*Foo` pointer, you can't obtain an address to the wrapped `Bar` value (which would be of type `*Bar`). – icza May 28 '18 at 11:29
  • Why can you not get that address? – ollien May 28 '18 at 11:34
  • @ollien In short, because the language spec does not allow it. In long, because a copy is stored in interface values, and if it would be allowed to take the copy's address, you could only modify the copy which would be the source of even more confusion. For details, see this answer: [How can a slice contain itself?](https://stackoverflow.com/questions/36077566/how-can-a-slice-contain-itself/36078970#36078970) – icza May 28 '18 at 11:39
  • I guess this all makes sense - it seems like an intentional decision. One last thing though. You say that modifying the copy would be the source of confusion. Could you elaborate on that? – ollien May 28 '18 at 11:54
  • @ollien If you could obtain a pointer to the non-pointer wrapped value, you modify it, and you would expect the interface value to hold the new value, but obviously it wouldn't. This "modification" could be indirect, e.g. the wrapped value could have a method with pointer receiver which modifies the (pointed) receiver. You call this method, and everyone would think it modified the value (pointed by the receiver), yet, its effect would be lost as that would only be done on a copy. – icza May 28 '18 at 12:02
  • I feel bad to keep drawing this out. I apologize. Why would the interface not hold the new value, if we are modifying the memory it points to? There's something I'm missing here that I can't put my finger on. – ollien May 28 '18 at 12:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/171914/discussion-between-icza-and-ollien). – icza May 28 '18 at 12:16