Summary
This lightning talk was presented at the Utah Go Meetup and covered some unintuitive ways
that nil
and interfaces can interact in Go.
Key Takeaways
Interfaces in Go wrap a value and include both a type
as well as a value
.
In order for an interface to be nil, both the type
and value
must be nil.
For example:
package main
func main() {
var a *int // a is nil
var b interface{} // b is nil
c := interface{}(a) // c is not nil
}
In this snippet a
is clearly nil
becase the default value for all pointer types in Go is nil
.
b is also nil because the default type
and value
for an interface{} are both nil
.
c however, is not nil because while it wraps a nil value
, it’s underlying type is *int
.
Name | Type | Value | Is Nil? |
---|---|---|---|
a | n/a | nil | yes |
b | nil | nil | yes |
c | *int | nil | no |
This can lead to some unexpected results when using nil
checks in your code.
package main
import (
"bytes"
"io"
)
func main() {
var bodyBuf *bytes.Buffer
example(bodyBuf)
}
func example(body io.Reader) {
if body == nil { // prevent nil panics
return
}
switch v := body.(type) {
case *bytes.Buffer:
bodyLen := v.Len() // still get a nil panic
}
}
In the example above you can see that while we do check if body is nil, we still end up with a nil
panic when the code is run.
This is because body
is actually an interface with a concrete type *bytes.Buffer
and therefore is not nil
.
Once we convert the interface back into a *bytes.Buffer
we’re back to a nil
value.
This causes the code to panic when we try to call the buffers Len method.
We can fix this code in one of three ways.
- We can avoid passing
nil
values into the example function and instead only passnil
in explicitly. This works because an explicit nil has not type so the underlying interface will have anil
type as well.
package main
import (
"bytes"
"io"
)
func main() {
var bodyBuf *bytes.Buffer
if bodyBuf == nil {
example(nil)
} else {
example(bodyBuf)
}
}
...
- We can check for nil after type casting inside the example function.
package main
import (
"bytes"
"io"
)
func main() {
var bodyBuf *bytes.Buffer
example(bodyBuf)
}
func example(body io.Reader) {
if body == nil {
return
}
switch v := body.(type) {
case *bytes.Buffer:
if v == nil { // check for nil again
return
}
bodyLen := v.Len()
}
}
- Or, we can directly check if the value inside the interface is
nil
using the reflect package.
package main
import (
"bytes"
"io"
"reflect"
)
func main() {
var bodyBuf *bytes.Buffer
example(bodyBuf)
}
func example(body io.Reader) {
if body == nil || isNilV(body) {
return
}
// ...
}
func isNilV(r io.Reader) bool {
if r == nil {
return true
}
// https://pkg.go.dev/reflect#Value.IsNil
rv := reflect.ValueOf(r)
if (rv.Kind() == reflect.Ptr && rv.IsNil()) ||
(rv.Kind() == reflect.Chan && rv.IsNil()) ||
(rv.Kind() == reflect.Func && rv.IsNil()) ||
(rv.Kind() == reflect.Map && rv.IsNil()) ||
(rv.Kind() == reflect.Interface && rv.IsNil()) ||
(rv.Kind() == reflect.Slice && rv.IsNil()) {
return true
}
return false
}
A quick note on this final fix.
isNilV
as presented here is error-prone and does not cover several edge cases.
Because of this, reflect
should be used with caution to check for nil interfaces.
Details
The full source code fo this talk can be found here