Programming for fun and profit

A blog about software engineering, programming languages and technical tinkering

Sat 21 October 2023

The sheer insanity of interfaces and nil in Go

Posted by Simon Larsén in Programming   

If you've only dabbled briefly in Go, you might think that its nil is analogous to the good ol' "billion dollar mistake" known as null. I thought so, too, up until just a few weeks ago when I decided to make a pass through Thorsten Ball's neat little book Writing an Interpreter in Go. That's when I first cut myself real bad on nil, or more specificaly, an interface with a nil value.

The sensible kind of nil

First let's look at nil behaving like we'd expect. Here's a code snippet of a "parser" that somewhat shows the situation I had.

package main

import (
    "fmt"
    "strings"
)

type IfExpression struct {
    raw string
}

func parseIf(s string) *IfExpression {
    if strings.HasPrefix(s, "if") {
        return &IfExpression{raw: s}
    }

    return nil
}

So, we have a parse function that returns a pointer to an IfExpression struct if the input is an if expression (with an incredibly loose definition of what constitutes an if expression). Let's add a main function and try it out.

func main() {
    ifExpr := parseIf("if a == b")
    fmt.Println("ifExpr", ifExpr, ifExpr == nil)

    notIfExpr := parseIf("not an expression")
    fmt.Println("notIfExpr", notIfExpr, notIfExpr == nil)
}

Running this results in completely sensible output; ifExpr is not nil while notIfExpr is.

$ go run main.go
&{if a == b} false
<nil> true

Say what you want about nil, this at least makes complete sense and is intuitively understandable. But when we throw interfaces into the mix, that goes way out the window.

The completely insane kind of nil

As my parser came along, it turned out I needed multiple kinds of expressions, and so had to abstract the parse function to be able to return multiple kinds of expressions. So let's generalize the expression in an interface and add another one.

type Expression interface {
    Raw() string
}

type IfExpression struct {
    raw string
}

func (ie *IfExpression) Raw() string { return ie.raw }

type ForExpression struct {
    raw string
}

func (fe *ForExpression) Raw() string { return fe.raw }

And then we adapt the parsing as well.

func parse(s string) Expression {
    if ifExpr := parseIf(s); ifExpr != nil {
        return ifExpr
    } else {
        return parseFor(s)
    }
}

func parseFor(s string) *ForExpression {
    if strings.HasPrefix(s, "for") {
        return &ForExpression{raw: s}
    }

    return nil
}

func parseIf(s string) *IfExpression {
    if strings.HasPrefix(s, "if") {
        return &IfExpression{raw: s}
    }

    return nil
}

Now, we have a more generalized parse function that tries to parse an if expression, and falls back on parsing a for expression if it turns out not to be an if. We then adapt our main function to make use of this.

func main() {
    ifExpr := parse("if a == b")
    fmt.Println("ifExpr", ifExpr, ifExpr == nil)

    forExpr := parse("for a in b")
    fmt.Println("forExpr", forExpr, forExpr == nil)
}

And at first glance, this seems to work as expected.

$ go run main.go 
ifExpr &{if a == b} false
forExpr &{for a in b} false

But what happens when you try to parse something that is neither an if nor a for? We add the following to main to find out.

    notAnExpr := parse("not an expression")
    fmt.Println("notAnExpr", notAnExpr, notAnExpr == nil)

Clearly, as "not an expression" is neither an if expression nor a for expression, both parseIf and parseFor will return nil, and as such parse should return nil. But that's not really what happens.

$ go run main.go 
ifExpr &{if a == b} false
forExpr &{for a in b} false
notAnExpr <nil> false

What? notAnExpr is <nil>, but it does not compare true to a literalnil? What does that mean?

The confusing type-and-value composition of interfaces

In Go, an interface is represented at runtime as a type and a value. You can think of it as a struct with two fields.

type Interface struct {
    type  string
    value interface{}
}

So, when parseIf(s) returns nil, the parse(s) function's return statement wraps the nil value into something like this.

