Author: Russ Cox
Last updated: July 2013
Discussion at https://go.dev/issue/4238.
Originally at https://go.dev/s/go12nil.
Implemented in Go 1.2 release.
For Go 1.2, we need to define that, if x
is a pointer to a struct type and x == nil
, &x.Field
causes a runtime panic rather than silently producing an unusable pointer.
Today, if you have:
package main type T struct { Field1 int32 Field2 int32 } type T2 struct { X [1<<24]byte Field int32 } func main() { var x *T p1 := &x.Field1 p2 := &x.Field2 var x2 *T2 p3 := &x2.Field }
then:
p1 == nil
; dereferencing it causes a panicp2 != nil
(it has pointer value 4); but dereferencing it still causes a panic&x2.Field
panics to avoid producing a pointer that might point into mapped memory.The spec does not define what should happen when &x.Field
is evaluated for x == nil
. The answer probably should not depend on Field
’s offset within the struct. The current behavior is at best merely historical accident; it was definitely not thought through or discussed.
Those three behaviors are three possible definitions. The behavior for p2
is clearly undesirable, since it creates unusable pointers that cannot be detected as unusable. hat leaves p1
(&x.Field
is nil
if x
is nil
) and p3
(&x.Field
panics if x
is nil
).
An analogous form of the question concerns &x[i]
where x
is a nil
pointer to an array. he current behaviors match those of the struct exactly, depending in the same way on both the offset of the field and the overall size of the array.
A related question is how &*x
should evaluate when x
is nil
. In C, &*x == x
even when x
is nil
. The spec again is silent. The gc compilers go out of their way to implement the C rule (it seemed like a good idea at a time).
A simplified version of a recent example is:
type T struct { f int64 sync.Mutex } var x *T x.Lock()
The method call turns into (&x.Mutex).Lock()
, which today is passed a receiver with pointer value 8
and panics inside the method, accessing a sync.Mutex
field.
If x
is a nil
pointer to a struct, then evaluating &x.Field
always panics.
If x
is a nil
pointer to an array, then evaluating &x[i]
panics or x[i:j]
panics.
If x
is a nil
pointer, then evaluating &*x
panics.
In general, the result of an evaluation of &expr
either panics or returns a non-nil pointer.
The alternative, defining &x.Field == nil
when x
is nil
, delays the error check. That feels more like something that belongs in a dynamically typed language like Python or JavaScript than in Go. Put another way, it pushes the panic farther away from the problem.
We have not seen a compelling use case for allowing &x.Field == nil
.
Panicking during &x.Field
is no more expensive (perhaps less) than defining &x.Field == nil
.
It is difficult to justify allowing &*x
but not &x.Field
. They are different expressions of the same computation.
The guarantee that &expr
—when it evaluates successfully—is always a non-nil pointer makes intuitive sense and avoids a surprise: how can you take the address of something and get nil
?
The addressable expressions are: “a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array.”
The address of a variable can never be nil
; the address of a slice indexing operation is already checked because a nil
slice will have 0
length, so any index is invalid.
That leaves pointer indirections, field selector of struct, and index of array, confirming at least that we’re considering the complete set of cases.
Assuming x
is in register AX, the current x86 implementation of case p3
is to read from the memory x
points at:
TEST 0(AX), AX
That causes a fault when x
is nil. Unfortunately, it also causes a read from the memory location x
, even if the actual field being addressed is later in memory. This can cause unnecessary cache conflicts if different goroutines own different sections of a large array and one is writing to the first entry.
(It is tempting to use a conditional move instruction:
TEST AX, AX CMOVZ 0, AX
Unfortunately, the definition of the conditional move is that the load is unconditional and only the assignment is conditional, so the fault at address 0
would happen always.)
An alternate implementation would be to test x
itself and use a conditional jump:
TEST AX, AX JNZ ok (branch hint: likely) MOV $0, 0 ok:
This is more code (something like 7 bytes instead of 3) but may run more efficiently, as it avoids spurious memory references and will be predicted easily.
(Note that defining &x.Field == nil
would require at least that much code, if not a little more, except when the offset is 0
.)
It will probably be important to have a basic flow analysis for variables, so that the compiler can avoid re-testing the same pointer over and over in a given function. I started on that general topic a year ago and got a prototype working but then put it aside (the goal then was index bounds check elimination). It could be adapted easily for nil check elimination.