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")
}
|
|