Golang Pointer Receivers and Error Pointer Returns
Recently, I added a GetMulti endpoint with the following header:
The definition is problematic because of the error pointer return. When testing the new endpoint, it threw an error (returned as a: *ServiceError
), which was fine and should have been handled graciously by propagating the error further. By propagating the error, the work of intercepting and converting to a final gRPC status code would be delegated to an error interceptor. However, due to the nature of how Golang handles pointer dereferencing, propagating the error caused a panic whenever the endpoint returned an error.
The error interceptor uses the following translation function:
Notice it takes in an error of type error
but, a *ServiceError
was returned by the endpoint. In this case, *ServiceError
satisfies the error interface. However, a nil
*ServiceError
is not equal to a nil
error
, which caused FromError
to perform the err.Error()
return.
The following example illustrates the difference:
Uncommenting the last line results in:
panic: runtime error: invalid memory address or nil pointer dereference
Taken from Golang FAQs:
Under the covers, interfaces are implemented as two elements, a type T and a value V. V is a concrete value such as an int, struct or pointer, never an interface itself, and has type T. For instance, if we store the int value 3 in an interface, the resulting interface value has, schematically, (T=int, V=3). The value V is also known as the interface’s dynamic value, since a given interface variable might hold different values V (and corresponding types T) during the execution of the program.
An interface value is nil only if the V and T are both unset, (T=nil, V is not set), In particular, a nil interface will always hold a nil type. If we store a nil pointer of type int inside an interface value, the inner type will be int regardless of the value of the pointer: (T=*int, V=nil). Such an interface value will therefore be non-nil even when the pointer value V inside is nil.
As a result, I ended up refactoring all methods that were returning the concrete type *ServiceError
to return error
type errors. Finally, to quote a colleague:
The separation of the pointer to the definition of the interface and the pointer to the implementing struct underlying it is a pretty important thing to learn in Golang. I know it’s been a “lightbulb” moment for a lot of people when learning the language.
It definitely was for me.
Additional resources on the topic: