Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
kevburnsjr opened this issue Nov 26, 2020 · 25 comments
Closed

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

kevburnsjr opened this issue Nov 26, 2020 · 25 comments
Labels
LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@kevburnsjr
Copy link

kevburnsjr commented Nov 26, 2020

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.

@gopherbot gopherbot added this to the Proposal milestone Nov 26, 2020
@mvdan
Copy link
Member

mvdan commented Nov 26, 2020

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

@mvdan mvdan added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Nov 26, 2020
@ianlancetaylor ianlancetaylor changed the title proposal: Go2: Safe navigation operator (?.) proposal: Go 2: Safe navigation operator (?.) Nov 26, 2020
@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Nov 26, 2020
@mdempsky
Copy link
Member

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 proposal: Go 2: add ?? operator to select first non-zero value #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 ?:.)

@kevburnsjr
Copy link
Author

kevburnsjr commented Nov 27, 2020

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. proposal: Go 2: lazy values #37739 lazy values

@gopherbot please remove label WaitingForInfo.

@gopherbot gopherbot removed the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Nov 27, 2020
@ianlancetaylor
Copy link
Contributor

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.

@kevburnsjr
Copy link
Author

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

@mdempsky
Copy link
Member

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.)

@kscooo
Copy link

kscooo commented Nov 28, 2020

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

@ianlancetaylor
Copy link
Contributor

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

@ianlancetaylor
Copy link
Contributor

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

@ianlancetaylor
Copy link
Contributor

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

@ianlancetaylor
Copy link
Contributor

ianlancetaylor commented Dec 1, 2020

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.

@mdempsky
Copy link
Member

mdempsky commented Dec 1, 2020

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.

@DeedleFake
Copy link

DeedleFake commented Dec 2, 2020

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.

@ianlancetaylor
Copy link
Contributor

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

@mdempsky
Copy link
Member

mdempsky commented Dec 2, 2020

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.

@kevburnsjr
Copy link
Author

kevburnsjr commented Dec 2, 2020

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.

@ianlancetaylor
Copy link
Contributor

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

@ianlancetaylor
Copy link
Contributor

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.

@0x2539
Copy link

0x2539 commented Dec 23, 2020

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

I find a?.[i] a bit more natural and I think this would also be easier to implement. a?[i] could be harder to understand by the compiler as it might interpret it as a ternary operator and it could throw an error that you are missing the : operator

eg of what compiler wants you to write: a?[i]:[i+1]

@0x2539
Copy link

0x2539 commented Dec 23, 2020

To make the safe navigation operator even more useful would be the addition of "nullable" types. I love how in typescript I can specify that a type can be nullable and then I'm forced to do a null check or use the safe navigation operator when using the object.

If nullable types are implemented then my view of the safe navigation operator would be to return nil (in case of a nil pointer along the way) or the wanted value (when all pointers are declared), eg:

type A struct {
  x int
}

var obj *A | nil
println(obj?.x) // nil
nullableX := obj?.x // nullableX's type would be "int | nil"

The takeaway would be that nullable types would directly impact the implementation of the safe navigation operator because it changes its return type

The closest issue I could find regarding this would be #19412

@vetcher
Copy link

vetcher commented Dec 25, 2020

I also want to mention, that generic Deref does not resolves an issue with navigation in my projects. Commonly we write code that looks like this:

		return PharmacyNetworkRequest{
			Title:           order.GetPharmacyNetwork().GetTitle(),
			Phone:           strings.Join(order.GetPharmacyNetwork().GetPhones(), ", "),
			Image:           imageLinker(order.GetPharmacyNetwork().GetImages()),
			DeliveredAtText: orderDeliveryAtFormat(order.Params.DeliveryAt),
		}

where methods Get*() looks like protoc-gen-go generates:

func (o *Order) GetPharmacyNetwork() *PharmacyNetwork {
	if o == nil {
		return nil
	}
	return o.PharmacyNetwork
}

func (pn *PharmacyNetwork) GetTitle() string {
	if pn == nil {
		return ""
	}
	return pn.Title
}

func (pn *PharmacyNetwork) GetImages() []string {
	if pn == nil {
		return nil
	}
	return pn.Images
}

If the navigation operator is accepted, we will be able to remove this strange code and replace it with ?.

		return PharmacyNetworkRequest{
			Title:           order?.PharmacyNetwork?.Title,
			Phone:           strings.Join(order?.PharmacyNetwork?.Phones, ", "),
			Image:           imageLinker(order?.PharmacyNetwork?.Images),
			DeliveredAtText: orderDeliveryAtFormat(order?.Params?.DeliveryAt),
		}

With Deref it looks ugly:

		return PharmacyNetworkRequest{
			Title:           Deref(Deref(order).PharmacyNetwork).Title,
			Phone:           strings.Join(Deref(Deref(order).PharmacyNetwork).Phones, ", "),
			Image:           imageLinker(Deref(Deref(order).PharmacyNetwork).Images),
			DeliveredAtText: orderDeliveryAtFormat(Deref(Deref(order).Params).DeliveryAt),
		}

@deanveloper
Copy link

As some final input of mine:

I think that my favorite feature of Kotlin, by far, is the ?. operator and all of the null-safe operators/features that Kotlin has. However, the ?. operator isn't very useful in my opinion without the rest of the operators/features that Kotlin has around null safety. The ?. feature really isn't useful to me without a null-coalescing operator to specify what I'd want instead of null, and it isn't clear when it is needed to be used without nullable types. In Kotlin, it's the entire family of null-safety features that make it so powerful, rather than just the ?. operator on its own.

@ianlancetaylor
Copy link
Contributor

There were additional comments, but no change in consensus.

@NV4RE
Copy link

NV4RE commented Jun 23, 2021

Hi, what about something like this ?

cn, ok := order.Sender?.Addresses[0]?.ContactNumber[0]

cn will be zero value of ContactNumber and ok will be false if any of Sender, Addresses[0] or ContactNumber[0] are nil


or safe prefix

cn, ok := safe order.Sender.Addresses[0].ContactNumber[0]

I think still better than

var cn string
if (order.Sender) {
  if (len(order.Sender.Addresses) > 0) {
    if (len(order.Sender.Addresses[0].ContactNumber) > 0) {
     cn = order.Sender.Addresses[0].ContactNumber[0]
    }
  }
}

@deanveloper
Copy link

Typically you'd want to do something more along the lines of

if order == nil {
    // return an "no order found" error
}
if order.Sender == nil {
    // return a "no sender for order" error
}
addresses := sender.Addresses
if len(addresses) == 0 {
    // return a "sender has no addresses" error
}
contactNumbers := addresses[0].ContactNumbers
if len(contactNumbers) == 0 {
    // return a "first address does not have contact numbers" error
}
return contactNumbers[0]

@golang golang locked as resolved and limited conversation to collaborators Jun 23, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests