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
nilvalues into the example function and instead only passnilin explicitly. This works because an explicit nil has not type so the underlying interface will have aniltype 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
nilusing 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
