Go: proposal: Go 2: Safe navigation operator (?.)

Created on 26 Nov 2020  路  18Comments  路  Source: golang/go

Proposal

Add a new operator (?.) to support safe navigation.

Example

package main

type a struct {
    b *b
}

type b struct {
    c int
}

Current Behavior

Navigation across pointer of nil value causes runtime panic.

func main() {
    x := a{&b{1}}
    y := a{}
    println(x.b.c)
    println(y.b.c)
}

x.b.c evaluates to 1
y.b.c panics

1
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x45db4c]

goroutine 1 [running]:
main.main()
    /tmp/sandbox869110152/prog.go:16 +0x6c

Proposed Behavior

Safe navigation across pointer of nil value evaluates to nil value of same type as target property.

func main() {
    x := a{&b{1}}
    y := a{}
    println(x.b?.c)
    println(y.b?.c)
}

x.b?.c evaluates to 1
y.b?.c evaluates to 0 (nil value of type similar to c)

1
0

Reasoning

Null property traversal is a common cause of runtime panic. Frequently developers must disrupt the flow of their program by adding nil checks in order to reproduce the behavior of a safe navigation operator. These nil checks add to the maintenance cost of the program by making the code less readable and introduce new opportunities for error.

Current idiom

func (a *a) getC() int {
    if a.b == nil {
        return 0
    }
    return a.b.c
}

Proposed idiom

func (a *a) getC() int {
    return a.b?.c
}

Similar features in other languages

See https://en.wikipedia.org/wiki/Safe_navigation_operator

Also known as

  • Optional chaining operator
  • Safe call operator
  • Null-conditional operator

