Monday, June 6, 2011

golang interface is not nil even if 'containing' a nil pointer


Today I was struggling with a weird issue with "err != nil" during a refactoring of the go9p library. Here is a simplified situation:




This prints:

"Test *main.Test 0x0 dummy"

here 'err' is not nil, even if nil is actually returned.

The problem arised because go9p has an inner API returning some custom *Error, which is then 'casted' to a os.Error interface in the public API layer, so that it can implement the io.Reader,... interfaces. All code using this standard API was getting errors even if no error was returned.

From what I understood, it goes this way: *Test implements os.Error. *Test can be nil since it's a pointer.
os.Error is an interface value. Any interface value can also be nil.
When a value of type X is put* in a variable (or return value) declared as interface Y, then a new value is created holding X. In our case a value of type os.Error contains a *Test, both of which can be nil.

"if err != nil" checks whether 'err', the interface value, is nil, which is not.  "%T" fetches the runtime type of err, which skips (confusingly) the interface envelope and prints *main.Test.
"%p" also skips the interface value, probably because it's not really a pointer (or perhaps because it sounded more natural to follow what the underlying real value points to).
Lastly "%s" invokes the String() method, which is implemented for *Test; here "self != nil" is true because here we have the real underlying pointer. 

So, assuming I understood correctly what's happening, we can generalize this issue to:


This is because an interface value has to have a "zero" value, and it's nil. If you assign a pointer which implements that interface, that pointer can point somewhere or be nil, it doesn't matter, it's a valid pointer; and if you assign this pointer to the interface value, that interface value now has a value which is not nil.

I imagine that otherwise it would be impossible to have a zero value for the interfaces:


So, in order to fix this code you have to catch and "rethrow" the error:

This might seem a big issue, but usually this kind of code already checks for errors and returns so you just have to pay attention to not assigning to a previously declared interface value like os.Error, for example here "err" was inferred as os.Error in a previous ":=" assignement and then used to hold a *Error:

2 comments: