blob: a19f66eb305a57f58f1a67c2fbe85608f42731c9 [file] [log] [blame] [view]
# Go 1.2 Field Selectors and Nil Checks
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.
## 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:
```Go
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 panic
* `p2 != nil` (it has pointer value 4); but dereferencing it still
causes a panic
* p3 is not computed: `&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:
```Go
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 successfullyis 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 were 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.