Interface {
    Type: "IfExpression",
    Value: nil,
}

Armed with this knowledge, we can actually check for nil using runtime reflection.

func IsNil(value interface{}) bool {
    return reflect.ValueOf(value).IsNil()
}

func main() {
    ifExpr := parse("if a == b")
    fmt.Println("ifExpr", ifExpr, IsNil(ifExpr))

    forExpr := parse("for a in b")
    fmt.Println("forExpr", forExpr, IsNil(forExpr))

    notAnExpr := parse("a == b")
    fmt.Println("notAnExpr", notAnExpr, IsNil(notAnExpr))
}

And running this, we now seem to have a functioning way to check for nil interfaces.

ifExpr &{if a == b} false
forExpr &{for a in b} false
notAnExpr <nil> true

But do we really? Of course not.

Actually, interfaces can also be "completely" nil

Let's make this even more confusing, and refactor our parse function a bit. Instead of "falling back" on parseFor, we simply return nil explicitly if we can't parse any known expression.

func parse(s string) Expression {
    if ifExpr := parseIf(s); ifExpr != nil {
        return ifExpr
    } else if forExpr := parseFor(s); forExpr != nil {
        return forExpr 
    }

    return nil
}

Now let's run it again.

$ go run main.go 
ifExpr &{if a == b} false
forExpr &{for a in b} false
panic: reflect: call of reflect.Value.IsNil on zero Value

Suddenly, notAnExpr is nil, causing our check to panic. What in the world is going on here? As it turns out, explicitly returning nil from a function that returns an interface is semantically different to returning a variable contaning nil value, and typed with something implementing the interface.

return nil // actually returns nil

var value *ForExpression = nil
return nil // returns an interface with type *ForExpression and value nil

So a way around this is of course to always return nil in the form of a typed variable. But I'm not satisfied with that, because even if I'm very strict about it myself, others may not be. And I may also just make an honest mistake. So let's tweak our IsNil() function to account for true nil.

func IsNil(value interface{}) bool {
    return value == nil || reflect.ValueOf(value).IsNil()
}

If you run this, you'll see that the runtime panic is replaced with the following.

notAnExpr <nil> true

So we're done? Unfortunately, not quite.

Structs are a problem

Our IsNil() handles true nil and interfaces with nil values. But, unfortunately, it does not handle structs.

value := IfExpression{raw: "if"}
fmt.Println(IsNil(value)) // panic: reflect: call of reflect.Value.IsNil on struct Value

Poop. It appears we missed a critical part of IsNil()'s documentation. And by missed, I of course mean that we didn't read it, but here's the relevant part.

IsNil reports whether its argument v is nil. The argument must be a chan, func, interface, map, pointer, or slice value; if it is not, IsNil panics.

So, we need to check if the value is of any of these nil-able types, and otherwise assume that it is not nil.

func IsNil(value interface{}) bool {
    if value == nil {
        return true
    }

    reflected := reflect.ValueOf(value)
    switch reflected.Kind() {
        case
        reflect.Chan,
        reflect.Func,
        reflect.Interface,
        reflect.Map,
        reflect.Ptr,
        reflect.Slice:
        return reflected.IsNil()
    }
    return false
}

And with this, the panic is abated.

value := IfExpression{raw: "if"}
fmt.Println(IsNil(value)) // false

This will work so long as Go doesn't add some other nil-able type. Given how conservative the language is, such an addition to the core types seems an unlikely prospect. But of course, it could happen, and then this function would return false incorrectly in some cases.

Footguns

All languages have footguns. Some languages, like C++ and JavaScript, are seemingly built exclusively of them. Go is a small-ish and conservative language, and so naturally has fewer such contraptions. But how nil is handled differently in different contexts and how the type of a variable can change the actual value that's returned from a function, is a big one.

Of course, all of this may just be symptoms of my not knowing Go very well, and this whole article could just be the frustrated ramblings of an apprentice. But, I'm an experienced programmer, and this confused me quite a bit. That alone should be sufficient evidence that some design choices made here aren't optimal.