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