In Go, sometimes a nil is not a nil

Today I ran into what turns out to be a Go FAQ . First, as a little Go quiz, see if you can tell what this program should print before you run it in the Go playground (I've put the program in a sidebar in case it ever goes away on the playground). The core of the program is this code:

type fake struct { io.Writer }
func fred (logger io.Writer) {
   if logger != nil {
      logger.Write([]byte("..."))
   }
}

func main() {
   var lp *fake
   fred(nil)
   fred(lp)
}

Since Go variables are explicitly created with their zero values, which in the case of pointers such as lp is nil , you would expect this code to run (but do nothing). In fact it crashes on the second call to fred() . What is happening is that sometimes in Go what started out as a nil value and will look like a nil value if you print it straightforwardly is not in fact a nil value. In a nutshell, Go distinguishes between nil interface values and nil values of concrete types that are converted to interfaces . Only the former is really nil and so will compare equal to a plain nil , as fred() attempts to do here.

(As a corollary of this, your concrete methods on (f *fake) can get called with a nil f value. It may be a nil pointer, but it's a typed nil pointer and so it can have methods. It can even have methods through interfaces, as is the case here.)

For the situation I found myself in, the way to deal with this is to change the setup procedure. The real program set up fake conditionally, something like:

var l *sLogger
if smtplog != nil {
   l = &sLogger
   l.prefix = logpref
   l.writer = bufio.NewWriterSize(smtplog, 4096)
}
convo = smtpd.NewConvo(conn, l)

This passes a nil of the concrete type ' * sLogger ' to something that expected an io.Writer , causing interface conversion and hiding the nil . To get around it we can just add a level of indirection with an io.Writer variable that must be explicitly set:

var l2 io.Writer
if smtplog != nil {
   l := &sLogger
   l.prefix = logpref
   l.writer = ....
   l2 = l
}
convo = smtpd.NewConvo(conn, l2)

If we are not initializing our special logging, l2 stays a pure and real io.Writer nil and will be detected as such down in the depths of the code in the smtpd package.

(You can do a similar trick by pulling the setup out into a function that has a return type of io.Writer and explicitly returns a nil in the case of no logging. But you have to return the interface type; if you give your setup function a return type of ' * sLogger ' you'll have the same problem all over again.)

It's a matter of taste if you want to keep nil guard code in your sLogger method functions. In the end I decided that I didn't want to; if I get this sort of initialization wrong in the future in this code, I want a crash so I can fix it.

The other lesson I've learned from this is that if I'm printing values for debugging purposes where I might run into this issue I don't want to use %v as the format specifier, I want to use %#v . The former will print a plain and misleading ' <nil> ' for both the nil interface and nil concrete type cases, while the latter will print ' <nil> ' for the former and something like ' (*main.fake)(nil) ' for the latter.

Sidebar: the test program

package main
import (
    "fmt"
    "io"
)

type fake struct {
    io.Writer
}

func fred(logger io.Writer) {
    if logger != nil {
        logger.Write([]byte("a test\n"))
    }
}

func main() {
    // this is born <nil>
    var t *fake

    fred(nil)
    fmt.Printf("passed 1\n")
    fred(t)
    fmt.Printf("passed 2\n")
}