2014-05-30
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 *sLoggerif 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.Writerif 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") }