Go 1.2 Field Selectors and Nil Checks

Russ Cox

July 2013

Abstract

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.

Background

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:

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. That 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. The 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.


Proposed Definition

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.

Rationale

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?

Implementation

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.