Go 2 language change template

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    Experienced

  • What other languages do you have experience with?
    PHP, Javascript, Java, TCL, Bash, Actionscript, Erlang, Python, C++

  • Would this change make Go easier or harder to learn, and why?
    Adds one more operator to learn which should feel familiar to people having experience with analogous operators in other languages popular among Gophers (C#, Ruby, Python, PHP, Typescript, Rust, Scala).

  • Has this idea, or one like it, been proposed before?
    Not found in github issues. One slightly similar golang-nuts post from 2013.

  • If so, how does this proposal differ?
    Previous discussion appears to relate specifically to null pointer receiver methods rather than nested struct traversal or command chaining.

  • Who does this proposal help, and why?
    Developers moving to Go from languages that already support safe navigation.

  • What is the proposed change?
    Add operator ?. (see above).

  • Please describe as precisely as possible the change to the language.
    Add token QUES_PERIOD = "?.". Modify _selector expression_ or add new _safe selector expression_ to achieve behavior described above. Not valid on left hand side.

  • What would change in the language spec?
    Expansion of selector expression definition and operator list

  • Please also describe the change informally, as in a class teaching Go.
    The safe navigation operator?. can be used to short circuit property traversal when a nil pointer is encountered, avoiding panic.

  • Is this change backward compatible?
    Yes.

  • What is the cost of this proposal? (Every language change has a cost).
    Higher maintenance costs due to increase in number of operators and more complex selector expression code.

  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
    Possibly several. Depending on implementation, maybe none.

  • What is the compile time cost?
    Presumably low

  • What is the run time cost?
    Presumably low

  • Can you describe a possible implementation?
    Add new token QUES_PERIOD = "?.". Then either modify ast.SelectorExpr or add new ast.SafeSelectorExpr

  • Do you have a prototype? (This is not required.)
    In development

  • How would the language spec change?
    Expansion of selector expression definition and operator list

  • Orthogonality: how does this change interact or overlap with existing features?
    Syntactically identical with . operator in _selector expressions_ except where the value of the expression is nil (where the . operator would panic)

  • Is the goal of this change a performance improvement?
    No.

  • Does this affect error handling?
    No.

  • Is this about generics?
    No.

Go2 LanguageChange Proposal Proposal-FinalCommentPeriod

Most helpful comment

Using the current generic design draft, we could use a generic function for this, although the syntax is more awkward.

func Deref[T any](p *T) T {
    if p == nil {
        var zero T
        return zero
    }
    return *p
}

This would be used as Deref(a.b).c, which would be equivalent to this proposal's a.b?.c.

All 18 comments

Please note that you should fill https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing a language change.

A few points that I see in need of consideration for this proposal:

  1. Because of struct embedding, a x.y field reference can actually dereference multiple pointers. I assume the semantics of x?.y would provide the zero value if any of the pointers are nil.

  2. What if x.y involves no pointer dereferences? Is it still valid to write x?.y?

  3. What are the semantics of x?.y.z? Does it mean x ? x.y.z : 0 or (x ? x.y : 0).z? (I think in JS it means the former, but that in some other languages it means the latter.)

  4. It looks like some languages also provide ?[ for safe indexing and ?( for safe function/method calls. I'm struggling a little to think how these would actually be used in idiomatic Go code, but seems like a possible future direction to keep in mind.

  5. I think this also relates to previous proposals like #37165. I seem to also remember a proposal for extending && and || to non-boolean types, but I'm not able to find it at the moment.

--

Personal opinion: I'd find this occasionally handy (e.g., I wrote this code yesterday, and I could have replaced 5 lines with just w.string(n.Sym()?.Name)). It's prevalence in other languages suggests it's something worthwhile to consider. On the other hand, Go has eschewed the ternary ?: operator (which is also common in other languages) and instead generally favored more explicit if statements. (For my part, I also occasionally wish Go had ?:.)

A few points that I see in need of consideration for this proposal:

  1. I see.
package main

type a struct{ *b }
type b struct{ *c }
type c struct{ d int }
func main() {
    println(a{&b{&c{1}}}.d) // 1
    println(a{}.d)          // panic
}

The goal is to replace nil pointer dereference panics with nil values of the expected type. I don't think there's any ambiguity in these scenarios about the type to which the expression should evaluate. Still seems feasible?

  1. I think yes. It would emulate . semantics, but should probably only be allowed on the _right hand side_ since safe navigation doesn't make sense for assignment.

  2. So it's either x.y resolves ? x.y.z : 0 or (x.y resolves ? x.y : <nil value of type y>).z The latter is probably simpler to implement. Javascript short circuits the whole expression to untyped null (iirc) which is not feasible in a strongly typed language like Go.

  3. Yes, that might be a natural progression. I chose to omit those operators to constrain the scope of the proposal, focusing on whether safe navigation should be deemed worthy in general.

  4. https://github.com/golang/go/issues/37739 lazy values

@gopherbot please remove label WaitingForInfo.

So it's either x.y resolves ? x.y.z : 0 or (x.y resolves ? x.y : ).z The latter is probably simpler to implement. Javascript short circuits the whole expression to untyped null (iirc) which is not feasible in a strongly typed language like Go.

The question is not what is simpler to implement, but what people reading code will naturally expect. It's hard for me to see that x?.y.z means anything other than (x ? y : <zero value of y's type>).z, but if y is a pointer type then nobody would want to write that.

True. x?.y?.z might be more common where y is a pointer type.

It seems I was mistaken about JavaScript. It looks like ECMA standardized that ({})?.x.y fails with a "Cannot read property y of undefined" error.

But at least according to these TypeScript docs, foo?.bar.baz() means (roughly) foo ? foo.bar.baz() : undefined: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html

I agree with Ian that the JS behavior seems more natural (at least from a language implementor's perspective). But the TypeScript semantics do seem more useful, for the same reason Ian points out. (I assume you could write (foo?.bar).baz() if you really wanted the JS behavior.)

I think If the ?. proposal is passed, it would be good to realize the ternary ?: operator as well. hah

It's worth noting that if we adopted this it would be the first use of ? in Go.

Should we also add x ?/ y which does not panic if y is 0 for integer types?

a?[i] would not panic if i were out of bounds for a slice or array a.

Using the current generic design draft, we could use a generic function for this, although the syntax is more awkward.

func Deref[T any](p *T) T {
    if p == nil {
        var zero T
        return zero
    }
    return *p
}

This would be used as Deref(a.b).c, which would be equivalent to this proposal's a.b?.c.

Is there a generics syntax for a?.b.c with its TypeScript semantics (i.e., a ? a.b.c : 0)? I can't immediately think of any.

a?[i] would not panic if i were out of bounds for a slice or array a.

I'm not sure how serious you are, but I think it would be more consistent to do this with a double assignment, like with maps:

v, _ := a[i]

It's a bit more awkward to stick into the middle of a call where the zero value'll work fine, though. It requires a second line of code.

Edit: You could also do this with a generic function, though:

func At[T any](a []T, i int) T {
  if (i < 0) || (i >= len(a)) {
    var z T
    return z
  }
  return a[i]
}

This also has the benefit of automatically handling nil slices, thanks to len([]T(nil)) being 0.

@mdempsky If I understand you correctly, that is Deref(a).b.c. Am I missing something?

If b is a pointer type, that will panic due to nil pointer dereference. The TypeScript semantics are that the selection chain gets short-circuited if the LHS of the ?. operator is nil. That is, a?.b.c means a ? a.b.c : 0, not (a ? a.b : 0).c.

This would be used as Deref(a.b).c, which would be equivalent to this proposal's a.b?.c.

@ianlancetaylor True. Deref appears a concise and complete expression of the expected behavior of the proposed operator.

@mdempsky Ah, OK, sorry for misunderstanding. But I don't think those semantics make sense for Go.

Based on the discussion above, this is a likely decline. If generics are not added to the language, we can revisit. Leaving open for four weeks for final comments.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jessfraz picture jessfraz  路  154Comments

ghost picture ghost  路  222Comments

adg picture adg  路  816Comments

bradfitz picture bradfitz  路  147Comments

derekperkins picture derekperkins  路  180Comments