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.