In Go, sometimes a nil is not a nil

May 30, 2014

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")
}
Written on 30 May 2014.
« The state of limits on how many groups you can be in (especially for NFS)
One of my test based development imperfections: use driven testing »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri May 30 03:19:39 2014
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.